@almadar/core 8.6.2 → 8.6.3

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.
@@ -181,7 +181,27 @@ interface FactoryConfigParam {
181
181
  * Stays a single free-form string; consumers decide their own
182
182
  * splitting/formatting policy. */
183
183
  synonyms?: string;
184
+ /** Visibility tier authored in `.lolo` as `@tier "..."` next to the
185
+ * knob declaration. Drives the studio Questionnaire's filter +
186
+ * disclosure: `internal` knobs are hidden entirely, `advanced` knobs
187
+ * collapse under a "Show advanced" toggle, `essential` /
188
+ * `customization` knobs render inline. Untagged knobs are treated as
189
+ * `customization` by the studio (current visible behavior). */
190
+ tier?: FactoryConfigTier;
184
191
  }
192
+ /**
193
+ * Audience tier for a `FactoryConfigParam`. Source-tagged via the
194
+ * `@tier "..."` annotation in `.lolo` — no heuristic inference on the
195
+ * consumer side. Missing tag = author did not tag = treat as
196
+ * `customization` at render time.
197
+ *
198
+ * - `essential` — every project of this kind needs to answer (app
199
+ * name, entity fields, primary nav items).
200
+ * - `customization` — meaningful refinement most projects will tweak.
201
+ * - `advanced` — sensible defaults; only specialised projects override.
202
+ * - `internal` — platform-internal; the studio never surfaces these.
203
+ */
204
+ type FactoryConfigTier = 'essential' | 'customization' | 'advanced' | 'internal';
185
205
  /**
186
206
  * One trait the factory composes into the orbital. The projector reads
187
207
  * these to determine which factory's trait stack covers a given
@@ -321,66 +341,187 @@ type FactoryParamValue = string | number | boolean | ReadonlyArray<FactoryParamV
321
341
  };
322
342
 
323
343
  /**
324
- * The per-orbital binding the agent assembles when it picks a
325
- * signature. Carries the user-authored overrides directly; no shared
326
- * domain ontology, no DomainDocument.
344
+ * Typed questionnaire surface shapes the studio renders + answers.
345
+ *
346
+ * Each `DomainQuestion` is one open slot derived deterministically from
347
+ * a `FactoryCallSite` + its matching `FactorySignature`. The studio
348
+ * renders the widget keyed off `inputType` (with optional structural
349
+ * carriers `itemSchema` / `objectSchema` for complex types), and the
350
+ * user's answer flows through `answersToMutations` into a typed
351
+ * `FactoryCallPlanMutation`.
352
+ *
353
+ * Relocated from `@almadar-io/agent/analysis/questions/` — the
354
+ * function is a pure transform over `@almadar/core` factory types
355
+ * and belongs alongside them.
356
+ *
357
+ * @packageDocumentation
327
358
  */
