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