@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.
package/src/planner.ts ADDED
@@ -0,0 +1,586 @@
1
+ /**
2
+ * BFS planner over the affordance graph.
3
+ *
4
+ * Port of siren-grail/packages/siren-agent/planner.js to TypeScript,
5
+ * extended with cost-aware path selection, preferred tiebreaker (GRAIL spec §5.4),
6
+ * and a buildDefsFromActions() path for client-side planning without a condition map.
7
+ *
8
+ * Pure module — no DOM, no fetch, no side effects.
9
+ */
10
+
11
+ import type { SirenAction, SirenEntity, ConditionMapEntry } from './types.js';
12
+
13
+ // ---------- Types ----------
14
+
15
+ export interface PlanStep {
16
+ readonly name: string;
17
+ readonly params: Readonly<Record<string, string>>;
18
+ key(): string;
19
+ label(): string;
20
+ }
21
+
22
+ export interface AffordanceDef {
23
+ readonly name: string;
24
+ readonly preconditions: readonly string[];
25
+ readonly effects: readonly string[];
26
+ readonly templateEffect: string | null;
27
+ readonly paramName: string | null;
28
+ readonly validValues: readonly string[];
29
+ readonly cost: number;
30
+ readonly preferred: boolean;
31
+ }
32
+
33
+ export interface PlanResult {
34
+ /** Ordered steps to reach the goal (empty if already satisfied) */
35
+ steps: PlanStep[];
36
+ /** Total cost of all steps */
37
+ totalCost: number;
38
+ /** Number of alternative equal-length paths found */
39
+ alternativeCount: number;
40
+ }
41
+
42
+ export interface PlanOptions {
43
+ maxDepth?: number;
44
+ maxQueueSize?: number;
45
+ blockedSteps?: PlanStep[];
46
+ }
47
+
48
+ interface HeuristicMapping {
49
+ property: string;
50
+ op: string;
51
+ value: string | number;
52
+ }
53
+
54
+ // ---------- Factories ----------
55
+
56
+ export function createPlanStep(name: string, params: Record<string, string> = {}): PlanStep {
57
+ const frozenParams = Object.freeze({ ...params });
58
+ return Object.freeze({
59
+ name,
60
+ params: frozenParams,
61
+ key() {
62
+ const entries = Object.entries(frozenParams).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
63
+ if (entries.length === 0) return name;
64
+ return `${name}(${entries.map(([k, v]) => `${k}=${v}`).join(',')})`;
65
+ },
66
+ label() {
67
+ const entries = Object.entries(frozenParams);
68
+ if (entries.length === 0) return name;
69
+ const p = entries.map(([k, v]) => `${k}=${v}`).join(', ');
70
+ return `${name}(${p})`;
71
+ },
72
+ });
73
+ }
74
+
75
+ export function createAffordanceDef(opts: {
76
+ name: string;
77
+ preconditions: string[];
78
+ effects: string[];
79
+ templateEffect?: string | null;
80
+ paramName?: string | null;
81
+ validValues?: string[];
82
+ cost?: number;
83
+ preferred?: boolean;
84
+ }): AffordanceDef {
85
+ return Object.freeze({
86
+ name: opts.name,
87
+ preconditions: Object.freeze([...opts.preconditions]),
88
+ effects: Object.freeze([...opts.effects]),
89
+ templateEffect: opts.templateEffect ?? null,
90
+ paramName: opts.paramName ?? null,
91
+ validValues: Object.freeze([...(opts.validValues ?? [])]),
92
+ cost: opts.cost ?? 1,
93
+ preferred: opts.preferred ?? false,
94
+ });
95
+ }
96
+
97
+ // ---------- State helpers ----------
98
+
99
+ function stateKey(state: Set<string>): string {
100
+ return [...state].sort().join(',');
101
+ }
102
+
103
+ /**
104
+ * Forward simulation: apply affordance effects, return new state.
105
+ * Handles template effect resolution and mutex prefix clearing.
106
+ */
107
+ export function applyEffects(
108
+ state: Set<string>,
109
+ affDef: AffordanceDef,
110
+ params: Record<string, string>,
111
+ mutexPrefixes: Set<string>
112
+ ): Set<string> {
113
+ const next = new Set(state);
114
+
115
+ if (affDef.templateEffect && params && Object.keys(params).length > 0) {
116
+ let resolved = affDef.templateEffect;
117
+ for (const [k, v] of Object.entries(params)) {
118
+ resolved = resolved.replace(`{${k}}`, v);
119
+ }
120
+ const prefix = affDef.templateEffect.slice(0, affDef.templateEffect.indexOf('{'));
121
+ for (const c of next) {
122
+ if (c.startsWith(prefix)) next.delete(c);
123
+ }
124
+ next.add(resolved);
125
+ } else {
126
+ for (const effect of affDef.effects) {
127
+ for (const prefix of mutexPrefixes) {
128
+ if (effect.startsWith(prefix)) {
129
+ for (const c of next) {
130
+ if (c.startsWith(prefix)) next.delete(c);
131
+ }
132
+ break;
133
+ }
134
+ }
135
+ next.add(effect);
136
+ }
137
+ }
138
+
139
+ return next;
140
+ }
141
+
142
+ // ---------- Condition inference ----------
143
+
144
+ /**
145
+ * Infer heuristic condition mappings from condition name structure.
146
+ *
147
+ * - "x.y.z" (3 segments): property=y, op=eq, value=z
148
+ * - "x.y.z" where z="nonempty": property=yCount (singular), op=gt, value=0
149
+ * - "x.y" (2 segments): derived from action-presence
150
+ */
151
+ export function inferHeuristicMappings(
152
+ conditionNames: Set<string>
153
+ ): Record<string, HeuristicMapping> {
154
+ const mappings: Record<string, HeuristicMapping> = {};
155
+ for (const name of conditionNames) {
156
+ const parts = name.split('.');
157
+ if (parts.length === 3) {
158
+ const [, prop, value] = parts;
159
+ if (value === 'nonempty') {
160
+ const singular = prop.endsWith('s') ? prop.slice(0, -1) : prop;
161
+ mappings[name] = { property: `${singular}Count`, op: 'gt', value: 0 };
162
+ } else {
163
+ mappings[name] = { property: prop, op: 'eq', value };
164
+ }
165
+ } else if (parts.length === 2) {
166
+ mappings[name] = { property: '_derived', op: 'action-presence', value: '' };
167
+ }
168
+ }
169
+ return mappings;
170
+ }
171
+
172
+ /**
173
+ * Infer active conditions from a Siren entity.
174
+ * Prefers server-provided x-conditions when available.
175
+ */
176
+ export function inferConditions(
177
+ entity: { properties?: Record<string, unknown>; actions?: unknown[]; 'x-conditions'?: string[] },
178
+ conditionNames: Set<string>,
179
+ mappings?: Record<string, HeuristicMapping>
180
+ ): Set<string> {
181
+ const xc = entity['x-conditions'];
182
+ if (Array.isArray(xc)) {
183
+ return new Set(xc);
184
+ }
185
+
186
+ if (!mappings) {
187
+ mappings = inferHeuristicMappings(conditionNames);
188
+ }
189
+
190
+ const props = entity.properties || {};
191
+ const actions = entity.actions || [];
192
+ const conds = new Set<string>();
193
+
194
+ // First pass: non-derived
195
+ for (const name of conditionNames) {
196
+ const m = mappings[name];
197
+ if (!m || m.property === '_derived') continue;
198
+ const val = props[m.property];
199
+ if (val === undefined || val === null) continue;
200
+ if (m.op === 'eq' && String(val) === String(m.value)) conds.add(name);
201
+ else if (m.op === 'gt') {
202
+ try {
203
+ if (Number(val) > Number(m.value)) conds.add(name);
204
+ } catch {
205
+ /* skip */
206
+ }
207
+ }
208
+ }
209
+
210
+ // Second pass: derived (action-presence)
211
+ for (const name of conditionNames) {
212
+ const m = mappings[name];
213
+ if (!m || m.property !== '_derived') continue;
214
+ if (actions.length > 0) conds.add(name);
215
+ }
216
+
217
+ return conds;
218
+ }
219
+
220
+ // ---------- Template detection ----------
221
+
222
+ /**
223
+ * Detect parameterized template in a list of effect condition names.
224
+ * Returns [template, paramName, validValues] or [null, null, effects].
225
+ */
226
+ function detectTemplate(effects: string[]): [string | null, string | null, string[]] {
227
+ if (effects.length < 2) return [null, null, effects];
228
+
229
+ let prefix = effects[0];
230
+ for (let i = 1; i < effects.length; i++) {
231
+ while (!effects[i].startsWith(prefix)) {
232
+ const dot = prefix.lastIndexOf('.');
233
+ if (dot === -1) return [null, null, effects];
234
+ prefix = prefix.slice(0, dot + 1);
235
+ }
236
+ }
237
+
238
+ if (!prefix.endsWith('.')) return [null, null, effects];
239
+
240
+ const values: string[] = [];
241
+ for (const e of effects) {
242
+ if (!e.startsWith(prefix)) return [null, null, effects];
243
+ values.push(e.slice(prefix.length));
244
+ }
245
+
246
+ const parts = prefix.replace(/\.$/, '').split('.');
247
+ const paramName = parts.length > 0 ? parts[parts.length - 1] : 'value';
248
+ const template = `${prefix}{${paramName}}`;
249
+
250
+ return [template, paramName, values];
251
+ }
252
+
253
+ // ---------- Condition map parsing ----------
254
+
255
+ /**
256
+ * Parse a condition map (from /conditions endpoint) into AffordanceDef objects.
257
+ * Input shape: { conditionName: { description, producedBy: string[], requiredBy: string[] } }
258
+ */
259
+ export function parseConditionMap(conditionMap: Record<string, ConditionMapEntry>): {
260
+ defs: AffordanceDef[];
261
+ conditionNames: Set<string>;
262
+ mutexPrefixes: Set<string>;
263
+ } {
264
+ const conditionNames = new Set(Object.keys(conditionMap));
265
+
266
+ const affNames = new Set<string>();
267
+ for (const info of Object.values(conditionMap)) {
268
+ for (const a of info.producedBy || []) affNames.add(a);
269
+ for (const a of info.requiredBy || []) affNames.add(a);
270
+ }
271
+
272
+ const defs: AffordanceDef[] = [];
273
+ for (const affName of [...affNames].sort()) {
274
+ const preconditions: string[] = [];
275
+ const effects: string[] = [];
276
+
277
+ for (const [condName, info] of Object.entries(conditionMap)) {
278
+ if ((info.requiredBy || []).includes(affName)) preconditions.push(condName);
279
+ if ((info.producedBy || []).includes(affName)) effects.push(condName);
280
+ }
281
+
282
+ preconditions.sort();
283
+ effects.sort();
284
+
285
+ const [templateEffect, paramName, validValues] = detectTemplate(effects);
286
+
287
+ defs.push(
288
+ createAffordanceDef({
289
+ name: affName,
290
+ preconditions,
291
+ effects,
292
+ templateEffect,
293
+ paramName,
294
+ validValues,
295
+ })
296
+ );
297
+ }
298
+
299
+ const mutexPrefixes = new Set<string>();
300
+ for (const d of defs) {
301
+ if (d.templateEffect) {
302
+ mutexPrefixes.add(d.templateEffect.slice(0, d.templateEffect.indexOf('{')));
303
+ }
304
+ }
305
+
306
+ return { defs, conditionNames, mutexPrefixes };
307
+ }
308
+
309
+ // ---------- Build defs from entity actions (simple path) ----------
310
+
311
+ /**
312
+ * Build AffordanceDefs directly from SirenAction[].
313
+ * This is the "simple path" — no condition map or server endpoint required.
314
+ * Reads preconditions, effects, and cost from each action.
315
+ */
316
+ export function buildDefsFromActions(actions: SirenAction[]): {
317
+ defs: AffordanceDef[];
318
+ conditionNames: Set<string>;
319
+ mutexPrefixes: Set<string>;
320
+ } {
321
+ const conditionNames = new Set<string>();
322
+ const defs: AffordanceDef[] = [];
323
+
324
+ for (const action of actions) {
325
+ const preconditions = action.preconditions ?? [];
326
+ const effects = action.effects ?? [];
327
+
328
+ for (const c of preconditions) conditionNames.add(c);
329
+ for (const c of effects) conditionNames.add(c);
330
+
331
+ const [templateEffect, paramName, validValues] = detectTemplate([...effects]);
332
+
333
+ defs.push(
334
+ createAffordanceDef({
335
+ name: action.name,
336
+ preconditions: [...preconditions],
337
+ effects: [...effects],
338
+ templateEffect,
339
+ paramName,
340
+ validValues,
341
+ cost: action.cost ?? 1,
342
+ })
343
+ );
344
+ }
345
+
346
+ const mutexPrefixes = new Set<string>();
347
+ for (const d of defs) {
348
+ if (d.templateEffect) {
349
+ mutexPrefixes.add(d.templateEffect.slice(0, d.templateEffect.indexOf('{')));
350
+ }
351
+ }
352
+
353
+ return { defs, conditionNames, mutexPrefixes };
354
+ }
355
+
356
+ // ---------- BFS planner ----------
357
+
358
+ function availableSteps(
359
+ state: Set<string>,
360
+ defs: AffordanceDef[],
361
+ blockedKeys: Set<string>
362
+ ): PlanStep[] {
363
+ const steps: PlanStep[] = [];
364
+ for (const d of defs) {
365
+ if (!d.preconditions.every(p => state.has(p))) continue;
366
+ if (d.paramName) {
367
+ for (const v of d.validValues) {
368
+ const step = createPlanStep(d.name, { [d.paramName]: v });
369
+ if (!blockedKeys.has(step.key())) steps.push(step);
370
+ }
371
+ } else {
372
+ const step = createPlanStep(d.name);
373
+ if (!blockedKeys.has(step.key())) steps.push(step);
374
+ }
375
+ }
376
+ return steps;
377
+ }
378
+
379
+ /**
380
+ * BFS: find ALL shortest action sequences from start state to goal condition.
381
+ */
382
+ export function plan(
383
+ start: Set<string>,
384
+ goalCondition: string,
385
+ defs: AffordanceDef[],
386
+ mutexPrefixes: Set<string>,
387
+ opts: PlanOptions = {}
388
+ ): PlanStep[][] {
389
+ const { maxDepth = 5, maxQueueSize = 10000, blockedSteps = [] } = opts;
390
+
391
+ if (start.has(goalCondition)) return [[]];
392
+
393
+ const blockedKeys = new Set(blockedSteps.map(s => s.key()));
394
+ const queue: Array<{ state: Set<string>; path: PlanStep[] }> = [{ state: start, path: [] }];
395
+ const visited = new Set([stateKey(start)]);
396
+ const solutions: PlanStep[][] = [];
397
+ let solutionDepth: number | null = null;
398
+
399
+ while (queue.length > 0) {
400
+ if (queue.length > maxQueueSize) return solutions;
401
+
402
+ const { state, path } = queue.shift()!;
403
+ if (solutionDepth !== null && path.length >= solutionDepth) continue;
404
+
405
+ for (const step of availableSteps(state, defs, blockedKeys)) {
406
+ const affDef = defs.find(d => d.name === step.name)!;
407
+ const newState = applyEffects(state, affDef, step.params, mutexPrefixes);
408
+ const newPath = [...path, step];
409
+
410
+ if (newState.has(goalCondition)) {
411
+ solutions.push(newPath);
412
+ solutionDepth = newPath.length;
413
+ continue;
414
+ }
415
+
416
+ const key = stateKey(newState);
417
+ if (!visited.has(key) && newPath.length < maxDepth) {
418
+ visited.add(key);
419
+ queue.push({ state: newState, path: newPath });
420
+ }
421
+ }
422
+ }
423
+
424
+ return solutions;
425
+ }
426
+
427
+ // ---------- Cost-aware planning ----------
428
+
429
+ function pathCost(path: PlanStep[], defs: AffordanceDef[]): number {
430
+ return path.reduce((sum, step) => {
431
+ const def = defs.find(d => d.name === step.name);
432
+ return sum + (def?.cost ?? 1);
433
+ }, 0);
434
+ }
435
+
436
+ function pathPreferredCount(path: PlanStep[], defs: AffordanceDef[]): number {
437
+ return path.reduce((count, step) => {
438
+ const def = defs.find(d => d.name === step.name);
439
+ return count + (def?.preferred ? 1 : 0);
440
+ }, 0);
441
+ }
442
+
443
+ /**
444
+ * Find the lowest-cost shortest path.
445
+ * Among equal-cost paths, prefer ones with more preferred steps (spec §5.4).
446
+ */
447
+ export function planWithCost(
448
+ start: Set<string>,
449
+ goalCondition: string,
450
+ defs: AffordanceDef[],
451
+ mutexPrefixes: Set<string>,
452
+ opts: PlanOptions = {}
453
+ ): PlanResult {
454
+ const paths = plan(start, goalCondition, defs, mutexPrefixes, opts);
455
+
456
+ if (paths.length === 0) {
457
+ return { steps: [], totalCost: 0, alternativeCount: 0 };
458
+ }
459
+
460
+ // Already-satisfied case: plan() returns [[]]
461
+ if (paths.length === 1 && paths[0].length === 0) {
462
+ return { steps: [], totalCost: 0, alternativeCount: 1 };
463
+ }
464
+
465
+ let bestPath = paths[0];
466
+ let bestCost = pathCost(bestPath, defs);
467
+ let bestPreferred = pathPreferredCount(bestPath, defs);
468
+
469
+ for (let i = 1; i < paths.length; i++) {
470
+ const cost = pathCost(paths[i], defs);
471
+ const preferred = pathPreferredCount(paths[i], defs);
472
+ if (cost < bestCost || (cost === bestCost && preferred > bestPreferred)) {
473
+ bestPath = paths[i];
474
+ bestCost = cost;
475
+ bestPreferred = preferred;
476
+ }
477
+ }
478
+
479
+ return {
480
+ steps: bestPath,
481
+ totalCost: bestCost,
482
+ alternativeCount: paths.length,
483
+ };
484
+ }
485
+
486
+ // ---------- Action-level planning ----------
487
+
488
+ /**
489
+ * Plan prerequisite chain for a goal action using a condition map.
490
+ * Uses BFS for each unmet precondition with forward state simulation.
491
+ */
492
+ export function planForAction(
493
+ entity: { properties?: Record<string, unknown>; actions?: unknown[]; 'x-conditions'?: string[] },
494
+ goalActionName: string,
495
+ conditionMap: Record<string, ConditionMapEntry>,
496
+ opts: PlanOptions = {}
497
+ ): PlanStep[] | null {
498
+ const { defs, conditionNames, mutexPrefixes } = parseConditionMap(conditionMap);
499
+ const goalDef = defs.find(d => d.name === goalActionName);
500
+ if (!goalDef) return null;
501
+
502
+ let state = inferConditions(entity, conditionNames);
503
+
504
+ const unmet = goalDef.preconditions.filter(p => !state.has(p));
505
+ if (unmet.length === 0) return [createPlanStep(goalActionName)];
506
+
507
+ const steps: PlanStep[] = [];
508
+ const usedKeys = new Set<string>();
509
+
510
+ for (const precond of unmet) {
511
+ if (state.has(precond)) continue;
512
+ const paths = plan(state, precond, defs, mutexPrefixes, opts);
513
+ if (paths.length === 0) return null;
514
+ for (const step of paths[0]) {
515
+ if (usedKeys.has(step.key())) continue;
516
+ usedKeys.add(step.key());
517
+ steps.push(step);
518
+ const affDef = defs.find(d => d.name === step.name);
519
+ if (affDef) state = applyEffects(state, affDef, step.params, mutexPrefixes);
520
+ }
521
+ }
522
+
523
+ steps.push(createPlanStep(goalActionName));
524
+ return steps;
525
+ }
526
+
527
+ // ---------- Entity-level planning (simple path) ----------
528
+
529
+ /**
530
+ * Plan from entity actions directly — no condition map required.
531
+ * Builds AffordanceDefs from the entity's actions, infers current state,
532
+ * and runs cost-aware BFS for the goal action's unmet preconditions.
533
+ */
534
+ export function planFromEntity(
535
+ entity: SirenEntity & { 'x-conditions'?: string[] },
536
+ goalActionName: string,
537
+ opts: PlanOptions = {}
538
+ ): PlanResult | null {
539
+ const actions = entity.actions ?? [];
540
+ if (actions.length === 0) return null;
541
+
542
+ const goalAction = actions.find(a => a.name === goalActionName);
543
+ if (!goalAction) return null;
544
+
545
+ const goalPreconditions = goalAction.preconditions ?? [];
546
+
547
+ const { defs, conditionNames, mutexPrefixes } = buildDefsFromActions(actions);
548
+
549
+ const state = inferConditions(entity, conditionNames);
550
+
551
+ const unmet = goalPreconditions.filter(p => !state.has(p));
552
+ if (unmet.length === 0) {
553
+ return {
554
+ steps: [createPlanStep(goalActionName)],
555
+ totalCost: goalAction.cost ?? 1,
556
+ alternativeCount: 1,
557
+ };
558
+ }
559
+
560
+ // Plan each unmet precondition with cost-aware selection, forward-simulating state
561
+ let currentState = state;
562
+ const steps: PlanStep[] = [];
563
+ const usedKeys = new Set<string>();
564
+ let totalCost = 0;
565
+
566
+ for (const precond of unmet) {
567
+ if (currentState.has(precond)) continue;
568
+ const result = planWithCost(currentState, precond, defs, mutexPrefixes, opts);
569
+ if (result.steps.length === 0 && !currentState.has(precond)) return null;
570
+ for (const step of result.steps) {
571
+ if (usedKeys.has(step.key())) continue;
572
+ usedKeys.add(step.key());
573
+ steps.push(step);
574
+ const affDef = defs.find(d => d.name === step.name);
575
+ if (affDef) {
576
+ totalCost += affDef.cost;
577
+ currentState = applyEffects(currentState, affDef, step.params, mutexPrefixes);
578
+ }
579
+ }
580
+ }
581
+
582
+ steps.push(createPlanStep(goalActionName));
583
+ totalCost += goalAction.cost ?? 1;
584
+
585
+ return { steps, totalCost, alternativeCount: 1 };
586
+ }
package/src/types.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Type definitions for the BFS planner.
3
+ *
4
+ * These are the subset of Siren/GRAIL types that the planner depends on.
5
+ * Standalone — no external dependencies.
6
+ */
7
+
8
+ export interface SirenEntity {
9
+ class?: string[];
10
+ properties?: Record<string, unknown>;
11
+ actions?: SirenAction[];
12
+ links?: SirenLink[];
13
+ entities?: SirenSubEntity[];
14
+ }
15
+
16
+ export interface SirenAction {
17
+ name: string;
18
+ title?: string;
19
+ method?: string;
20
+ href: string;
21
+ type?: string;
22
+ fields?: SirenField[];
23
+ /** Cooperative affordance: preconditions that must be met */
24
+ preconditions?: string[];
25
+ /** Cooperative affordance: effects of this action */
26
+ effects?: string[];
27
+ /** Cooperative affordance: relative effort weight for planning */
28
+ cost?: number;
29
+ }
30
+
31
+ export interface SirenField {
32
+ name: string;
33
+ type?: string;
34
+ value?: unknown;
35
+ title?: string;
36
+ description?: string;
37
+ options?: unknown[];
38
+ required?: boolean;
39
+ min?: number;
40
+ max?: number;
41
+ step?: number;
42
+ minlength?: number;
43
+ maxlength?: number;
44
+ pattern?: string;
45
+ }
46
+
47
+ export interface SirenLink {
48
+ rel: string[];
49
+ href: string;
50
+ title?: string;
51
+ }
52
+
53
+ export interface SirenSubEntity {
54
+ class?: string[];
55
+ rel?: string[];
56
+ href?: string;
57
+ properties?: Record<string, unknown>;
58
+ }
59
+
60
+ /**
61
+ * The properties block in a GRAIL 409 Blocked response body.
62
+ */
63
+ export interface BlockedProperties {
64
+ status: 409;
65
+ message: string;
66
+ blockedAction: string;
67
+ blockedCondition: string;
68
+ unmetConditions?: string[];
69
+ }
70
+
71
+ /**
72
+ * A 409 Conflict response body from a GRAIL server.
73
+ * Contains the blocked action info and offered affordances.
74
+ */
75
+ export interface BlockedResponse {
76
+ class: string[];
77
+ properties: BlockedProperties;
78
+ actions?: SirenAction[];
79
+ /** Active conditions on this entity (also sent as x-conditions header) */
80
+ 'x-conditions'?: string[];
81
+ }
82
+
83
+ /** Entry in a condition map from the /conditions endpoint */
84
+ export interface ConditionMapEntry {
85
+ description?: string;
86
+ producedBy: string[];
87
+ requiredBy: string[];
88
+ }