328
- interface TranslationBinding {
329
- /** Entity name override. When equal to `signature.entities[0].name`,
330
- * no rename is emitted. */
331
- entityName: string;
332
- /** Extra fields beyond the signature's canonical entity surface. */
333
- entityFields?: ReadonlyArray<EntityField>;
334
- /** Persistence override. */
335
- persistence?: EntityPersistence;
336
- /** Collection name override. */
337
- collection?: string;
338
- /** Per-page path overrides keyed by `signature.pages[i].name`. */
339
- pagePaths?: Readonly<Record<string, string>>;
340
- }
341
- interface TranslationWarning {
342
- /** Dotted path of the binding field that couldn't be lowered. */
343
- field: string;
344
- /** Human-readable reason. */
359
+
360
+ /**
361
+ * One open slot in the questionnaire. The studio renders the question
362
+ * + input widget keyed off `inputType` + `suggestedAnswers` (and, for
363
+ * structured types, `itemSchema` / `objectSchema`); the answer fills
364
+ * the matching `mutationTemplate`.
365
+ */
366
+ interface DomainQuestion {
367
+ /** Unique within a session. Keys the answer record. */
368
+ id: string;
369
+ /** Natural-language prompt shown to the user. */
370
+ question: string;
371
+ /** One short sentence — why this question matters. */
345
372
  reason: string;
373
+ /** Orbital this question scopes to (matches
374
+ * `FactoryCallSite.orbital`). */
375
+ orbitalName: string;
376
+ /** Free-form capability tag this question explores. Set when the
377
+ * question was emitted from a factory trait's `capabilities[]`
378
+ * advertisement (e.g. `'privacy'`, `'audit'`). Undefined for
379
+ * config-key-driven questions. */
380
+ capability?: string;
381
+ /** Drives the form widget. Matches a narrow type set the questionnaire
382
+ * UI knows how to render. */
383
+ inputType: DomainQuestionInputType;
384
+ /** Typed slot for the answer. `answersToMutations` switches on the
385
+ * template's `kind` to produce the matching
386
+ * `FactoryCallPlanMutation`. */
387
+ mutationTemplate: FactoryCallPlanMutationTemplate;
388
+ /** Canonical default value from the factory signature — the studio
389
+ * pre-fills the input, the user can confirm or override. */
390
+ defaultValue?: DomainQuestionAnswer;
391
+ /** Closed-enum or curated suggestions. Empty when the input is
392
+ * free-form. */
393
+ suggestedAnswers?: ReadonlyArray<string>;
394
+ /** Free-text help shown under the input. */
395
+ helpText?: string;
396
+ /** Per-element schema for array-of-objects (`listOfObjects`) and
397
+ * free-form chip lists (`tagList`). Propagated from
398
+ * `FactoryConfigParam.items`. */
399
+ itemSchema?: EntityField;
400
+ /** Per-property schema for structured objects (`objectForm`).
401
+ * Propagated from `FactoryConfigParam.properties`. */
402
+ objectSchema?: Readonly<Record<string, EntityField>>;
403
+ /** Visibility tier propagated from `FactoryConfigParam.tier`.
404
+ * Undefined when the source `.lolo` did not author an explicit
405
+ * `@tier` annotation — the studio treats undefined as
406
+ * `'customization'`. */
407
+ tier?: FactoryConfigTier;
346
408
  }
347
- interface TranslationResult {
348
- callSite: FactoryCallSite;
349
- warnings: ReadonlyArray<TranslationWarning>;
350
- }
351
- declare function translateOverlaysToParams(binding: TranslationBinding, signature: FactorySignature, presentation?: PresentationOverlay, ruleOverlay?: RuleOverlay, traitOverlay?: TraitOverlay, catalog?: ReadonlyArray<FactorySignature>): TranslationResult;
352
-
353
409
  /**
354
- * Structural diff between two `FactoryCallSite[]` lists. Pure typed,
355
- * no side effects. Used by downstream consumers (agent planner, UI
356
- * cascade view) to turn a "previous calls" + "next calls" pair into a
357
- * minimal change set.
410
+ * Narrow set of widget kinds the studio renders.
358
411
  *
359
- * Identity for diffing is `(organism, orbital)`. Renames are not
360
- * handled here — they show up as a delete + add. The agent layer is
361
- * expected to merge those back into a `'rename'` op if it can prove
362
- * the underlying domain entity was renamed.
412
+ * The derivation in `generate.ts:deriveInputType` reads structural
413
+ * metadata (`param.items.properties`, `param.properties`,
414
+ * `param.enumValues`) on top of `param.type` so authors can ship a
415
+ * `[NavItem]` field and have the studio render a per-element form,
416
+ * not a JSON textarea.
363
417
  */
364
-
365
- type CallSiteDiff = {
366
- kind: 'add';
418
+ type DomainQuestionInputType = 'text' | 'number' | 'boolean' | 'enum' | 'persistence' | 'capability' | 'multiselect' | 'tagList' | 'fieldList' | 'urlPath' | 'listOfObjects' | 'objectForm';
419
+ /**
420
+ * One variant per `FactoryCallPlanMutation.kind` the questionnaire
421
+ * surface supports. Each variant carries the discriminant pieces of
422
+ * the target mutation that are known at question-generation time
423
+ * (e.g. the orbital we're editing). The answer fills the remaining
424
+ * value in `answersToMutations`.
425
+ */
426
+ type FactoryCallPlanMutationTemplate = {
427
+ kind: 'set-orbital-entity-name';
367
428
  orbitalName: string;
368
- next: FactoryCallSite;
369
429
  } | {
370
- kind: 'delete';
430
+ kind: 'set-orbital-entity-fields';
371
431
  orbitalName: string;
372
- prior: FactoryCallSite;
373
432
  } | {
374
- kind: 'edit';
433
+ kind: 'set-orbital-persistence';
375
434
  orbitalName: string;
376
- prior: FactoryCallSite;
377
- next: FactoryCallSite;
378
435
  } | {
379
- kind: 'keep';
436
+ kind: 'set-orbital-collection';
380
437
  orbitalName: string;
381
- call: FactoryCallSite;
438
+ } | {
439
+ kind: 'set-orbital-page-path';
440
+ orbitalName: string;
441
+ pageName: string;
442
+ } | {
443
+ kind: 'set-trait-override-config';
444
+ orbitalName: string;
445
+ traitName: string;
446
+ configKey: string;
447
+ } | {
448
+ kind: 'add-extra-trait';
449
+ orbitalName: string;
450
+ } | {
451
+ kind: 'remove-extra-trait';
452
+ orbitalName: string;
453
+ } | {
454
+ kind: 'set-rule';
455
+ capability: string;
456
+ appliesTo: ReadonlyArray<string>;
457
+ } | {
458
+ kind: 'remove-rule';
459
+ ruleId: string;
382
460
  };
383
- declare function diffFactoryCalls(prior: ReadonlyArray<FactoryCallSite>, next: ReadonlyArray<FactoryCallSite>): ReadonlyArray<CallSiteDiff>;
461
+ /**
462
+ * Typed answer shape. The studio coerces the input into one of these
463
+ * before submitting. `answersToMutations` narrows on the matching
464
+ * `mutationTemplate.kind`.
465
+ *
466
+ * `null` means "answer skipped" (UI honours, reducer emits no
467
+ * mutation).
468
+ */
469
+ type DomainQuestionAnswer = string | EntityPersistence | ReadonlyArray<EntityField> | TraitReference | FactoryParamValue | null;
470
+ /**
471
+ * The shape passed to `answersToMutations` — keyed by
472
+ * `DomainQuestion.id`. Questions the user skipped MAY be omitted
473
+ * entirely OR carry `null`; both behave the same downstream.
474
+ */
475
+ type DomainQuestionAnswers = Readonly<Record<string, DomainQuestionAnswer>>;
476
+
477
+ /**
478
+ * `generateQuestions(plan, catalog, ruleOverlay?)`.
479
+ *
480
+ * Deterministic, no LLM call. Walks each `FactoryCallSite` in the
481
+ * durable plan against its matching `FactorySignature` and emits one
482
+ * open question per advertised knob the user hasn't already filled.
483
+ *
484
+ * Two families of questions, both source-derived:
485
+ *
486
+ * 1. CONFIG-KEY — one question per
487
+ * `signature.traits[*].overridableConfigKeys[*]`. Each entry is a
488
+ * `FactoryConfigParam` carrying `{ key, type, default?, label?,
489
+ * description?, enumValues?, items?, properties?, tier? }` lifted
490
+ * directly from the source `.lolo` `config { }` block. The widget
491
+ * choice falls out of `deriveInputType` (reads `items.properties`
492
+ * and `properties` so structured types render proper editors, not
493
+ * JSON textareas); the prompt comes from `label` (or a humanized
494
+ * fallback of `key`); the default value pre-fills the widget.
495
+ * Knobs tagged `tier: 'internal'` are filtered out entirely.
496
+ *
497
+ * 2. CAPABILITY-DRIVEN — one question per unique
498
+ * `signature.traits[*].capabilities[*]` tag NOT already covered
499
+ * by `ruleOverlay.rules[].capability`. Surfaces a `set-rule`
500
+ * mutation template scoped to the bound entity.
501
+ *
502
+ * @packageDocumentation
503
+ */
504
+
505
+ declare function generateQuestions(plan: ReadonlyArray<FactoryCallSite>, catalog: ReadonlyArray<FactorySignature>, ruleOverlay?: RuleOverlay): DomainQuestion[];
506
+ /**
507
+ * Pick a widget for a config-key question from the param's typed
508
+ * declaration + its structural carriers. The type tag is lifted
509
+ * verbatim from `.lolo`; `items.properties` and `properties` carry the
510
+ * per-element / per-property schema for structured types so the studio
511
+ * can render a real form instead of a JSON textarea.
512
+ *
513
+ * - `[T]` with `items.properties` → `listOfObjects` (repeatable form)
514
+ * - `[T]` with `enumValues` → `multiselect` (closed checkboxes)
515
+ * - `[T]` otherwise → `tagList` (free-form chips)
516
+ * - `object` with `properties` → `objectForm` (per-property form)
517
+ * - `object` otherwise → `text` (rare; flagged by tests)
518
+ * - `number` / `integer` → `number`
519
+ * - `boolean` → `boolean`
520
+ * - `enum` → `enum`
521
+ * - `string` with `enumValues` → `enum`
522
+ * - `string` → `text`
523
+ */
524
+ declare function deriveInputType(param: FactoryConfigParam): DomainQuestionInputType;
384
525
 
385
526
  /**
386
527
  * One slot the agent emits per orbital. Slim shape — agent-facing,
@@ -462,4 +603,79 @@ interface FactoryCallPlanState {
462
603
  */
463
604
  declare function applyFactoryCallPlanMutation(state: FactoryCallPlanState, m: FactoryCallPlanMutation): FactoryCallPlanState;
464
605
 
465
- export { type CallSiteDiff, type FactoryCallPlanMutation, type FactoryCallPlanState, type FactoryCallSite, type FactoryCallSiteParams, type FactoryConfigParam, type FactoryEntitySignature, type FactoryPageSignature, type FactoryParamValue, type FactorySignature, type FactorySignatureCatalog, type FactorySignatureEntityField, type FactoryTraitSignature, type JsonSchema, type JsonSchemaType, type JsonValue, type OrbitalCallInput, type OwnershipOverlayEntry, type PresentationNavItem, type PresentationOverlay, type RuleOverlay, type RuleOverlayEntry, type SchemaFieldType, type TraitOverlay, type TraitOverlayEntry, type TraitOverlayListener, type TranslationBinding, type TranslationResult, type TranslationWarning, applyFactoryCallPlanMutation, diffFactoryCalls, translateOverlaysToParams };
606
+ /**
607
+ * `answersToMutations(answers, questions)` — turns a
608
+ * `DomainQuestionAnswers` record into a typed
609
+ * `FactoryCallPlanMutation[]`. Each question's `mutationTemplate`
610
+ * matches one variant of the union; the answer fills in the value.
611
+ * `null` (or omitted) answers produce no mutation.
612
+ *
613
+ * @packageDocumentation
614
+ */
615
+
616
+ declare function answersToMutations(answers: DomainQuestionAnswers, questions: ReadonlyArray<DomainQuestion>): FactoryCallPlanMutation[];
617
+ declare function answerToMutations(template: FactoryCallPlanMutationTemplate, answer: DomainQuestionAnswer, _question: DomainQuestion): FactoryCallPlanMutation[];
618
+
619
+ /**
620
+ * The per-orbital binding the agent assembles when it picks a
621
+ * signature. Carries the user-authored overrides directly; no shared
622
+ * domain ontology, no DomainDocument.
623
+ */
624
+ interface TranslationBinding {
625
+ /** Entity name override. When equal to `signature.entities[0].name`,
626
+ * no rename is emitted. */
627
+ entityName: string;
628
+ /** Extra fields beyond the signature's canonical entity surface. */
629
+ entityFields?: ReadonlyArray<EntityField>;
630
+ /** Persistence override. */
631
+ persistence?: EntityPersistence;
632
+ /** Collection name override. */
633
+ collection?: string;
634
+ /** Per-page path overrides keyed by `signature.pages[i].name`. */
635
+ pagePaths?: Readonly<Record<string, string>>;
636
+ }
637
+ interface TranslationWarning {
638
+ /** Dotted path of the binding field that couldn't be lowered. */
639
+ field: string;
640
+ /** Human-readable reason. */
641
+ reason: string;
642
+ }
643
+ interface TranslationResult {
644
+ callSite: FactoryCallSite;
645
+ warnings: ReadonlyArray<TranslationWarning>;
646
+ }
647
+ declare function translateOverlaysToParams(binding: TranslationBinding, signature: FactorySignature, presentation?: PresentationOverlay, ruleOverlay?: RuleOverlay, traitOverlay?: TraitOverlay, catalog?: ReadonlyArray<FactorySignature>): TranslationResult;
648
+
649
+ /**
650
+ * Structural diff between two `FactoryCallSite[]` lists. Pure typed,
651
+ * no side effects. Used by downstream consumers (agent planner, UI
652
+ * cascade view) to turn a "previous calls" + "next calls" pair into a
653
+ * minimal change set.
654
+ *
655
+ * Identity for diffing is `(organism, orbital)`. Renames are not
656
+ * handled here — they show up as a delete + add. The agent layer is
657
+ * expected to merge those back into a `'rename'` op if it can prove
658
+ * the underlying domain entity was renamed.
659
+ */
660
+
661
+ type CallSiteDiff = {
662
+ kind: 'add';
663
+ orbitalName: string;
664
+ next: FactoryCallSite;
665
+ } | {
666
+ kind: 'delete';
667
+ orbitalName: string;
668
+ prior: FactoryCallSite;
669
+ } | {
670
+ kind: 'edit';
671
+ orbitalName: string;
672
+ prior: FactoryCallSite;
673
+ next: FactoryCallSite;
674
+ } | {
675
+ kind: 'keep';
676
+ orbitalName: string;
677
+ call: FactoryCallSite;
678
+ };
679
+ declare function diffFactoryCalls(prior: ReadonlyArray<FactoryCallSite>, next: ReadonlyArray<FactoryCallSite>): ReadonlyArray<CallSiteDiff>;
680
+
681
+ export { type CallSiteDiff, type DomainQuestion, type DomainQuestionAnswer, type DomainQuestionAnswers, type DomainQuestionInputType, type FactoryCallPlanMutation, type FactoryCallPlanMutationTemplate, type FactoryCallPlanState, type FactoryCallSite, type FactoryCallSiteParams, type FactoryConfigParam, type FactoryConfigTier, type FactoryEntitySignature, type FactoryPageSignature, type FactoryParamValue, type FactorySignature, type FactorySignatureCatalog, type FactorySignatureEntityField, type FactoryTraitSignature, type JsonSchema, type JsonSchemaType, type JsonValue, type OrbitalCallInput, type OwnershipOverlayEntry, type PresentationNavItem, type PresentationOverlay, type RuleOverlay, type RuleOverlayEntry, type SchemaFieldType, type TraitOverlay, type TraitOverlayEntry, type TraitOverlayListener, type TranslationBinding, type TranslationResult, type TranslationWarning, answerToMutations, answersToMutations, applyFactoryCallPlanMutation, deriveInputType, diffFactoryCalls, generateQuestions, translateOverlaysToParams };
@@ -1,3 +1,294 @@
1
+ // src/factory/questions/generate.ts
2
+ function generateQuestions(plan, catalog, ruleOverlay) {
3
+ const out = [];
4
+ const ruleCapabilities = collectRuleCapabilities(ruleOverlay);
5
+ for (const call of plan) {
6
+ const signature = findSignature(catalog, call.organism, call.orbital);
7
+ if (!signature) continue;
8
+ out.push(...configKeyQuestions(call, signature));
9
+ out.push(...capabilityQuestions(call, signature, ruleCapabilities));
10
+ }
11
+ return out;
12
+ }
13
+ function configKeyQuestions(call, signature) {
14
+ const out = [];
15
+ for (const trait of signature.traits) {
16
+ for (const param of trait.overridableConfigKeys) {
17
+ if (param.tier === "internal") continue;
18
+ if (isAlreadyCustomized(call, trait.name, param.key)) continue;
19
+ out.push(buildConfigKeyQuestion(call, trait, param));
20
+ }
21
+ }
22
+ return out;
23
+ }
24
+ function buildConfigKeyQuestion(call, trait, param) {
25
+ const label = param.label ?? humanizeKey(param.key);
26
+ const question = `${label}?`;
27
+ const reason = param.description ?? `Customizes "${param.key}" on the ${trait.name} trait. ` + (param.default !== void 0 ? `Default: ${stringifyDefault(param.default)}.` : `No default \u2014 leave blank to inherit the factory's behavior.`);
28
+ const out = {
29
+ id: `${call.orbital}.${trait.name}.${param.key}`,
30
+ orbitalName: call.orbital,
31
+ question,
32
+ reason,
33
+ inputType: deriveInputType(param),
34
+ mutationTemplate: {
35
+ kind: "set-trait-override-config",
36
+ orbitalName: call.orbital,
37
+ traitName: trait.name,
38
+ configKey: param.key
39
+ }
40
+ };
41
+ if (param.default !== void 0) {
42
+ out.defaultValue = param.default;
43
+ }
44
+ if (param.enumValues && param.enumValues.length > 0) {
45
+ out.suggestedAnswers = param.enumValues;
46
+ }
47
+ if (param.items) {
48
+ out.itemSchema = param.items;
49
+ }
50
+ if (param.properties) {
51
+ out.objectSchema = param.properties;
52
+ }
53
+ if (param.tier) {
54
+ out.tier = param.tier;
55
+ }
56
+ return out;
57
+ }
58
+ function isAlreadyCustomized(call, traitName, configKey) {
59
+ const override = call.params.traitOverrides?.[traitName];
60
+ if (!override?.config) return false;
61
+ return Object.prototype.hasOwnProperty.call(override.config, configKey);
62
+ }
63
+ function deriveInputType(param) {
64
+ if (param.type.startsWith("[")) {
65
+ if (param.items?.properties && Object.keys(param.items.properties).length > 0) {
66
+ return "listOfObjects";
67
+ }
68
+ if (param.enumValues && param.enumValues.length > 0) {
69
+ return "multiselect";
70
+ }
71
+ return "tagList";
72
+ }
73
+ if (param.type === "object") {
74
+ if (param.properties && Object.keys(param.properties).length > 0) {
75
+ return "objectForm";
76
+ }
77
+ return "text";
78
+ }
79
+ switch (param.type) {
80
+ case "number":
81
+ case "integer":
82
+ return "number";
83
+ case "boolean":
84
+ return "boolean";
85
+ case "enum":
86
+ return "enum";
87
+ case "string":
88
+ return param.enumValues && param.enumValues.length > 0 ? "enum" : "text";
89
+ default:
90
+ return "text";
91
+ }
92
+ }
93
+ function humanizeKey(key) {
94
+ const parts = key.replace(/[-_]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").split(" ").filter((s) => s.length > 0);
95
+ return parts.map((p) => /^[A-Z]+$/.test(p) ? p : p.charAt(0).toUpperCase() + p.slice(1)).join(" ");
96
+ }
97
+ function stringifyDefault(v) {
98
+ if (typeof v === "string") return JSON.stringify(v);
99
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
100
+ if (Array.isArray(v)) return `${v.length} item${v.length === 1 ? "" : "s"}`;
101
+ return "object";
102
+ }
103
+ function capabilityQuestions(call, signature, ruleCapabilities) {
104
+ const entityName = call.params.entityName ?? signature.entities[0]?.name ?? call.orbital;
105
+ const seen = /* @__PURE__ */ new Set();
106
+ const out = [];
107
+ for (const trait of signature.traits) {
108
+ for (const cap of trait.capabilities) {
109
+ if (seen.has(cap)) continue;
110
+ seen.add(cap);
111
+ if (ruleCapabilities.has(cap)) continue;
112
+ out.push({
113
+ id: `${call.orbital}.capability.${cap}`,
114
+ orbitalName: call.orbital,
115
+ question: capabilityPrompt(cap, entityName),
116
+ reason: capabilityReason(cap, trait),
117
+ capability: cap,
118
+ inputType: "capability",
119
+ mutationTemplate: {
120
+ kind: "set-rule",
121
+ capability: cap,
122
+ appliesTo: [entityName]
123
+ },
124
+ suggestedAnswers: ["yes", "skip"]
125
+ });
126
+ }
127
+ }
128
+ return out;
129
+ }
130
+ function capabilityPrompt(capability, entity) {
131
+ return `Should "${capability}" apply to ${entity}?`;
132
+ }
133
+ function capabilityReason(capability, trait) {
134
+ return `The "${trait.name}" trait advertises capability "${capability}". Answer "yes" to wire a matching rule into the schema.`;
135
+ }
136
+ function findSignature(catalog, organism, orbital) {
137
+ return catalog.find((s) => s.organism === organism && s.orbital === orbital);
138
+ }
139
+ function collectRuleCapabilities(overlay) {
140
+ const out = /* @__PURE__ */ new Set();
141
+ for (const r of overlay?.rules ?? []) {
142
+ if (typeof r.capability === "string") out.add(r.capability);
143
+ }
144
+ return out;
145
+ }
146
+
147
+ // src/factory/questions/reducer.ts
148
+ var PERSISTENCE_VALUES = [
149
+ "persistent",
150
+ "runtime",
151
+ "singleton",
152
+ "instance",
153
+ "local"
154
+ ];
155
+ function answersToMutations(answers, questions) {
156
+ const out = [];
157
+ for (const q of questions) {
158
+ const answer = answers[q.id];
159
+ if (answer === void 0 || answer === null) continue;
160
+ out.push(...answerToMutations(q.mutationTemplate, answer));
161
+ }
162
+ return out;
163
+ }
164
+ function answerToMutations(template, answer, _question) {
165
+ switch (template.kind) {
166
+ case "set-orbital-entity-name": {
167
+ if (typeof answer !== "string" || answer.length === 0) return [];
168
+ return [
169
+ {
170
+ kind: "set-orbital-entity-name",
171
+ orbitalName: template.orbitalName,
172
+ name: answer
173
+ }
174
+ ];
175
+ }
176
+ case "set-orbital-entity-fields": {
177
+ if (!isEntityFieldArray(answer)) return [];
178
+ if (answer.length === 0) return [];
179
+ return [
180
+ {
181
+ kind: "set-orbital-entity-fields",
182
+ orbitalName: template.orbitalName,
183
+ fields: answer
184
+ }
185
+ ];
186
+ }
187
+ case "set-orbital-persistence": {
188
+ if (!isPersistence(answer)) return [];
189
+ return [
190
+ {
191
+ kind: "set-orbital-persistence",
192
+ orbitalName: template.orbitalName,
193
+ persistence: answer
194
+ }
195
+ ];
196
+ }
197
+ case "set-orbital-collection": {
198
+ if (typeof answer !== "string" || answer.length === 0) return [];
199
+ return [
200
+ {
201
+ kind: "set-orbital-collection",
202
+ orbitalName: template.orbitalName,
203
+ collection: answer
204
+ }
205
+ ];
206
+ }
207
+ case "set-orbital-page-path": {
208
+ if (typeof answer !== "string" || answer.length === 0) return [];
209
+ return [
210
+ {
211
+ kind: "set-orbital-page-path",
212
+ orbitalName: template.orbitalName,
213
+ pageName: template.pageName,
214
+ path: answer
215
+ }
216
+ ];
217
+ }
218
+ case "set-trait-override-config": {
219
+ if (!isFactoryParamValue(answer)) return [];
220
+ return [
221
+ {
222
+ kind: "set-trait-override-config",
223
+ orbitalName: template.orbitalName,
224
+ traitName: template.traitName,
225
+ key: template.configKey,
226
+ value: answer
227
+ }
228
+ ];
229
+ }
230
+ case "add-extra-trait": {
231
+ if (!isTraitReference(answer)) return [];
232
+ return [
233
+ {
234
+ kind: "add-extra-trait",
235
+ orbitalName: template.orbitalName,
236
+ trait: answer
237
+ }
238
+ ];
239
+ }
240
+ case "remove-extra-trait": {
241
+ if (typeof answer !== "string" || answer.length === 0) return [];
242
+ return [
243
+ {
244
+ kind: "remove-extra-trait",
245
+ orbitalName: template.orbitalName,
246
+ ref: answer
247
+ }
248
+ ];
249
+ }
250
+ case "set-rule": {
251
+ if (typeof answer !== "string") return [];
252
+ if (answer !== "yes") return [];
253
+ const rule = {
254
+ id: `${template.capability}::${template.appliesTo.join(",")}`,
255
+ capability: template.capability,
256
+ description: `User accepted ${template.capability} for ${template.appliesTo.join(", ")}.`,
257
+ appliesTo: template.appliesTo
258
+ };
259
+ return [{ kind: "set-rule", rule }];
260
+ }
261
+ case "remove-rule": {
262
+ return [{ kind: "remove-rule", ruleId: template.ruleId }];
263
+ }
264
+ }
265
+ }
266
+ function isEntityFieldArray(a) {
267
+ if (!Array.isArray(a)) return false;
268
+ for (const x of a) {
269
+ if (typeof x !== "object" || x === null) return false;
270
+ if (!("name" in x) || !("type" in x)) return false;
271
+ if (typeof x.name !== "string" || typeof x.type !== "string") return false;
272
+ }
273
+ return true;
274
+ }
275
+ function isPersistence(a) {
276
+ return typeof a === "string" && PERSISTENCE_VALUES.includes(a);
277
+ }
278
+ function isFactoryParamValue(a) {
279
+ if (a === null) return false;
280
+ const t = typeof a;
281
+ if (t === "string" || t === "number" || t === "boolean") return true;
282
+ if (Array.isArray(a)) return true;
283
+ if (t === "object") return true;
284
+ return false;
285
+ }
286
+ function isTraitReference(a) {
287
+ if (typeof a !== "object" || a === null || Array.isArray(a)) return false;
288
+ if (!("ref" in a)) return false;
289
+ return typeof a.ref === "string";
290
+ }
291
+
1
292
  // src/factory/translate.ts
2
293
  function translateOverlaysToParams(binding, signature, presentation, ruleOverlay, traitOverlay, catalog) {
3
294
  const warnings = [];
@@ -145,8 +436,9 @@ function mergeTraitOverride(traitName, entry, trait, params, warnings) {
145
436
  }
146
437
  function applyRuleOverlay(overlay, signature, binding, catalog, params, warnings) {
147
438
  if (!overlay) return;
439
+ const matchableNames = collectMatchableEntityNames(signature, binding);
148
440
  for (const rule of overlay.rules) {
149
- if (!ruleAppliesToBinding(rule, binding)) continue;
441
+ if (!ruleAppliesToAnyName(rule, matchableNames)) continue;
150
442
  if (!catalog) {
151
443
  warnings.push({
152
444
  field: `ruleOverlay.rules.${rule.id}`,
@@ -169,14 +461,25 @@ function applyRuleOverlay(overlay, signature, binding, catalog, params, warnings
169
461
  }
170
462
  if (overlay.ownership) {
171
463
  for (const entry of overlay.ownership) {
172
- if (entry.entity !== binding.entityName) continue;
464
+ if (!matchableNames.includes(entry.entity)) continue;
173
465
  applyOwnership(entry, params, signature, catalog, warnings);
174
466
  }
175
467
  }
176
468
  }
177
- function ruleAppliesToBinding(rule, binding) {
469
+ function collectMatchableEntityNames(signature, binding) {
470
+ const out = /* @__PURE__ */ new Set();
471
+ if (binding.entityName.length > 0) out.add(binding.entityName);
472
+ for (const ent of signature.entities) {
473
+ if (typeof ent.name === "string" && ent.name.length > 0) out.add(ent.name);
474
+ }
475
+ return [...out];
476
+ }
477
+ function ruleAppliesToAnyName(rule, matchableNames) {
178
478
  if (rule.appliesTo.length === 0) return true;
179
- return rule.appliesTo.includes(binding.entityName);
479
+ for (const n of rule.appliesTo) {
480
+ if (matchableNames.includes(n)) return true;
481
+ }
482
+ return false;
180
483
  }
181
484
  function findTraitByCapability(catalog, capability) {
182
485
  for (const sig of catalog) {
@@ -433,6 +736,6 @@ function patchOrbital(state, orbitalName, patch) {
433
736
  };
434
737
  }
435
738
 
436
- export { applyFactoryCallPlanMutation, diffFactoryCalls, translateOverlaysToParams };
739
+ export { answerToMutations, answersToMutations, applyFactoryCallPlanMutation, deriveInputType, diffFactoryCalls, generateQuestions, translateOverlaysToParams };
437
740
  //# sourceMappingURL=index.js.map
438
741
  //# sourceMappingURL=index.js.map