@lokascript/planner 2.4.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.
@@ -0,0 +1,605 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ createPlanStep,
4
+ createAffordanceDef,
5
+ applyEffects,
6
+ inferHeuristicMappings,
7
+ inferConditions,
8
+ parseConditionMap,
9
+ buildDefsFromActions,
10
+ plan,
11
+ planWithCost,
12
+ planForAction,
13
+ planFromEntity,
14
+ } from '../planner.js';
15
+ import type { SirenAction, ConditionMapEntry } from '../types.js';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Fixtures: order workflow (matches Go demo)
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const ORDER_CONDITION_MAP: Record<string, ConditionMapEntry> = {
22
+ 'order.mutable': {
23
+ description: 'Order is mutable',
24
+ producedBy: [],
25
+ requiredBy: ['add-item', 'update-order', 'delete-order'],
26
+ },
27
+ 'order.status.pending': {
28
+ description: 'Order status is pending',
29
+ producedBy: ['update-order'],
30
+ requiredBy: [],
31
+ },
32
+ 'order.status.processing': {
33
+ description: 'Order status is processing',
34
+ producedBy: ['update-order'],
35
+ requiredBy: ['ship-order'],
36
+ },
37
+ 'order.status.shipped': {
38
+ description: 'Order status is shipped',
39
+ producedBy: ['ship-order'],
40
+ requiredBy: [],
41
+ },
42
+ 'order.items.nonempty': {
43
+ description: 'Order has items',
44
+ producedBy: ['add-item'],
45
+ requiredBy: ['ship-order'],
46
+ },
47
+ };
48
+
49
+ const ORDER_ACTIONS: SirenAction[] = [
50
+ {
51
+ name: 'add-item',
52
+ href: '/orders/1/items',
53
+ method: 'POST',
54
+ preconditions: ['order.mutable'],
55
+ effects: ['order.items.nonempty'],
56
+ cost: 1,
57
+ },
58
+ {
59
+ name: 'update-order',
60
+ href: '/orders/1',
61
+ method: 'PUT',
62
+ preconditions: ['order.mutable'],
63
+ effects: ['order.status.pending', 'order.status.processing', 'order.status.shipped'],
64
+ cost: 1,
65
+ },
66
+ {
67
+ name: 'ship-order',
68
+ href: '/orders/1/ship',
69
+ method: 'POST',
70
+ preconditions: ['order.status.processing', 'order.items.nonempty'],
71
+ effects: ['order.status.shipped'],
72
+ cost: 2,
73
+ },
74
+ {
75
+ name: 'delete-order',
76
+ href: '/orders/1',
77
+ method: 'DELETE',
78
+ preconditions: ['order.mutable'],
79
+ effects: [],
80
+ cost: 1,
81
+ },
82
+ ];
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // PlanStep
86
+ // ---------------------------------------------------------------------------
87
+
88
+ describe('PlanStep', () => {
89
+ it('label() returns name without params', () => {
90
+ const step = createPlanStep('ship-order');
91
+ expect(step.label()).toBe('ship-order');
92
+ });
93
+
94
+ it('label() includes params', () => {
95
+ const step = createPlanStep('update-order', { status: 'processing' });
96
+ expect(step.label()).toBe('update-order(status=processing)');
97
+ });
98
+
99
+ it('key() is stable regardless of param insertion order', () => {
100
+ const a = createPlanStep('action', { b: '2', a: '1' });
101
+ const b = createPlanStep('action', { a: '1', b: '2' });
102
+ expect(a.key()).toBe(b.key());
103
+ });
104
+
105
+ it('key() without params returns just name', () => {
106
+ const step = createPlanStep('ship-order');
107
+ expect(step.key()).toBe('ship-order');
108
+ });
109
+ });
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // AffordanceDef
113
+ // ---------------------------------------------------------------------------
114
+
115
+ describe('AffordanceDef', () => {
116
+ it('defaults cost to 1 and preferred to false', () => {
117
+ const def = createAffordanceDef({
118
+ name: 'test',
119
+ preconditions: [],
120
+ effects: ['a'],
121
+ });
122
+ expect(def.cost).toBe(1);
123
+ expect(def.preferred).toBe(false);
124
+ });
125
+
126
+ it('preserves cost and preferred', () => {
127
+ const def = createAffordanceDef({
128
+ name: 'test',
129
+ preconditions: [],
130
+ effects: [],
131
+ cost: 5,
132
+ preferred: true,
133
+ });
134
+ expect(def.cost).toBe(5);
135
+ expect(def.preferred).toBe(true);
136
+ });
137
+
138
+ it('freezes arrays', () => {
139
+ const def = createAffordanceDef({
140
+ name: 'test',
141
+ preconditions: ['a'],
142
+ effects: ['b'],
143
+ });
144
+ expect(Object.isFrozen(def.preconditions)).toBe(true);
145
+ expect(Object.isFrozen(def.effects)).toBe(true);
146
+ });
147
+ });
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // applyEffects
151
+ // ---------------------------------------------------------------------------
152
+
153
+ describe('applyEffects', () => {
154
+ it('adds simple effect', () => {
155
+ const def = createAffordanceDef({
156
+ name: 'add-item',
157
+ preconditions: [],
158
+ effects: ['order.items.nonempty'],
159
+ });
160
+ const state = new Set(['order.mutable']);
161
+ const result = applyEffects(state, def, {}, new Set());
162
+ expect(result.has('order.items.nonempty')).toBe(true);
163
+ expect(result.has('order.mutable')).toBe(true);
164
+ });
165
+
166
+ it('resolves template effect and clears mutex', () => {
167
+ const def = createAffordanceDef({
168
+ name: 'update-order',
169
+ preconditions: [],
170
+ effects: ['order.status.pending', 'order.status.processing'],
171
+ templateEffect: 'order.status.{status}',
172
+ paramName: 'status',
173
+ validValues: ['pending', 'processing'],
174
+ });
175
+ const state = new Set(['order.status.pending', 'order.mutable']);
176
+ const result = applyEffects(state, def, { status: 'processing' }, new Set(['order.status.']));
177
+ expect(result.has('order.status.processing')).toBe(true);
178
+ expect(result.has('order.status.pending')).toBe(false);
179
+ expect(result.has('order.mutable')).toBe(true);
180
+ });
181
+
182
+ it('clears mutex prefix for non-template effects', () => {
183
+ const def = createAffordanceDef({
184
+ name: 'ship',
185
+ preconditions: [],
186
+ effects: ['order.status.shipped'],
187
+ });
188
+ const state = new Set(['order.status.processing']);
189
+ const result = applyEffects(state, def, {}, new Set(['order.status.']));
190
+ expect(result.has('order.status.shipped')).toBe(true);
191
+ expect(result.has('order.status.processing')).toBe(false);
192
+ });
193
+ });
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // inferHeuristicMappings
197
+ // ---------------------------------------------------------------------------
198
+
199
+ describe('inferHeuristicMappings', () => {
200
+ it('maps 3-segment eq condition', () => {
201
+ const mappings = inferHeuristicMappings(new Set(['order.status.pending']));
202
+ expect(mappings['order.status.pending']).toEqual({
203
+ property: 'status',
204
+ op: 'eq',
205
+ value: 'pending',
206
+ });
207
+ });
208
+
209
+ it('maps 3-segment nonempty condition', () => {
210
+ const mappings = inferHeuristicMappings(new Set(['order.items.nonempty']));
211
+ expect(mappings['order.items.nonempty']).toEqual({
212
+ property: 'itemCount',
213
+ op: 'gt',
214
+ value: 0,
215
+ });
216
+ });
217
+
218
+ it('maps 2-segment to derived', () => {
219
+ const mappings = inferHeuristicMappings(new Set(['order.mutable']));
220
+ expect(mappings['order.mutable'].property).toBe('_derived');
221
+ });
222
+ });
223
+
224
+ // ---------------------------------------------------------------------------
225
+ // inferConditions
226
+ // ---------------------------------------------------------------------------
227
+
228
+ describe('inferConditions', () => {
229
+ it('prefers x-conditions when present', () => {
230
+ const entity = {
231
+ properties: { status: 'pending' },
232
+ 'x-conditions': ['order.status.processing', 'order.items.nonempty'],
233
+ };
234
+ const result = inferConditions(
235
+ entity,
236
+ new Set(['order.status.pending', 'order.status.processing'])
237
+ );
238
+ expect(result.has('order.status.processing')).toBe(true);
239
+ expect(result.has('order.status.pending')).toBe(false);
240
+ });
241
+
242
+ it('infers 3-segment eq from properties', () => {
243
+ const entity = { properties: { status: 'pending' } };
244
+ const result = inferConditions(
245
+ entity,
246
+ new Set(['order.status.pending', 'order.status.processing'])
247
+ );
248
+ expect(result.has('order.status.pending')).toBe(true);
249
+ expect(result.has('order.status.processing')).toBe(false);
250
+ });
251
+
252
+ it('infers nonempty from count property', () => {
253
+ const entity = { properties: { itemCount: 3 } };
254
+ const result = inferConditions(entity, new Set(['order.items.nonempty']));
255
+ expect(result.has('order.items.nonempty')).toBe(true);
256
+ });
257
+
258
+ it('infers derived condition from action presence', () => {
259
+ const entity = { properties: {}, actions: [{ name: 'foo' }] };
260
+ const result = inferConditions(entity, new Set(['order.mutable']));
261
+ expect(result.has('order.mutable')).toBe(true);
262
+ });
263
+
264
+ it('does not infer derived when no actions', () => {
265
+ const entity = { properties: {}, actions: [] };
266
+ const result = inferConditions(entity, new Set(['order.mutable']));
267
+ expect(result.has('order.mutable')).toBe(false);
268
+ });
269
+ });
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // parseConditionMap
273
+ // ---------------------------------------------------------------------------
274
+
275
+ describe('parseConditionMap', () => {
276
+ it('produces correct AffordanceDefs from order condition map', () => {
277
+ const { defs, conditionNames, mutexPrefixes } = parseConditionMap(ORDER_CONDITION_MAP);
278
+
279
+ expect(conditionNames.size).toBe(5);
280
+ expect(defs.length).toBe(4); // add-item, delete-order, ship-order, update-order
281
+
282
+ const updateDef = defs.find(d => d.name === 'update-order')!;
283
+ expect(updateDef.templateEffect).toBe('order.status.{status}');
284
+ expect(updateDef.paramName).toBe('status');
285
+ // update-order produces pending and processing (shipped is produced by ship-order)
286
+ expect(updateDef.validValues).toContain('pending');
287
+ expect(updateDef.validValues).toContain('processing');
288
+
289
+ expect(mutexPrefixes.has('order.status.')).toBe(true);
290
+ });
291
+
292
+ it('handles empty condition map', () => {
293
+ const { defs, conditionNames } = parseConditionMap({});
294
+ expect(defs).toEqual([]);
295
+ expect(conditionNames.size).toBe(0);
296
+ });
297
+ });
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // buildDefsFromActions
301
+ // ---------------------------------------------------------------------------
302
+
303
+ describe('buildDefsFromActions', () => {
304
+ it('builds defs from SirenAction array', () => {
305
+ const { defs, conditionNames } = buildDefsFromActions(ORDER_ACTIONS);
306
+ expect(defs.length).toBe(4);
307
+ expect(conditionNames.has('order.mutable')).toBe(true);
308
+ expect(conditionNames.has('order.status.shipped')).toBe(true);
309
+ });
310
+
311
+ it('preserves cost from SirenAction', () => {
312
+ const { defs } = buildDefsFromActions(ORDER_ACTIONS);
313
+ const shipDef = defs.find(d => d.name === 'ship-order')!;
314
+ expect(shipDef.cost).toBe(2);
315
+ });
316
+
317
+ it('defaults cost to 1 when not specified', () => {
318
+ const actions: SirenAction[] = [
319
+ { name: 'test', href: '/test', preconditions: [], effects: ['a'] },
320
+ ];
321
+ const { defs } = buildDefsFromActions(actions);
322
+ expect(defs[0].cost).toBe(1);
323
+ });
324
+
325
+ it('detects template effects', () => {
326
+ const { defs } = buildDefsFromActions(ORDER_ACTIONS);
327
+ const updateDef = defs.find(d => d.name === 'update-order')!;
328
+ expect(updateDef.templateEffect).toBe('order.status.{status}');
329
+ });
330
+
331
+ it('handles actions without preconditions/effects', () => {
332
+ const actions: SirenAction[] = [{ name: 'noop', href: '/noop' }];
333
+ const { defs, conditionNames } = buildDefsFromActions(actions);
334
+ expect(defs.length).toBe(1);
335
+ expect(defs[0].preconditions).toEqual([]);
336
+ expect(defs[0].effects).toEqual([]);
337
+ expect(conditionNames.size).toBe(0);
338
+ });
339
+ });
340
+
341
+ // ---------------------------------------------------------------------------
342
+ // plan (BFS)
343
+ // ---------------------------------------------------------------------------
344
+
345
+ describe('plan (BFS)', () => {
346
+ const { defs, mutexPrefixes } = parseConditionMap(ORDER_CONDITION_MAP);
347
+
348
+ it('returns [[]] when goal already satisfied', () => {
349
+ const start = new Set(['order.status.shipped']);
350
+ const result = plan(start, 'order.status.shipped', defs, mutexPrefixes);
351
+ expect(result).toEqual([[]]);
352
+ });
353
+
354
+ it('finds single-step path', () => {
355
+ const start = new Set(['order.mutable']);
356
+ const result = plan(start, 'order.items.nonempty', defs, mutexPrefixes);
357
+ expect(result.length).toBeGreaterThanOrEqual(1);
358
+ expect(result[0].length).toBe(1);
359
+ expect(result[0][0].name).toBe('add-item');
360
+ });
361
+
362
+ it('finds multi-step path to ship-order', () => {
363
+ // Need: order.status.processing + order.items.nonempty
364
+ // From: order.mutable + order.status.pending
365
+ const start = new Set(['order.mutable', 'order.status.pending']);
366
+ const result = plan(start, 'order.status.shipped', defs, mutexPrefixes);
367
+ expect(result.length).toBeGreaterThanOrEqual(1);
368
+ // Should be 3 steps: add-item, update-order(processing), ship-order
369
+ const path = result[0];
370
+ expect(path.length).toBe(3);
371
+ const names = path.map(s => s.name);
372
+ expect(names).toContain('add-item');
373
+ expect(names).toContain('update-order');
374
+ expect(names).toContain('ship-order');
375
+ });
376
+
377
+ it('returns [] when goal unreachable', () => {
378
+ // No way to reach ship-order without order.mutable
379
+ const start = new Set<string>();
380
+ const result = plan(start, 'order.status.shipped', defs, mutexPrefixes);
381
+ expect(result).toEqual([]);
382
+ });
383
+
384
+ it('respects blocked steps', () => {
385
+ const start = new Set(['order.mutable']);
386
+ const blocked = [createPlanStep('add-item')];
387
+ const result = plan(start, 'order.items.nonempty', defs, mutexPrefixes, {
388
+ blockedSteps: blocked,
389
+ });
390
+ // add-item is the only way to reach order.items.nonempty, so no path
391
+ expect(result).toEqual([]);
392
+ });
393
+
394
+ it('respects maxDepth', () => {
395
+ const start = new Set(['order.mutable', 'order.status.pending']);
396
+ const result = plan(start, 'order.status.shipped', defs, mutexPrefixes, {
397
+ maxDepth: 2,
398
+ });
399
+ // Needs 3 steps, but maxDepth is 2
400
+ expect(result).toEqual([]);
401
+ });
402
+
403
+ it('returns all shortest paths', () => {
404
+ // With the update-order template, there may be multiple paths of same length
405
+ const start = new Set(['order.mutable', 'order.status.pending']);
406
+ const result = plan(start, 'order.status.shipped', defs, mutexPrefixes);
407
+ // All paths should be the same length
408
+ if (result.length > 1) {
409
+ const len = result[0].length;
410
+ for (const path of result) {
411
+ expect(path.length).toBe(len);
412
+ }
413
+ }
414
+ });
415
+ });
416
+
417
+ // ---------------------------------------------------------------------------
418
+ // planWithCost
419
+ // ---------------------------------------------------------------------------
420
+
421
+ describe('planWithCost', () => {
422
+ it('selects lowest-cost path', () => {
423
+ // Create two paths to the same goal with different costs
424
+ const cheapDef = createAffordanceDef({
425
+ name: 'cheap',
426
+ preconditions: [],
427
+ effects: ['goal'],
428
+ cost: 1,
429
+ });
430
+ const expensiveDef = createAffordanceDef({
431
+ name: 'expensive',
432
+ preconditions: [],
433
+ effects: ['goal'],
434
+ cost: 10,
435
+ });
436
+ const start = new Set<string>();
437
+ const result = planWithCost(start, 'goal', [cheapDef, expensiveDef], new Set());
438
+ expect(result.steps.length).toBe(1);
439
+ expect(result.steps[0].name).toBe('cheap');
440
+ expect(result.totalCost).toBe(1);
441
+ expect(result.alternativeCount).toBe(2);
442
+ });
443
+
444
+ it('prefers preferred steps at equal cost', () => {
445
+ const regular = createAffordanceDef({
446
+ name: 'regular',
447
+ preconditions: [],
448
+ effects: ['goal'],
449
+ cost: 1,
450
+ preferred: false,
451
+ });
452
+ const preferred = createAffordanceDef({
453
+ name: 'preferred',
454
+ preconditions: [],
455
+ effects: ['goal'],
456
+ cost: 1,
457
+ preferred: true,
458
+ });
459
+ const start = new Set<string>();
460
+ const result = planWithCost(start, 'goal', [regular, preferred], new Set());
461
+ expect(result.steps[0].name).toBe('preferred');
462
+ });
463
+
464
+ it('returns empty result when no paths', () => {
465
+ const def = createAffordanceDef({
466
+ name: 'blocked',
467
+ preconditions: ['impossible'],
468
+ effects: ['goal'],
469
+ });
470
+ const result = planWithCost(new Set(), 'goal', [def], new Set());
471
+ expect(result.steps).toEqual([]);
472
+ expect(result.alternativeCount).toBe(0);
473
+ });
474
+
475
+ it('returns empty steps when already satisfied', () => {
476
+ const def = createAffordanceDef({
477
+ name: 'unused',
478
+ preconditions: [],
479
+ effects: ['goal'],
480
+ });
481
+ const result = planWithCost(new Set(['goal']), 'goal', [def], new Set());
482
+ expect(result.steps).toEqual([]);
483
+ expect(result.totalCost).toBe(0);
484
+ expect(result.alternativeCount).toBe(1);
485
+ });
486
+ });
487
+
488
+ // ---------------------------------------------------------------------------
489
+ // planForAction (condition map path)
490
+ // ---------------------------------------------------------------------------
491
+
492
+ describe('planForAction', () => {
493
+ it('returns just the goal when all preconditions met', () => {
494
+ const entity = {
495
+ 'x-conditions': ['order.status.processing', 'order.items.nonempty'],
496
+ };
497
+ const result = planForAction(entity, 'ship-order', ORDER_CONDITION_MAP);
498
+ expect(result).not.toBeNull();
499
+ expect(result!.length).toBe(1);
500
+ expect(result![0].name).toBe('ship-order');
501
+ });
502
+
503
+ it('plans prerequisite chain', () => {
504
+ const entity = {
505
+ 'x-conditions': ['order.mutable', 'order.status.pending'],
506
+ };
507
+ const result = planForAction(entity, 'ship-order', ORDER_CONDITION_MAP);
508
+ expect(result).not.toBeNull();
509
+ // Should include add-item, update-order(status=processing), ship-order
510
+ const names = result!.map(s => s.name);
511
+ expect(names).toContain('add-item');
512
+ expect(names).toContain('update-order');
513
+ expect(names[names.length - 1]).toBe('ship-order');
514
+ });
515
+
516
+ it('produces parameterized steps from template effects', () => {
517
+ const entity = {
518
+ 'x-conditions': ['order.mutable', 'order.items.nonempty'],
519
+ };
520
+ const result = planForAction(entity, 'ship-order', ORDER_CONDITION_MAP);
521
+ expect(result).not.toBeNull();
522
+ const updateStep = result!.find(s => s.name === 'update-order');
523
+ expect(updateStep).toBeDefined();
524
+ expect(updateStep!.params.status).toBe('processing');
525
+ });
526
+
527
+ it('returns null for unknown action', () => {
528
+ const entity = { 'x-conditions': ['order.mutable'] };
529
+ const result = planForAction(entity, 'nonexistent', ORDER_CONDITION_MAP);
530
+ expect(result).toBeNull();
531
+ });
532
+
533
+ it('returns null when precondition unreachable', () => {
534
+ // ship-order needs order.status.processing + order.items.nonempty
535
+ // But order.mutable is not active, so add-item and update-order are blocked
536
+ const entity = { 'x-conditions': [] as string[] };
537
+ const result = planForAction(entity, 'ship-order', ORDER_CONDITION_MAP);
538
+ expect(result).toBeNull();
539
+ });
540
+ });
541
+
542
+ // ---------------------------------------------------------------------------
543
+ // planFromEntity (simple path)
544
+ // ---------------------------------------------------------------------------
545
+
546
+ describe('planFromEntity', () => {
547
+ it('plans from entity actions directly', () => {
548
+ const entity = {
549
+ actions: ORDER_ACTIONS,
550
+ 'x-conditions': ['order.mutable', 'order.status.pending'] as string[],
551
+ };
552
+ const result = planFromEntity(entity, 'ship-order');
553
+ expect(result).not.toBeNull();
554
+ const names = result!.steps.map(s => s.name);
555
+ expect(names).toContain('add-item');
556
+ expect(names[names.length - 1]).toBe('ship-order');
557
+ expect(result!.totalCost).toBeGreaterThan(0);
558
+ });
559
+
560
+ it('returns null when goal action not found', () => {
561
+ const entity = { actions: ORDER_ACTIONS };
562
+ const result = planFromEntity(entity, 'nonexistent');
563
+ expect(result).toBeNull();
564
+ });
565
+
566
+ it('returns null when entity has no actions', () => {
567
+ const entity = { actions: [] as SirenAction[] };
568
+ const result = planFromEntity(entity, 'ship-order');
569
+ expect(result).toBeNull();
570
+ });
571
+
572
+ it('returns single-step when all preconditions met', () => {
573
+ const entity = {
574
+ actions: ORDER_ACTIONS,
575
+ 'x-conditions': ['order.status.processing', 'order.items.nonempty'] as string[],
576
+ };
577
+ const result = planFromEntity(entity, 'ship-order');
578
+ expect(result).not.toBeNull();
579
+ expect(result!.steps.length).toBe(1);
580
+ expect(result!.steps[0].name).toBe('ship-order');
581
+ expect(result!.totalCost).toBe(2); // ship-order cost
582
+ });
583
+
584
+ it('returns null when preconditions unreachable', () => {
585
+ // ship-order needs order.status.processing + order.items.nonempty
586
+ // add-item and update-order need order.mutable which is not active
587
+ const entity = {
588
+ actions: ORDER_ACTIONS,
589
+ 'x-conditions': [] as string[],
590
+ };
591
+ const result = planFromEntity(entity, 'ship-order');
592
+ expect(result).toBeNull();
593
+ });
594
+
595
+ it('includes cost in result', () => {
596
+ const entity = {
597
+ actions: ORDER_ACTIONS,
598
+ 'x-conditions': ['order.mutable', 'order.status.pending'] as string[],
599
+ };
600
+ const result = planFromEntity(entity, 'ship-order');
601
+ expect(result).not.toBeNull();
602
+ // add-item(1) + update-order(1) + ship-order(2) = 4
603
+ expect(result!.totalCost).toBe(4);
604
+ });
605
+ });
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @lokascript/planner — BFS planner over affordance graphs.
3
+ *
4
+ * Pure module — no DOM, no fetch, no side effects.
5
+ * Used by GRAIL MCP tools and Siren hypermedia clients.
6
+ */
7
+
8
+ // Types (Siren/GRAIL primitives)
9
+ export type {
10
+ SirenEntity,
11
+ SirenAction,
12
+ SirenField,
13
+ SirenLink,
14
+ SirenSubEntity,
15
+ BlockedProperties,
16
+ BlockedResponse,
17
+ ConditionMapEntry,
18
+ } from './types.js';
19
+
20
+ // Planner types
21
+ export type { PlanStep, AffordanceDef, PlanResult, PlanOptions } from './planner.js';
22
+
23
+ // Planner functions
24
+ export {
25
+ createPlanStep,
26
+ createAffordanceDef,
27
+ applyEffects,
28
+ inferHeuristicMappings,
29
+ inferConditions,
30
+ parseConditionMap,
31
+ buildDefsFromActions,
32
+ plan,
33
+ planWithCost,
34
+ planForAction,
35
+ planFromEntity,
36
+ } from './planner.js';