@orkestrel/qualifier 0.0.1 → 0.0.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.
@@ -1,5 +1,5 @@
1
1
  import { arrayOf, isFiniteNumber, isRecord, isString, literalOf, recordOf, resolveField } from "@orkestrel/contract";
2
- import { createEvaluator, createLogicalReasoner, createQuantitativeReasoner, createReason, extractAtoms, extractConclusions, formatField, isBounds, isLogicalDefinition, isQuantitativeDefinition } from "@orkestrel/reason";
2
+ import { createEvaluator, createLogicalReasoner, createQuantitativeReasoner, createReason, extractAtoms, extractConclusions, findDuplicates, formatField, isBounds, isLogicalDefinition, isQuantitativeDefinition, isReasonError } from "@orkestrel/reason";
3
3
  import { Emitter } from "@orkestrel/emitter";
4
4
  //#region src/core/constants.ts
5
5
  /** Default definition validation policy for `createQualifier` / `Qualifier.qualify`. */
@@ -27,6 +27,8 @@ var EFFECT_ELIGIBILITIES = Object.freeze({
27
27
  * `DEFINITION` — a qualification definition failed semantic validation.
28
28
  * `MISMATCH` — a subject is not a record or already carries the reserved
29
29
  * `qualification` key. `DESTROYED` — use of a destroyed qualifier.
30
+ * `ENGINE` — the underlying reason engine threw while running a pass (the
31
+ * original throw is preserved as `context.cause`).
30
32
  */
31
33
  var QualifierError = class extends Error {
32
34
  code;
@@ -65,7 +67,6 @@ function isRuling(value) {
65
67
  }
66
68
  /** Determine whether a value is an exact {@link QualificationDefinition} record. */
67
69
  function isQualificationDefinition(value) {
68
- if (!isRecord(value)) return false;
69
70
  return recordOf({
70
71
  id: isString,
71
72
  name: isString,
@@ -298,6 +299,26 @@ function findRule(definition, id) {
298
299
  * @param definition - The pass definition that produced the result
299
300
  * @param result - The evaluated reason result
300
301
  * @returns The pass's working projection
302
+ *
303
+ * @example
304
+ * ```ts
305
+ * import { reasonResultToProjection } from '@orkestrel/qualifier'
306
+ * import { factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'
307
+ *
308
+ * const cap = quantitativeDefinition('cap', 'TIV cap', [
309
+ * factorGroup('limit', 'sum', [staticFactor('base', 500_000)]),
310
+ * ])
311
+ * const result = {
312
+ * reasoning: 'quantitative' as const,
313
+ * value: 500_000,
314
+ * groups: [],
315
+ * count: 1,
316
+ * success: true,
317
+ * trace: [],
318
+ * errors: [],
319
+ * }
320
+ * reasonResultToProjection(cap, result) // 500000
321
+ * ```
301
322
  */
302
323
  function reasonResultToProjection(definition, result) {
303
324
  if (definition.reasoning === "quantitative" && result.reasoning === "quantitative") return result.value;
@@ -318,6 +339,15 @@ function reasonResultToProjection(definition, result) {
318
339
  * @param id - The pass id
319
340
  * @param result - The evaluated quantitative result
320
341
  * @returns A fresh derivation
342
+ *
343
+ * @example
344
+ * ```ts
345
+ * import { quantitativeResultToDerivation } from '@orkestrel/qualifier'
346
+ *
347
+ * const result = { reasoning: 'quantitative' as const, value: 500_000, groups: [], count: 1, success: true, trace: ['cap: 500000'], errors: [] }
348
+ * quantitativeResultToDerivation('cap', result)
349
+ * // { id: 'cap', value: 500000, success: true, trace: ['cap: 500000'], errors: [] }
350
+ * ```
321
351
  */
322
352
  function quantitativeResultToDerivation(id, result) {
323
353
  return {
@@ -333,6 +363,13 @@ function quantitativeResultToDerivation(id, result) {
333
363
  *
334
364
  * @param context - The accumulated projection context
335
365
  * @returns A record carrying the context under the reserved key
366
+ *
367
+ * @example
368
+ * ```ts
369
+ * import { qualificationToRecord } from '@orkestrel/qualifier'
370
+ *
371
+ * qualificationToRecord({ cap: 500_000 }) // { qualification: { cap: 500000 } }
372
+ * ```
336
373
  */
337
374
  function qualificationToRecord(context) {
338
375
  return { [QUALIFICATION_KEY]: context };
@@ -344,6 +381,14 @@ function qualificationToRecord(context) {
344
381
  * @param id - The pass id to project under
345
382
  * @param projection - The pass's working projection
346
383
  * @returns A fresh context with the projection added
384
+ *
385
+ * @example
386
+ * ```ts
387
+ * import { mergeQualificationContext } from '@orkestrel/qualifier'
388
+ *
389
+ * mergeQualificationContext({ cap: 500_000 }, 'excess', 250_000)
390
+ * // { cap: 500000, excess: 250000 }
391
+ * ```
347
392
  */
348
393
  function mergeQualificationContext(context, id, projection) {
349
394
  return {
@@ -368,6 +413,21 @@ function mergeQualificationContext(context, id, projection) {
368
413
  * @param evaluator - The shared reason check evaluator
369
414
  * @param labels - Optional field-to-label overrides, keyed by dot-joined field
370
415
  * @returns A fresh finding
416
+ *
417
+ * @example
418
+ * ```ts
419
+ * import { rulingToFinding } from '@orkestrel/qualifier'
420
+ * import { atom, createEvaluator, logicalDefinition, rule } from '@orkestrel/reason'
421
+ *
422
+ * const gates = logicalDefinition('gates', 'Gates', [
423
+ * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),
424
+ * ])
425
+ * const ruling = { id: 'license', pass: 'gates', rule: 'licensed', effect: 'restriction' as const }
426
+ * const result = { reasoning: 'logical' as const, conclusion: true, rules: [{ id: 'licensed', applied: true, premises: [true], conclusion: true }], count: 1, success: true, trace: [], errors: [] }
427
+ * const finding = rulingToFinding(ruling, gates, result, { licensed: false }, createEvaluator())
428
+ *
429
+ * finding.applied // true
430
+ * ```
371
431
  */
372
432
  function rulingToFinding(ruling, pass, result, subject, evaluator, labels) {
373
433
  const applied = result.rules.find((item) => item.id === ruling.rule)?.applied ?? false;
@@ -395,6 +455,14 @@ function rulingToFinding(ruling, pass, result, subject, evaluator, labels) {
395
455
  * @param findings - The resolved findings
396
456
  * @param failed - Whether an operational pass failure occurred
397
457
  * @returns The most severe global eligibility
458
+ *
459
+ * @example
460
+ * ```ts
461
+ * import { deriveFindingEligibility } from '@orkestrel/qualifier'
462
+ *
463
+ * const finding = { id: 'license', pass: 'gates', rule: 'licensed', effect: 'restriction' as const, applied: true, premises: [] }
464
+ * deriveFindingEligibility([finding]) // 'ineligible'
465
+ * ```
398
466
  */
399
467
  function deriveFindingEligibility(findings, failed = false) {
400
468
  const eligibilities = findings.filter((finding) => finding.applied && finding.scope === void 0).map((finding) => EFFECT_ELIGIBILITIES[finding.effect]);
@@ -428,6 +496,14 @@ function combineEligibilities(eligibilities) {
428
496
  *
429
497
  * @param findings - The resolved findings
430
498
  * @returns A fresh record of scope to its combined eligibility
499
+ *
500
+ * @example
501
+ * ```ts
502
+ * import { deriveScopeEligibilities } from '@orkestrel/qualifier'
503
+ *
504
+ * const finding = { id: 'coastal', pass: 'wind', rule: 'coastal', effect: 'restriction' as const, scope: 'wind', applied: true, premises: [] }
505
+ * deriveScopeEligibilities([finding]) // { wind: 'ineligible' }
506
+ * ```
431
507
  */
432
508
  function deriveScopeEligibilities(findings) {
433
509
  const grouped = {};
@@ -446,6 +522,22 @@ function deriveScopeEligibilities(findings) {
446
522
  *
447
523
  * @param definition - The qualification definition to check
448
524
  * @returns A fresh list of reference-error messages
525
+ *
526
+ * @example
527
+ * ```ts
528
+ * import { findMissingReferences, qualificationDefinition, rulingDefinition } from '@orkestrel/qualifier'
529
+ * import { atom, logicalDefinition, rule } from '@orkestrel/reason'
530
+ *
531
+ * const gates = logicalDefinition('gates', 'Gates', [
532
+ * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),
533
+ * ])
534
+ * const definition = qualificationDefinition('standard', 'Standard', [gates], {
535
+ * rulings: [rulingDefinition('license', 'gates', 'absent', 'restriction')],
536
+ * })
537
+ *
538
+ * findMissingReferences(definition)
539
+ * // ["Ruling 'license' references missing rule 'absent' in pass 'gates'"]
540
+ * ```
449
541
  */
450
542
  function findMissingReferences(definition) {
451
543
  const errors = [];
@@ -466,23 +558,17 @@ function findMissingReferences(definition) {
466
558
  return errors;
467
559
  }
468
560
  /**
469
- * Find duplicate ids in a list, once each.
470
- *
471
- * @param ids - The ids to scan
472
- * @returns A fresh list of ids that appear more than once
473
- */
474
- function findDuplicateIds(ids) {
475
- const seen = /* @__PURE__ */ new Set();
476
- const duplicates = /* @__PURE__ */ new Set();
477
- for (const id of ids) if (seen.has(id)) duplicates.add(id);
478
- else seen.add(id);
479
- return [...duplicates];
480
- }
481
- /**
482
561
  * Determine whether a subject already owns the reserved {@link QUALIFICATION_KEY}.
483
562
  *
484
563
  * @param subject - The subject to check
485
564
  * @returns `true` when the subject owns the reserved key
565
+ *
566
+ * @example
567
+ * ```ts
568
+ * import { hasReservedKey } from '@orkestrel/qualifier'
569
+ *
570
+ * hasReservedKey({ id: 's1', qualification: {} }) // true
571
+ * ```
486
572
  */
487
573
  function hasReservedKey(subject) {
488
574
  return Object.hasOwn(subject, QUALIFICATION_KEY);
@@ -493,16 +579,198 @@ function hasReservedKey(subject) {
493
579
  * @param value - The candidate subject to validate
494
580
  * @throws {@link QualifierError} `'MISMATCH'` when the value is not a record, or
495
581
  * when it already carries the reserved `qualification` key
582
+ *
583
+ * @example
584
+ * ```ts
585
+ * import { assertSubject } from '@orkestrel/qualifier'
586
+ *
587
+ * assertSubject({ id: 's1' }) // narrows to Subject
588
+ * ```
496
589
  */
497
590
  function assertSubject(value) {
498
591
  if (!isRecord(value)) throw new QualifierError("MISMATCH", "Qualification subject must be a record");
499
592
  if (hasReservedKey(value)) throw new QualifierError("MISMATCH", `Qualification subject must not contain reserved key '${QUALIFICATION_KEY}'`);
500
593
  }
594
+ /**
595
+ * Map an engine throw caught while running one pass to a typed {@link QualifierError}.
596
+ *
597
+ * @remarks
598
+ * A reason `ReasonError('INVALID')` maps to `DEFINITION` (the engine rejected the
599
+ * pass as an invalid definition); `ReasonError('DESTROYED')` maps to `DESTROYED`;
600
+ * every other `ReasonError` code (e.g. `MISSING`) and every non-`ReasonError` throw
601
+ * (including a `bail: true` reasoner rethrow) maps to `ENGINE`. The original throw
602
+ * is preserved as `context.cause`.
603
+ *
604
+ * @param error - The value caught from the engine's `reason` call
605
+ * @param pass - The id of the pass that was running
606
+ * @returns A fresh, typed {@link QualifierError}
607
+ *
608
+ * @example
609
+ * ```ts
610
+ * import { mapEngineError } from '@orkestrel/qualifier'
611
+ * import { ReasonError } from '@orkestrel/reason'
612
+ *
613
+ * mapEngineError(new ReasonError('MISSING', 'No reasoner registered'), 'cap').code // 'ENGINE'
614
+ * ```
615
+ */
616
+ function mapEngineError(error, pass) {
617
+ if (isReasonError(error)) {
618
+ if (error.code === "INVALID") return new QualifierError("DEFINITION", `Pass '${pass}' failed engine validation: ${error.message}`, {
619
+ pass,
620
+ cause: error
621
+ });
622
+ if (error.code === "DESTROYED") return new QualifierError("DESTROYED", `Qualifier engine was destroyed during pass '${pass}'`, {
623
+ pass,
624
+ cause: error
625
+ });
626
+ return new QualifierError("ENGINE", `Pass '${pass}' engine failure: ${error.message}`, {
627
+ pass,
628
+ cause: error
629
+ });
630
+ }
631
+ return new QualifierError("ENGINE", `Pass '${pass}' engine failure: ${error instanceof Error ? error.message : String(error)}`, {
632
+ pass,
633
+ cause: error
634
+ });
635
+ }
636
+ /**
637
+ * Find logical passes carrying no rulings.
638
+ *
639
+ * @remarks
640
+ * A logical pass with no matching ruling can never contribute a finding, so its
641
+ * rules only ever influence later passes' reads — this is very likely an authoring
642
+ * oversight.
643
+ *
644
+ * @param definition - The qualification definition to check
645
+ * @returns A fresh list of warning messages
646
+ *
647
+ * @example
648
+ * ```ts
649
+ * import { findEmptyLogicalPasses, qualificationDefinition } from '@orkestrel/qualifier'
650
+ * import { atom, logicalDefinition, rule } from '@orkestrel/reason'
651
+ *
652
+ * const gates = logicalDefinition('gates', 'Gates', [
653
+ * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),
654
+ * ])
655
+ * const definition = qualificationDefinition('standard', 'Standard', [gates])
656
+ *
657
+ * findEmptyLogicalPasses(definition) // ["Logical pass 'gates' has no rulings"]
658
+ * ```
659
+ */
660
+ function findEmptyLogicalPasses(definition) {
661
+ const warnings = [];
662
+ for (const pass of definition.passes) {
663
+ if (pass.reasoning !== "logical") continue;
664
+ if (!(definition.rulings ?? []).some((ruling) => ruling.pass === pass.id)) warnings.push(`Logical pass '${pass.id}' has no rulings`);
665
+ }
666
+ return warnings;
667
+ }
668
+ /**
669
+ * Find quantitative passes never read by a later pass.
670
+ *
671
+ * @remarks
672
+ * A quantitative pass's projected value lives under `qualification.<id>` — if no
673
+ * later logical premise or quantitative factor field reads that key, the
674
+ * derivation is dead weight. Reads are collected per later pass: a logical pass's
675
+ * premise atoms (conclusion atoms are writes, not reads), and a quantitative
676
+ * pass's field/lookup/range factor sources plus factor checks.
677
+ *
678
+ * @param definition - The qualification definition to check
679
+ * @returns A fresh list of warning messages
680
+ *
681
+ * @example
682
+ * ```ts
683
+ * import { findUnreadDerivations, qualificationDefinition } from '@orkestrel/qualifier'
684
+ * import { factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'
685
+ *
686
+ * const cap = quantitativeDefinition('cap', 'TIV cap', [
687
+ * factorGroup('limit', 'sum', [staticFactor('base', 500_000)]),
688
+ * ])
689
+ * const definition = qualificationDefinition('standard', 'Standard', [cap])
690
+ *
691
+ * findUnreadDerivations(definition) // ["Quantitative pass 'cap' is never read by a later pass"]
692
+ * ```
693
+ */
694
+ function findUnreadDerivations(definition) {
695
+ const warnings = [];
696
+ definition.passes.forEach((pass, index) => {
697
+ if (pass.reasoning !== "quantitative") return;
698
+ const key = formatField(["qualification", pass.id]);
699
+ const reads = /* @__PURE__ */ new Set();
700
+ for (const later of definition.passes.slice(index + 1)) {
701
+ if (later.reasoning === "logical") for (const rule of later.rules) for (const premise of rule.premises) for (const atom of extractAtoms(premise)) reads.add(formatField(atom.check.field));
702
+ if (later.reasoning === "quantitative") for (const group of later.groups) for (const factor of group.factors) {
703
+ if (factor.source.origin === "field" || factor.source.origin === "lookup" || factor.source.origin === "range") reads.add(formatField(factor.source.field));
704
+ for (const check of factor.checks ?? []) reads.add(formatField(check.field));
705
+ }
706
+ }
707
+ if (!reads.has(key)) warnings.push(`Quantitative pass '${pass.id}' is never read by a later pass`);
708
+ });
709
+ return warnings;
710
+ }
711
+ /**
712
+ * Build a {@link QualificationDefinition}.
713
+ *
714
+ * @remarks
715
+ * Returns a fresh top-level definition, omitting absent optional keys;
716
+ * `passes` and `rulings` arrays are copied, and record `metadata` is
717
+ * shallow-copied so nested values are not deep-cloned.
718
+ *
719
+ * @param id - The definition id
720
+ * @param name - The display name
721
+ * @param passes - The ordered qualification passes
722
+ * @param input - Optional description, rulings, and metadata
723
+ * @returns A fresh qualification definition
724
+ *
725
+ * @example
726
+ * ```ts
727
+ * import { qualificationDefinition } from '@orkestrel/qualifier'
728
+ *
729
+ * qualificationDefinition('standard', 'Standard', [gates], { rulings: [ruling] })
730
+ * ```
731
+ */
732
+ function qualificationDefinition(id, name, passes, input) {
733
+ return {
734
+ id,
735
+ name,
736
+ passes: [...passes],
737
+ ...input?.description === void 0 ? {} : { description: input.description },
738
+ ...input?.rulings === void 0 ? {} : { rulings: [...input.rulings] },
739
+ ...input?.metadata === void 0 ? {} : { metadata: { ...input.metadata } }
740
+ };
741
+ }
742
+ /**
743
+ * Build a {@link Ruling} — one authored consequence for one rule in one pass.
744
+ *
745
+ * @param id - The ruling id
746
+ * @param pass - The logical pass id the rule lives in
747
+ * @param rule - The rule id whose firing this ruling reacts to
748
+ * @param effect - The eligibility impact when the rule fires
749
+ * @param input - Optional selection scope and message template
750
+ * @returns A fresh ruling
751
+ *
752
+ * @example
753
+ * ```ts
754
+ * import { rulingDefinition } from '@orkestrel/qualifier'
755
+ *
756
+ * rulingDefinition('license', 'gates', 'licensed', 'restriction', { message: 'A license is required' })
757
+ * ```
758
+ */
759
+ function rulingDefinition(id, pass, rule, effect, input) {
760
+ return {
761
+ id,
762
+ pass,
763
+ rule,
764
+ effect,
765
+ ...input?.scope === void 0 ? {} : { scope: input.scope },
766
+ ...input?.message === void 0 ? {} : { message: input.message }
767
+ };
768
+ }
501
769
  //#endregion
502
770
  //#region src/core/Qualifier.ts
503
771
  /**
504
772
  * A qualifier — runs ordered passes over one reason engine and returns
505
- * pre-rating eligibility.
773
+ * eligibility.
506
774
  *
507
775
  * @remarks
508
776
  * The engine is OWNED when self-created (destroyed on `destroy()`) and borrowed
@@ -549,10 +817,18 @@ var Qualifier = class {
549
817
  const warnings = [];
550
818
  if (definition.id.length === 0) errors.push("Definition id must not be empty");
551
819
  if (definition.name.length === 0) errors.push("Definition name must not be empty");
552
- if (definition.passes.length === 0) warnings.push("Definition has no passes");
553
- for (const id of findDuplicateIds(definition.passes.map((pass) => pass.id))) errors.push(`Duplicate pass id '${id}'`);
554
- for (const id of findDuplicateIds((definition.rulings ?? []).map((ruling) => ruling.id))) errors.push(`Duplicate ruling id '${id}'`);
820
+ definition.passes.forEach((pass, index) => {
821
+ if (!isQualificationPass(pass)) errors.push(`Pass at index ${index} is not a valid qualification pass`);
822
+ });
823
+ (definition.rulings ?? []).forEach((ruling, index) => {
824
+ if (!isRuling(ruling)) errors.push(`Ruling at index ${index} is not a valid ruling`);
825
+ });
826
+ for (const id of findDuplicates(definition.passes)) errors.push(`Duplicate pass id '${id}'`);
827
+ for (const id of findDuplicates(definition.rulings ?? [])) errors.push(`Duplicate ruling id '${id}'`);
555
828
  for (const reference of findMissingReferences(definition)) errors.push(reference);
829
+ if (definition.passes.length === 0) warnings.push("Definition has no passes");
830
+ for (const warning of findEmptyLogicalPasses(definition)) warnings.push(warning);
831
+ for (const warning of findUnreadDerivations(definition)) warnings.push(warning);
556
832
  return {
557
833
  valid: errors.length === 0,
558
834
  errors,
@@ -576,8 +852,14 @@ var Qualifier = class {
576
852
  const errors = [];
577
853
  let failed = false;
578
854
  for (const pass of definition.passes) {
855
+ this.#alive();
579
856
  const evaluated = working;
580
- const result = this.#engine.reason(evaluated, pass);
857
+ let result;
858
+ try {
859
+ result = this.#engine.reason(evaluated, pass);
860
+ } catch (error) {
861
+ throw mapEngineError(error, pass.id);
862
+ }
581
863
  trace.push(...result.trace.map((entry) => `${pass.id}: ${entry}`));
582
864
  errors.push(...result.errors.map((entry) => `${pass.id}: ${entry}`));
583
865
  if (result.reasoning === "quantitative") {
@@ -606,17 +888,18 @@ var Qualifier = class {
606
888
  if (deriveFindingEligibility(findings) === "ineligible") break;
607
889
  }
608
890
  const success = !failed && errors.length === 0;
609
- const qualification = {
891
+ const scopes = deriveScopeEligibilities(findings);
892
+ const qualification = Object.freeze({
610
893
  id: definition.id,
611
894
  name: definition.name,
612
895
  eligibility: deriveFindingEligibility(findings, !success),
613
- scopes: deriveScopeEligibilities(findings),
614
- findings,
615
- derivations,
896
+ scopes: Object.freeze(scopes),
897
+ findings: Object.freeze(findings),
898
+ derivations: Object.freeze(derivations),
616
899
  success,
617
- trace,
618
- errors
619
- };
900
+ trace: Object.freeze(trace),
901
+ errors: Object.freeze(errors)
902
+ });
620
903
  this.#emitter.emit("qualify", qualification);
621
904
  return qualification;
622
905
  }
@@ -649,65 +932,7 @@ var Qualifier = class {
649
932
  function createQualifier(options) {
650
933
  return new Qualifier(options);
651
934
  }
652
- /**
653
- * Build a {@link QualificationDefinition}.
654
- *
655
- * @remarks
656
- * Returns a fresh top-level definition, omitting absent optional keys;
657
- * `passes` and `rulings` arrays are copied, and record `metadata` is
658
- * shallow-copied so nested values are not deep-cloned.
659
- *
660
- * @param id - The definition id
661
- * @param name - The display name
662
- * @param passes - The ordered qualification passes
663
- * @param input - Optional description, rulings, and metadata
664
- * @returns A fresh qualification definition
665
- *
666
- * @example
667
- * ```ts
668
- * import { qualificationDefinition } from '@orkestrel/qualifier'
669
- *
670
- * qualificationDefinition('standard', 'Standard', [gates], { rulings: [ruling] })
671
- * ```
672
- */
673
- function qualificationDefinition(id, name, passes, input) {
674
- return {
675
- id,
676
- name,
677
- passes: [...passes],
678
- ...input?.description === void 0 ? {} : { description: input.description },
679
- ...input?.rulings === void 0 ? {} : { rulings: [...input.rulings] },
680
- ...input?.metadata === void 0 ? {} : { metadata: { ...input.metadata } }
681
- };
682
- }
683
- /**
684
- * Build a {@link Ruling} — one authored consequence for one rule in one pass.
685
- *
686
- * @param id - The ruling id
687
- * @param pass - The logical pass id the rule lives in
688
- * @param rule - The rule id whose firing this ruling reacts to
689
- * @param effect - The eligibility impact when the rule fires
690
- * @param input - Optional rating scope and message template
691
- * @returns A fresh ruling
692
- *
693
- * @example
694
- * ```ts
695
- * import { rulingDefinition } from '@orkestrel/qualifier'
696
- *
697
- * rulingDefinition('license', 'gates', 'licensed', 'restriction', { message: 'A license is required' })
698
- * ```
699
- */
700
- function rulingDefinition(id, pass, rule, effect, input) {
701
- return {
702
- id,
703
- pass,
704
- rule,
705
- effect,
706
- ...input?.scope === void 0 ? {} : { scope: input.scope },
707
- ...input?.message === void 0 ? {} : { message: input.message }
708
- };
709
- }
710
935
  //#endregion
711
- export { DEFAULT_QUALIFIER_VALIDATE, EFFECT_ELIGIBILITIES, ELIGIBILITY_PRECEDENCE, QUALIFICATION_KEY, Qualifier, QualifierError, assertSubject, combineEligibilities, createQualifier, deriveFindingEligibility, deriveScopeEligibilities, describeComparison, describePremise, describeValue, findDuplicateIds, findMissingReferences, findRule, hasReservedKey, interpolateMessage, isEligibility, isQualificationDefinition, isQualificationEffect, isQualificationPass, isQualifierError, isRuling, logicalPremises, mergeQualificationContext, premiseCheck, qualificationDefinition, qualificationToRecord, quantitativeResultToDerivation, reasonResultToProjection, rulingDefinition, rulingToFinding };
936
+ export { DEFAULT_QUALIFIER_VALIDATE, EFFECT_ELIGIBILITIES, ELIGIBILITY_PRECEDENCE, QUALIFICATION_KEY, Qualifier, QualifierError, assertSubject, combineEligibilities, createQualifier, deriveFindingEligibility, deriveScopeEligibilities, describeComparison, describePremise, describeValue, findEmptyLogicalPasses, findMissingReferences, findRule, findUnreadDerivations, hasReservedKey, interpolateMessage, isEligibility, isQualificationDefinition, isQualificationEffect, isQualificationPass, isQualifierError, isRuling, logicalPremises, mapEngineError, mergeQualificationContext, premiseCheck, qualificationDefinition, qualificationToRecord, quantitativeResultToDerivation, reasonResultToProjection, rulingDefinition, rulingToFinding };
712
937
 
713
938
  //# sourceMappingURL=index.js.map