@orkestrel/qualifier 0.0.8 → 0.0.9

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.
@@ -51,6 +51,122 @@ function isQualifierError(value) {
51
51
  var isEligibility = (0, _orkestrel_contract.literalOf)("eligible", "ineligible", "referral");
52
52
  /** Determine whether a value is a {@link QualificationEffect} literal. */
53
53
  var isQualificationEffect = (0, _orkestrel_contract.literalOf)("restriction", "referral", "condition");
54
+ /**
55
+ * Determine whether a value is an open string-keyed record of {@link Eligibility} values.
56
+ *
57
+ * @remarks
58
+ * Every own string-named property is checked, including non-enumerable properties. Inherited and
59
+ * symbol-named members are left unchecked because they are outside the record this guard certifies.
60
+ *
61
+ * @param value - The value to test
62
+ * @returns `true` when `value` is a non-array object whose own string-named values are eligibilities
63
+ */
64
+ function isEligibilityRecord(value) {
65
+ return (0, _orkestrel_contract.whereOf)((0, _orkestrel_contract.objectOf)({}), (record) => Object.getOwnPropertyNames(record).every((key) => isEligibility(Reflect.get(record, key))))(value);
66
+ }
67
+ /**
68
+ * Determine whether a value is an open result-side {@link Premise}.
69
+ *
70
+ * @remarks
71
+ * `expected` is unchecked and may be absent or contain any value because its published type is
72
+ * `unknown`. `actual` is unchecked and may be absent or contain any value for the same reason.
73
+ * Unknown members are admitted.
74
+ *
75
+ * @param value - The value to test
76
+ * @returns `true` when every checked premise member follows its published type
77
+ */
78
+ function isPremise(value) {
79
+ return (0, _orkestrel_contract.objectOf)({
80
+ field: _orkestrel_reason.isFieldPath,
81
+ label: _orkestrel_contract.isString,
82
+ description: _orkestrel_contract.isString,
83
+ comparison: _orkestrel_reason.isComparison,
84
+ met: _orkestrel_contract.isBoolean
85
+ }, [
86
+ "field",
87
+ "label",
88
+ "description",
89
+ "comparison",
90
+ "met"
91
+ ])(value);
92
+ }
93
+ /**
94
+ * Determine whether a value is an open result-side {@link Finding}.
95
+ *
96
+ * @remarks
97
+ * Unknown members are admitted. Optional `scope` and `message` members may be absent or
98
+ * `undefined`; a present defined value must be a string.
99
+ *
100
+ * @param value - The value to test
101
+ * @returns `true` when every published finding member is valid
102
+ */
103
+ function isFinding(value) {
104
+ return (0, _orkestrel_contract.objectOf)({
105
+ id: _orkestrel_contract.isString,
106
+ pass: _orkestrel_contract.isString,
107
+ rule: _orkestrel_contract.isString,
108
+ effect: isQualificationEffect,
109
+ scope: _orkestrel_contract.isString,
110
+ applied: _orkestrel_contract.isBoolean,
111
+ message: _orkestrel_contract.isString,
112
+ premises: (0, _orkestrel_contract.arrayOf)(isPremise)
113
+ }, ["scope", "message"])(value);
114
+ }
115
+ /**
116
+ * Determine whether a value is an open result-side {@link Derivation}.
117
+ *
118
+ * @remarks
119
+ * Unknown members are admitted. `value` follows the published `number` type, including `NaN` and
120
+ * infinities.
121
+ *
122
+ * @param value - The value to test
123
+ * @returns `true` when every published derivation member is valid
124
+ */
125
+ function isDerivation(value) {
126
+ return (0, _orkestrel_contract.objectOf)({
127
+ id: _orkestrel_contract.isString,
128
+ value: _orkestrel_contract.isNumber,
129
+ success: _orkestrel_contract.isBoolean,
130
+ trace: (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString),
131
+ errors: (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)
132
+ })(value);
133
+ }
134
+ /**
135
+ * Determine whether a value is an open {@link QualificationResult} returned by a qualifier.
136
+ *
137
+ * @remarks
138
+ * This guard is result-postured for values returned through a borrowed qualifier interface. It
139
+ * admits unknown members and class instances while checking the complete published result closure.
140
+ *
141
+ * @param value - The value to test
142
+ * @returns `true` when every published qualification-result member is valid
143
+ */
144
+ function isQualificationResult(value) {
145
+ return (0, _orkestrel_contract.objectOf)({
146
+ id: _orkestrel_contract.isString,
147
+ name: _orkestrel_contract.isString,
148
+ eligibility: isEligibility,
149
+ scopes: isEligibilityRecord,
150
+ findings: (0, _orkestrel_contract.arrayOf)(isFinding),
151
+ derivations: (0, _orkestrel_contract.arrayOf)(isDerivation),
152
+ success: _orkestrel_contract.isBoolean,
153
+ trace: (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString),
154
+ errors: (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)
155
+ })(value);
156
+ }
157
+ /**
158
+ * Determine whether a value is an open {@link QualificationValidationResult} returned by a qualifier.
159
+ *
160
+ * @remarks
161
+ * `QualificationValidationResult` is an alias of reasons' validation-result type, so this IS
162
+ * reasons' published open result guard by delegation — the alias tracks reasons' shape
163
+ * automatically, and a hand-written member list here would silently stop checking a member a
164
+ * later reason release adds.
165
+ *
166
+ * @param value - The value to test
167
+ * @returns `true` when every published qualification-validation-result member is valid
168
+ */
169
+ var isQualificationValidationResult = _orkestrel_reason.isReasonValidationResult;
54
170
  /** Determine whether a value is a {@link QualificationPass} (a quantitative or logical definition). */
55
171
  function isQualificationPass(value) {
56
172
  return (0, _orkestrel_reason.isQuantitativeDefinition)(value) || (0, _orkestrel_reason.isLogicalDefinition)(value);
@@ -223,7 +339,7 @@ function premiseCheck(check, result, labels) {
223
339
  comparison: check.operator,
224
340
  expected: check.value,
225
341
  actual: result.actual,
226
- ...result.met === void 0 ? {} : { met: result.met }
342
+ met: result.met
227
343
  };
228
344
  }
229
345
  /**
@@ -954,10 +1070,16 @@ exports.findRule = findRule;
954
1070
  exports.findUnreadDerivations = findUnreadDerivations;
955
1071
  exports.hasReservedKey = hasReservedKey;
956
1072
  exports.interpolateMessage = interpolateMessage;
1073
+ exports.isDerivation = isDerivation;
957
1074
  exports.isEligibility = isEligibility;
1075
+ exports.isEligibilityRecord = isEligibilityRecord;
1076
+ exports.isFinding = isFinding;
1077
+ exports.isPremise = isPremise;
958
1078
  exports.isQualificationDefinition = isQualificationDefinition;
959
1079
  exports.isQualificationEffect = isQualificationEffect;
960
1080
  exports.isQualificationPass = isQualificationPass;
1081
+ exports.isQualificationResult = isQualificationResult;
1082
+ exports.isQualificationValidationResult = isQualificationValidationResult;
961
1083
  exports.isQualifierError = isQualifierError;
962
1084
  exports.isRuling = isRuling;
963
1085
  exports.logicalPremises = logicalPremises;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["#emitter","#engine","#evaluator","#owned","#validate","#labels","#alive","#qualify","#destroyed"],"sources":["../../../src/core/constants.ts","../../../src/core/errors.ts","../../../src/core/validators.ts","../../../src/core/helpers.ts","../../../src/core/Qualifier.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { Eligibility, QualificationEffect } from './types.js'\n\n/** Default definition validation policy for `createQualifier` / `Qualifier.qualify`. */\nexport const DEFAULT_QUALIFIER_VALIDATE = true\n\n/** The reserved internal projection namespace a pass's working projection is written under. */\nexport const QUALIFICATION_KEY = 'qualification'\n\n/** Eligibility severity order — most to least severe. */\nexport const ELIGIBILITY_PRECEDENCE: readonly Eligibility[] = Object.freeze([\n\t'ineligible',\n\t'referral',\n\t'eligible',\n])\n\n/** The eligibility impact of each {@link QualificationEffect}; `condition` remains eligible. */\nexport const EFFECT_ELIGIBILITIES: Readonly<Record<QualificationEffect, Eligibility>> =\n\tObject.freeze({\n\t\trestriction: 'ineligible',\n\t\treferral: 'referral',\n\t\tcondition: 'eligible',\n\t})\n","import type { QualifierErrorCode } from './types.js'\n\n/**\n * A coded programmer error thrown by the qualifier layer.\n *\n * @remarks\n * `DEFINITION` — a qualification definition failed semantic validation.\n * `MISMATCH` — a subject is not a record or already carries the reserved\n * `qualification` key. `DESTROYED` — use of a destroyed qualifier.\n * `ENGINE` — the underlying reason engine threw while running a pass (the\n * original throw is preserved as `context.cause`).\n */\nexport class QualifierError extends Error {\n\treadonly code: QualifierErrorCode\n\treadonly context: unknown\n\n\tconstructor(code: QualifierErrorCode, message: string, context?: unknown) {\n\t\tsuper(message)\n\t\tthis.name = 'QualifierError'\n\t\tthis.code = code\n\t\tthis.context = context\n\t}\n}\n\n/** Narrow a caught value to a {@link QualifierError}. */\nexport function isQualifierError(value: unknown): value is QualifierError {\n\treturn value instanceof QualifierError\n}\n","import type { QualificationDefinition, QualificationPass, Ruling } from './types.js'\nimport { arrayOf, isRecord, isString, literalOf, recordOf } from '@orkestrel/contract'\nimport { isLogicalDefinition, isQuantitativeDefinition } from '@orkestrel/reason'\n\n/** Determine whether a value is an {@link Eligibility} literal. */\nexport const isEligibility = literalOf('eligible', 'ineligible', 'referral')\n\n/** Determine whether a value is a {@link QualificationEffect} literal. */\nexport const isQualificationEffect = literalOf('restriction', 'referral', 'condition')\n\n/** Determine whether a value is a {@link QualificationPass} (a quantitative or logical definition). */\nexport function isQualificationPass(value: unknown): value is QualificationPass {\n\treturn isQuantitativeDefinition(value) || isLogicalDefinition(value)\n}\n\n/** Determine whether a value is an exact {@link Ruling} record. */\nexport function isRuling(value: unknown): value is Ruling {\n\treturn recordOf(\n\t\t{\n\t\t\tid: isString,\n\t\t\tpass: isString,\n\t\t\trule: isString,\n\t\t\teffect: isQualificationEffect,\n\t\t\tscope: isString,\n\t\t\tmessage: isString,\n\t\t},\n\t\t['scope', 'message'],\n\t)(value)\n}\n\n/** Determine whether a value is an exact {@link QualificationDefinition} record. */\nexport function isQualificationDefinition(value: unknown): value is QualificationDefinition {\n\treturn recordOf(\n\t\t{\n\t\t\tid: isString,\n\t\t\tname: isString,\n\t\t\tdescription: isString,\n\t\t\tpasses: arrayOf(isQualificationPass),\n\t\t\trulings: arrayOf(isRuling),\n\t\t\tmetadata: isRecord,\n\t\t},\n\t\t['description', 'rulings', 'metadata'],\n\t)(value)\n}\n","import type {\n\tDerivation,\n\tEligibility,\n\tFinding,\n\tPremise,\n\tQualificationContext,\n\tQualificationDefinition,\n\tQualificationEffect,\n\tQualificationInput,\n\tQualificationPass,\n\tQualificationProjection,\n\tRuling,\n\tRulingInput,\n} from './types.js'\nimport type {\n\tCheck,\n\tCheckResult,\n\tEvaluatorInterface,\n\tLogicalDefinition,\n\tLogicalResult,\n\tQuantitativeResult,\n\tReasonResult,\n\tRule,\n\tSubject,\n} from '@orkestrel/reason'\nimport { EFFECT_ELIGIBILITIES, ELIGIBILITY_PRECEDENCE, QUALIFICATION_KEY } from './constants.js'\nimport { QualifierError } from './errors.js'\nimport { isFiniteNumber, isRecord, resolveField } from '@orkestrel/contract'\nimport {\n\textractAtoms,\n\textractConclusions,\n\tformatField,\n\tisBounds,\n\tisReasonError,\n} from '@orkestrel/reason'\n\n/**\n * Interpolate `{{dotted.path}}` tokens in a message template against a subject.\n *\n * @remarks\n * Each token is split on `.` into a `FieldPath` array and resolved with\n * `resolveField` (a plain string field is ONE key, never dot-split — the split\n * here is the token-to-path bridge). A finite number renders with `en-US`\n * thousand grouping (`5010` → `5,010`); any other resolved value String-coerces.\n * An UNRESOLVED path renders as the empty string.\n *\n * @param template - The message template carrying `{{dotted.path}}` tokens\n * @param subject - The record tokens resolve against\n * @returns The template with every token replaced\n *\n * @example\n * ```ts\n * import { interpolateMessage } from '@orkestrel/qualifier'\n *\n * interpolateMessage('Limit is {{limit}}', { limit: 5010 }) // 'Limit is 5,010'\n * interpolateMessage('Missing {{gone}}', {}) // 'Missing '\n * ```\n */\nexport function interpolateMessage(\n\ttemplate: string,\n\tsubject: Readonly<Record<string, unknown>>,\n): string {\n\treturn template.replace(/\\{\\{\\s*([^}]+?)\\s*\\}\\}/g, (_match, path: string) => {\n\t\tconst value = resolveField(subject, path.split('.'))\n\t\tif (value === undefined) return ''\n\t\tif (isFiniteNumber(value)) return value.toLocaleString('en-US')\n\t\treturn String(value)\n\t})\n}\n\n/**\n * Describe a {@link Premise} comparison as a display-neutral verb phrase.\n *\n * @param comparison - The comparison to describe\n * @returns A display-neutral phrase\n *\n * @example\n * ```ts\n * import { describeComparison } from '@orkestrel/qualifier'\n *\n * describeComparison('above') // 'is more than'\n * ```\n */\nexport function describeComparison(comparison: NonNullable<Premise['comparison']>): string {\n\tswitch (comparison) {\n\t\tcase 'equals':\n\t\t\treturn 'is'\n\t\tcase 'not':\n\t\t\treturn 'is not'\n\t\tcase 'above':\n\t\t\treturn 'is more than'\n\t\tcase 'below':\n\t\t\treturn 'is less than'\n\t\tcase 'from':\n\t\t\treturn 'is at least'\n\t\tcase 'to':\n\t\t\treturn 'is at most'\n\t\tcase 'any':\n\t\t\treturn 'is any of'\n\t\tcase 'none':\n\t\t\treturn 'is none of'\n\t\tcase 'between':\n\t\t\treturn 'is between'\n\t\tcase 'outside':\n\t\t\treturn 'is outside'\n\t}\n}\n\n/**\n * Render a structured or scalar expected value display-neutrally.\n *\n * @remarks\n * An array renders as its elements, each `String`-coerced, joined with `', '`.\n * A reason `Bounds`-shaped record (the `between` / `outside` expected value)\n * renders its present sides joined with the word `and`; an absent side is\n * omitted. A finite number and everything else fall back to plain `String()` —\n * a finite number is NOT grouped here (grouping is {@link interpolateMessage}'s\n * message-token-only behavior).\n *\n * @param value - The value to render\n * @returns A display-neutral rendering\n *\n * @example\n * ```ts\n * import { describeValue } from '@orkestrel/qualifier'\n *\n * describeValue([18, 25, 40]) // '18, 25, 40'\n * describeValue({ minimum: 18, maximum: 65 }) // '18 and 65'\n * describeValue(42) // '42'\n * ```\n */\nexport function describeValue(value: unknown): string {\n\tif (Array.isArray(value)) return value.map((entry) => String(entry)).join(', ')\n\tif (isBounds(value)) {\n\t\tconst sides: string[] = []\n\t\tif (value.minimum !== undefined) sides.push(String(value.minimum))\n\t\tif (value.maximum !== undefined) sides.push(String(value.maximum))\n\t\treturn sides.join(' and ')\n\t}\n\treturn String(value)\n}\n\n/**\n * Render one {@link Premise} into a display-neutral sentence.\n *\n * @param entry - The premise to render\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A display-neutral sentence\n *\n * @example\n * ```ts\n * import { describePremise } from '@orkestrel/qualifier'\n *\n * describePremise({ field: 'age', comparison: 'above', expected: 18, actual: 25, met: true })\n * // 'age is more than 18 → met'\n * ```\n */\nexport function describePremise(entry: Premise, labels?: Readonly<Record<string, string>>): string {\n\tconst status = entry.met === undefined ? 'unknown' : entry.met ? 'met' : 'not met'\n\tif (entry.field === undefined || entry.comparison === undefined) {\n\t\treturn `${entry.description ?? 'Premise'} → ${status}`\n\t}\n\tconst field = formatField(entry.field)\n\tconst label = labels?.[field] ?? entry.label ?? field\n\tconst expected = entry.expected === undefined ? '' : ` ${describeValue(entry.expected)}`\n\treturn `${label} ${describeComparison(entry.comparison)}${expected} → ${status}`\n}\n\n/**\n * Build a {@link Premise} from an authored {@link Check} and its evaluated\n * `CheckResult`.\n *\n * @param check - The authored check\n * @param result - The evaluated check result carrying the resolved `actual` and `met`\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A fresh premise\n *\n * @example\n * ```ts\n * import { premiseCheck } from '@orkestrel/qualifier'\n * import { check } from '@orkestrel/reason'\n *\n * const authored = check('age', 'above', 18)\n * premiseCheck(authored, { field: 'age', actual: 25, met: true })\n * // { field: 'age', comparison: 'above', expected: 18, actual: 25, met: true }\n * ```\n */\nexport function premiseCheck(\n\tcheck: Check,\n\tresult: CheckResult,\n\tlabels?: Readonly<Record<string, string>>,\n): Premise {\n\tconst field = formatField(check.field)\n\treturn {\n\t\tfield: check.field,\n\t\t...(labels?.[field] === undefined ? {} : { label: labels[field] }),\n\t\tcomparison: check.operator,\n\t\texpected: check.value,\n\t\tactual: result.actual,\n\t\t...(result.met === undefined ? {} : { met: result.met }),\n\t}\n}\n\n/**\n * Build rich premises for one fired {@link Rule} by walking its premise atoms\n * and re-evaluating each against the working subject.\n *\n * @remarks\n * A reason rule result carries only booleans, so this is the qualifier's own\n * premise-audit projection: each authored premise expression is flattened to its\n * atom leaves via `extractAtoms`, and each leaf's `Check` is re-evaluated through\n * the injected `evaluator`. A membership check (`any` / `none`) over an EMPTY\n * array value is content-free and is skipped.\n *\n * @param rule - The authored rule\n * @param working - The working subject to evaluate against\n * @param evaluator - The shared reason check evaluator\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A fresh, ordered list of rich premises\n *\n * @example\n * ```ts\n * import { logicalPremises } from '@orkestrel/qualifier'\n * import { atom, createEvaluator, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const evaluator = createEvaluator()\n * const premises = logicalPremises(gates.rules[0], { licensed: false }, evaluator)\n *\n * premises[0]?.met // true\n * ```\n */\nexport function logicalPremises(\n\trule: Rule,\n\tworking: Subject,\n\tevaluator: EvaluatorInterface,\n\tlabels?: Readonly<Record<string, string>>,\n): readonly Premise[] {\n\tconst output: Premise[] = []\n\tfor (const premise of rule.premises) {\n\t\tfor (const atom of extractAtoms(premise)) {\n\t\t\tconst { check } = atom\n\t\t\tif (\n\t\t\t\t(check.operator === 'any' || check.operator === 'none') &&\n\t\t\t\tArray.isArray(check.value) &&\n\t\t\t\tcheck.value.length === 0\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\toutput.push(premiseCheck(check, evaluator.evaluate(check, working), labels))\n\t\t}\n\t}\n\treturn output\n}\n\n/**\n * Locate an authored {@link Rule} by id.\n *\n * @param definition - The logical definition to search\n * @param id - The rule id\n * @returns The matching rule, or `undefined`\n *\n * @example\n * ```ts\n * import { findRule } from '@orkestrel/qualifier'\n * import { atom, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n *\n * findRule(gates, 'licensed')?.id // 'licensed'\n * ```\n */\nexport function findRule(definition: LogicalDefinition, id: string): Rule | undefined {\n\treturn definition.rules.find((rule) => rule.id === id)\n}\n\n/**\n * Project one reason result into the internal qualification namespace.\n *\n * @remarks\n * A quantitative result contributes its numeric `value`. A logical result\n * contributes a record of its `conclusion` plus every applied rule's asserted\n * conclusion fields (`extractConclusions`). A mismatched pairing contributes\n * `false`. The projection is pure — it never mutates the subject or the result.\n *\n * @param definition - The pass definition that produced the result\n * @param result - The evaluated reason result\n * @returns The pass's working projection\n *\n * @example\n * ```ts\n * import { reasonResultToProjection } from '@orkestrel/qualifier'\n * import { factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'\n *\n * const cap = quantitativeDefinition('cap', 'TIV cap', [\n * factorGroup('limit', 'sum', [staticFactor('base', 500_000)]),\n * ])\n * const result = {\n * reasoning: 'quantitative' as const,\n * value: 500_000,\n * groups: [],\n * count: 1,\n * success: true,\n * trace: [],\n * errors: [],\n * }\n * reasonResultToProjection(cap, result) // 500000\n * ```\n */\nexport function reasonResultToProjection(\n\tdefinition: QualificationPass,\n\tresult: ReasonResult,\n): QualificationProjection {\n\tif (definition.reasoning === 'quantitative' && result.reasoning === 'quantitative') {\n\t\treturn result.value\n\t}\n\tif (definition.reasoning !== 'logical' || result.reasoning !== 'logical') return false\n\tconst projection: Record<string, unknown> = { conclusion: result.conclusion }\n\tfor (const resolved of result.rules) {\n\t\tif (!resolved.applied) continue\n\t\tconst authored = definition.rules.find((rule) => rule.id === resolved.id)\n\t\tif (authored === undefined) continue\n\t\tconst conclusions = extractConclusions(authored.conclusion)\n\t\tfor (const [key, value] of Object.entries(conclusions)) projection[key] = value\n\t}\n\treturn projection\n}\n\n/**\n * Project a quantitative result into a {@link Derivation} audit record.\n *\n * @param id - The pass id\n * @param result - The evaluated quantitative result\n * @returns A fresh derivation\n *\n * @example\n * ```ts\n * import { quantitativeResultToDerivation } from '@orkestrel/qualifier'\n *\n * const result = { reasoning: 'quantitative' as const, value: 500_000, groups: [], count: 1, success: true, trace: ['cap: 500000'], errors: [] }\n * quantitativeResultToDerivation('cap', result)\n * // { id: 'cap', value: 500000, success: true, trace: ['cap: 500000'], errors: [] }\n * ```\n */\nexport function quantitativeResultToDerivation(id: string, result: QuantitativeResult): Derivation {\n\treturn {\n\t\tid,\n\t\tvalue: result.value,\n\t\tsuccess: result.success,\n\t\ttrace: [...result.trace],\n\t\terrors: [...result.errors],\n\t}\n}\n\n/**\n * Wrap a {@link QualificationContext} under {@link QUALIFICATION_KEY}.\n *\n * @param context - The accumulated projection context\n * @returns A record carrying the context under the reserved key\n *\n * @example\n * ```ts\n * import { qualificationToRecord } from '@orkestrel/qualifier'\n *\n * qualificationToRecord({ cap: 500_000 }) // { qualification: { cap: 500000 } }\n * ```\n */\nexport function qualificationToRecord(\n\tcontext: QualificationContext,\n): Readonly<Record<string, QualificationContext>> {\n\treturn { [QUALIFICATION_KEY]: context }\n}\n\n/**\n * Copy-on-write merge one pass projection into the context.\n *\n * @param context - The current projection context\n * @param id - The pass id to project under\n * @param projection - The pass's working projection\n * @returns A fresh context with the projection added\n *\n * @example\n * ```ts\n * import { mergeQualificationContext } from '@orkestrel/qualifier'\n *\n * mergeQualificationContext({ cap: 500_000 }, 'excess', 250_000)\n * // { cap: 500000, excess: 250000 }\n * ```\n */\nexport function mergeQualificationContext(\n\tcontext: QualificationContext,\n\tid: string,\n\tprojection: QualificationProjection,\n): QualificationContext {\n\treturn { ...context, [id]: projection }\n}\n\n/**\n * Join a ruling, its logical rule result, the pass, the pre-projection subject,\n * and an evaluator into a {@link Finding}.\n *\n * @remarks\n * `applied` reflects whether the ruling's rule fired. Premises are re-evaluated\n * against the SAME subject snapshot the pass evaluated, so a conclusion the pass\n * derived can never satisfy its own reported premise. The message interpolates\n * against that snapshot.\n *\n * @param ruling - The authored ruling\n * @param pass - The pass the ruling's rule lives in\n * @param result - The pass's evaluated logical result\n * @param subject - The pre-projection subject snapshot the pass evaluated\n * @param evaluator - The shared reason check evaluator\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A fresh finding\n *\n * @example\n * ```ts\n * import { rulingToFinding } from '@orkestrel/qualifier'\n * import { atom, createEvaluator, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const ruling = { id: 'license', pass: 'gates', rule: 'licensed', effect: 'restriction' as const }\n * const result = { reasoning: 'logical' as const, conclusion: true, rules: [{ id: 'licensed', applied: true, premises: [true], conclusion: true }], count: 1, success: true, trace: [], errors: [] }\n * const finding = rulingToFinding(ruling, gates, result, { licensed: false }, createEvaluator())\n *\n * finding.applied // true\n * ```\n */\nexport function rulingToFinding(\n\truling: Ruling,\n\tpass: QualificationPass,\n\tresult: LogicalResult,\n\tsubject: Subject,\n\tevaluator: EvaluatorInterface,\n\tlabels?: Readonly<Record<string, string>>,\n): Finding {\n\tconst entry = result.rules.find((item) => item.id === ruling.rule)\n\tconst applied = entry?.applied ?? false\n\tconst rule = pass.reasoning === 'logical' ? findRule(pass, ruling.rule) : undefined\n\tconst premises = rule === undefined ? [] : logicalPremises(rule, subject, evaluator, labels)\n\treturn {\n\t\tid: ruling.id,\n\t\tpass: ruling.pass,\n\t\trule: ruling.rule,\n\t\teffect: ruling.effect,\n\t\t...(ruling.scope === undefined ? {} : { scope: ruling.scope }),\n\t\tapplied,\n\t\t...(ruling.message === undefined\n\t\t\t? {}\n\t\t\t: { message: interpolateMessage(ruling.message, subject) }),\n\t\tpremises,\n\t}\n}\n\n/**\n * Derive global eligibility from applied, unscoped findings.\n *\n * @remarks\n * Only findings that both applied and carry no `scope` shape global eligibility.\n * A failed pass contributes a synthetic `referral` (fail-closed), passed through\n * `failed`.\n *\n * @param findings - The resolved findings\n * @param failed - Whether an operational pass failure occurred\n * @returns The most severe global eligibility\n *\n * @example\n * ```ts\n * import { deriveFindingEligibility } from '@orkestrel/qualifier'\n *\n * const finding = { id: 'license', pass: 'gates', rule: 'licensed', effect: 'restriction' as const, applied: true, premises: [] }\n * deriveFindingEligibility([finding]) // 'ineligible'\n * ```\n */\nexport function deriveFindingEligibility(findings: Finding[], failed = false): Eligibility {\n\tconst eligibilities = findings\n\t\t.filter((finding) => finding.applied && finding.scope === undefined)\n\t\t.map((finding) => EFFECT_ELIGIBILITIES[finding.effect])\n\tif (failed) eligibilities.push('referral')\n\treturn combineEligibilities(eligibilities)\n}\n\n/**\n * Return the most severe {@link Eligibility} in a list.\n *\n * @param eligibilities - The eligibilities to combine\n * @returns The most severe eligibility, or `'eligible'` for an empty list\n *\n * @example\n * ```ts\n * import { combineEligibilities } from '@orkestrel/qualifier'\n *\n * combineEligibilities(['eligible', 'referral']) // 'referral'\n * ```\n */\nexport function combineEligibilities(eligibilities: Eligibility[]): Eligibility {\n\tfor (const eligibility of ELIGIBILITY_PRECEDENCE) {\n\t\tif (eligibilities.includes(eligibility)) return eligibility\n\t}\n\treturn 'eligible'\n}\n\n/**\n * Derive one eligibility per finding scope.\n *\n * @remarks\n * Only applied, scoped findings contribute. Each named scope combines its own\n * findings' impacts by severity. An unmentioned scope is absent (eligible by\n * default at the program layer).\n *\n * @param findings - The resolved findings\n * @returns A fresh record of scope to its combined eligibility\n *\n * @example\n * ```ts\n * import { deriveScopeEligibilities } from '@orkestrel/qualifier'\n *\n * const finding = { id: 'coastal', pass: 'wind', rule: 'coastal', effect: 'restriction' as const, scope: 'wind', applied: true, premises: [] }\n * deriveScopeEligibilities([finding]) // { wind: 'ineligible' }\n * ```\n */\nexport function deriveScopeEligibilities(\n\tfindings: Finding[],\n): Readonly<Record<string, Eligibility>> {\n\tconst grouped: Record<string, Eligibility[]> = {}\n\tfor (const finding of findings) {\n\t\tif (!finding.applied || finding.scope === undefined) continue\n\t\tconst current = grouped[finding.scope] ?? []\n\t\tgrouped[finding.scope] = [...current, EFFECT_ELIGIBILITIES[finding.effect]]\n\t}\n\tconst scopes: Record<string, Eligibility> = {}\n\tfor (const [scope, eligibilities] of Object.entries(grouped)) {\n\t\tscopes[scope] = combineEligibilities(eligibilities)\n\t}\n\treturn scopes\n}\n\n/**\n * Find rulings whose pass or rule does not exist, whose pass is not logical, or\n * a pass id shadowing the reserved {@link QUALIFICATION_KEY}.\n *\n * @param definition - The qualification definition to check\n * @returns A fresh list of reference-error messages\n *\n * @example\n * ```ts\n * import { findMissingReferences, qualificationDefinition, rulingDefinition } from '@orkestrel/qualifier'\n * import { atom, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const definition = qualificationDefinition('standard', 'Standard', [gates], {\n * rulings: [rulingDefinition('license', 'gates', 'absent', 'restriction')],\n * })\n *\n * findMissingReferences(definition)\n * // [\"Ruling 'license' references missing rule 'absent' in pass 'gates'\"]\n * ```\n */\nexport function findMissingReferences(definition: QualificationDefinition): readonly string[] {\n\tconst errors: string[] = []\n\tconst passes = new Map(definition.passes.map((pass) => [pass.id, pass]))\n\tfor (const pass of definition.passes) {\n\t\tif (pass.id === QUALIFICATION_KEY) {\n\t\t\terrors.push(`Pass id must not equal reserved key '${QUALIFICATION_KEY}'`)\n\t\t}\n\t}\n\tfor (const ruling of definition.rulings ?? []) {\n\t\tconst pass = passes.get(ruling.pass)\n\t\tif (pass === undefined) {\n\t\t\terrors.push(`Ruling '${ruling.id}' references missing pass '${ruling.pass}'`)\n\t\t\tcontinue\n\t\t}\n\t\tif (pass.reasoning !== 'logical') {\n\t\t\terrors.push(`Ruling '${ruling.id}' references non-logical pass '${ruling.pass}'`)\n\t\t\tcontinue\n\t\t}\n\t\tif (findRule(pass, ruling.rule) === undefined) {\n\t\t\terrors.push(\n\t\t\t\t`Ruling '${ruling.id}' references missing rule '${ruling.rule}' in pass '${ruling.pass}'`,\n\t\t\t)\n\t\t}\n\t}\n\treturn errors\n}\n\n/**\n * Determine whether a subject already owns the reserved {@link QUALIFICATION_KEY}.\n *\n * @param subject - The subject to check\n * @returns `true` when the subject owns the reserved key\n *\n * @example\n * ```ts\n * import { hasReservedKey } from '@orkestrel/qualifier'\n *\n * hasReservedKey({ id: 's1', qualification: {} }) // true\n * ```\n */\nexport function hasReservedKey(subject: Subject): boolean {\n\treturn Object.hasOwn(subject, QUALIFICATION_KEY)\n}\n\n/**\n * Assert a value is a valid qualification {@link Subject}, narrowing it in place.\n *\n * @param value - The candidate subject to validate\n * @throws {@link QualifierError} `'MISMATCH'` when the value is not a record, or\n * when it already carries the reserved `qualification` key\n *\n * @example\n * ```ts\n * import { assertSubject } from '@orkestrel/qualifier'\n *\n * assertSubject({ id: 's1' }) // narrows to Subject\n * ```\n */\nexport function assertSubject(value: unknown): asserts value is Subject {\n\tif (!isRecord(value)) {\n\t\tthrow new QualifierError('MISMATCH', 'Qualification subject must be a record')\n\t}\n\tif (hasReservedKey(value)) {\n\t\tthrow new QualifierError(\n\t\t\t'MISMATCH',\n\t\t\t`Qualification subject must not contain reserved key '${QUALIFICATION_KEY}'`,\n\t\t)\n\t}\n}\n\n/**\n * Map an engine throw caught while running one pass to a typed {@link QualifierError}.\n *\n * @remarks\n * A reason `ReasonError('INVALID')` maps to `DEFINITION` (the engine rejected the\n * pass as an invalid definition); `ReasonError('DESTROYED')` maps to `DESTROYED`;\n * every other `ReasonError` code (e.g. `MISSING`) and every non-`ReasonError` throw\n * (including a `bail: true` reasoner rethrow) maps to `ENGINE`. The original throw\n * is preserved as `context.cause`.\n *\n * @param error - The value caught from the engine's `reason` call\n * @param pass - The id of the pass that was running\n * @returns A fresh, typed {@link QualifierError}\n *\n * @example\n * ```ts\n * import { mapEngineError } from '@orkestrel/qualifier'\n * import { ReasonError } from '@orkestrel/reason'\n *\n * mapEngineError(new ReasonError('MISSING', 'No reasoner registered'), 'cap').code // 'ENGINE'\n * ```\n */\nexport function mapEngineError(error: unknown, pass: string): QualifierError {\n\tif (isReasonError(error)) {\n\t\tif (error.code === 'INVALID') {\n\t\t\treturn new QualifierError(\n\t\t\t\t'DEFINITION',\n\t\t\t\t`Pass '${pass}' failed engine validation: ${error.message}`,\n\t\t\t\t{ pass, cause: error },\n\t\t\t)\n\t\t}\n\t\tif (error.code === 'DESTROYED') {\n\t\t\treturn new QualifierError(\n\t\t\t\t'DESTROYED',\n\t\t\t\t`Qualifier engine was destroyed during pass '${pass}'`,\n\t\t\t\t{ pass, cause: error },\n\t\t\t)\n\t\t}\n\t\treturn new QualifierError('ENGINE', `Pass '${pass}' engine failure: ${error.message}`, {\n\t\t\tpass,\n\t\t\tcause: error,\n\t\t})\n\t}\n\tconst message = error instanceof Error ? error.message : String(error)\n\treturn new QualifierError('ENGINE', `Pass '${pass}' engine failure: ${message}`, {\n\t\tpass,\n\t\tcause: error,\n\t})\n}\n\n/**\n * Find logical passes carrying no rulings.\n *\n * @remarks\n * A logical pass with no matching ruling can never contribute a finding, so its\n * rules only ever influence later passes' reads — this is very likely an authoring\n * oversight.\n *\n * @param definition - The qualification definition to check\n * @returns A fresh list of warning messages\n *\n * @example\n * ```ts\n * import { findEmptyLogicalPasses, qualificationDefinition } from '@orkestrel/qualifier'\n * import { atom, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const definition = qualificationDefinition('standard', 'Standard', [gates])\n *\n * findEmptyLogicalPasses(definition) // [\"Logical pass 'gates' has no rulings\"]\n * ```\n */\nexport function findEmptyLogicalPasses(definition: QualificationDefinition): readonly string[] {\n\tconst warnings: string[] = []\n\tfor (const pass of definition.passes) {\n\t\tif (pass.reasoning !== 'logical') continue\n\t\tconst hasRuling = (definition.rulings ?? []).some((ruling) => ruling.pass === pass.id)\n\t\tif (!hasRuling) warnings.push(`Logical pass '${pass.id}' has no rulings`)\n\t}\n\treturn warnings\n}\n\n/**\n * Find quantitative passes never read by a later pass.\n *\n * @remarks\n * A quantitative pass's projected value lives under `qualification.<id>` — if no\n * later logical premise or quantitative factor field reads that key, the\n * derivation is dead weight. Reads are collected per later pass: a logical pass's\n * premise atoms (conclusion atoms are writes, not reads), and a quantitative\n * pass's field/lookup/range factor sources plus factor checks.\n *\n * @param definition - The qualification definition to check\n * @returns A fresh list of warning messages\n *\n * @example\n * ```ts\n * import { findUnreadDerivations, qualificationDefinition } from '@orkestrel/qualifier'\n * import { factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'\n *\n * const cap = quantitativeDefinition('cap', 'TIV cap', [\n * factorGroup('limit', 'sum', [staticFactor('base', 500_000)]),\n * ])\n * const definition = qualificationDefinition('standard', 'Standard', [cap])\n *\n * findUnreadDerivations(definition) // [\"Quantitative pass 'cap' is never read by a later pass\"]\n * ```\n */\nexport function findUnreadDerivations(definition: QualificationDefinition): readonly string[] {\n\tconst warnings: string[] = []\n\tdefinition.passes.forEach((pass, index) => {\n\t\tif (pass.reasoning !== 'quantitative') return\n\t\tconst key = formatField(['qualification', pass.id])\n\t\tconst reads = new Set<string>()\n\t\tfor (const later of definition.passes.slice(index + 1)) {\n\t\t\tif (later.reasoning === 'logical') {\n\t\t\t\tfor (const rule of later.rules) {\n\t\t\t\t\tfor (const premise of rule.premises) {\n\t\t\t\t\t\tfor (const atom of extractAtoms(premise)) reads.add(formatField(atom.check.field))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (later.reasoning === 'quantitative') {\n\t\t\t\tfor (const group of later.groups) {\n\t\t\t\t\tfor (const factor of group.factors) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tfactor.source.origin === 'field' ||\n\t\t\t\t\t\t\tfactor.source.origin === 'lookup' ||\n\t\t\t\t\t\t\tfactor.source.origin === 'range'\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\treads.add(formatField(factor.source.field))\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfor (const check of factor.checks ?? []) reads.add(formatField(check.field))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!reads.has(key)) {\n\t\t\twarnings.push(`Quantitative pass '${pass.id}' is never read by a later pass`)\n\t\t}\n\t})\n\treturn warnings\n}\n\n/**\n * Build a {@link QualificationDefinition}.\n *\n * @remarks\n * Returns a fresh top-level definition, omitting absent optional keys;\n * `passes` and `rulings` arrays are copied, and record `metadata` is\n * shallow-copied so nested values are not deep-cloned.\n *\n * @param id - The definition id\n * @param name - The display name\n * @param passes - The ordered qualification passes\n * @param input - Optional description, rulings, and metadata\n * @returns A fresh qualification definition\n *\n * @example\n * ```ts\n * import { qualificationDefinition } from '@orkestrel/qualifier'\n *\n * qualificationDefinition('standard', 'Standard', [gates], { rulings: [ruling] })\n * ```\n */\nexport function qualificationDefinition(\n\tid: string,\n\tname: string,\n\tpasses: QualificationPass[],\n\tinput?: QualificationInput,\n): QualificationDefinition {\n\treturn {\n\t\tid,\n\t\tname,\n\t\tpasses: [...passes],\n\t\t...(input?.description === undefined ? {} : { description: input.description }),\n\t\t...(input?.rulings === undefined ? {} : { rulings: [...input.rulings] }),\n\t\t...(input?.metadata === undefined ? {} : { metadata: { ...input.metadata } }),\n\t}\n}\n\n/**\n * Build a {@link Ruling} — one authored consequence for one rule in one pass.\n *\n * @param id - The ruling id\n * @param pass - The logical pass id the rule lives in\n * @param rule - The rule id whose firing this ruling reacts to\n * @param effect - The eligibility impact when the rule fires\n * @param input - Optional selection scope and message template\n * @returns A fresh ruling\n *\n * @example\n * ```ts\n * import { rulingDefinition } from '@orkestrel/qualifier'\n *\n * rulingDefinition('license', 'gates', 'licensed', 'restriction', { message: 'A license is required' })\n * ```\n */\nexport function rulingDefinition(\n\tid: string,\n\tpass: string,\n\trule: string,\n\teffect: QualificationEffect,\n\tinput?: RulingInput,\n): Ruling {\n\treturn {\n\t\tid,\n\t\tpass,\n\t\trule,\n\t\teffect,\n\t\t...(input?.scope === undefined ? {} : { scope: input.scope }),\n\t\t...(input?.message === undefined ? {} : { message: input.message }),\n\t}\n}\n","import type {\n\tDerivation,\n\tFinding,\n\tQualificationContext,\n\tQualificationDefinition,\n\tQualificationResult,\n\tQualificationValidationResult,\n\tQualifierEventMap,\n\tQualifierInterface,\n\tQualifierOptions,\n} from './types.js'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { EvaluatorInterface, ReasonInterface, ReasonResult, Subject } from '@orkestrel/reason'\nimport { DEFAULT_QUALIFIER_VALIDATE } from './constants.js'\nimport { QualifierError } from './errors.js'\nimport {\n\tassertSubject,\n\tderiveFindingEligibility,\n\tderiveScopeEligibilities,\n\tfindEmptyLogicalPasses,\n\tfindMissingReferences,\n\tfindUnreadDerivations,\n\tmapEngineError,\n\tmergeQualificationContext,\n\tqualificationToRecord,\n\tquantitativeResultToDerivation,\n\treasonResultToProjection,\n\trulingToFinding,\n} from './helpers.js'\nimport { isQualificationPass, isRuling } from './validators.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport {\n\tcreateEvaluator,\n\tcreateLogicalReasoner,\n\tcreateQuantitativeReasoner,\n\tcreateReason,\n\tfindDuplicates,\n} from '@orkestrel/reason'\n\n/**\n * A qualifier — runs ordered passes over one reason engine and returns\n * eligibility.\n *\n * @remarks\n * The engine is OWNED when self-created (destroyed on `destroy()`) and borrowed\n * when injected (never destroyed). `qualify` builds its working subject through\n * copy-on-write overlays only — the caller's `subject` is never mutated, and the\n * qualifier's internal projection is discarded after each call.\n */\nexport class Qualifier implements QualifierInterface {\n\treadonly #emitter: Emitter<QualifierEventMap>\n\treadonly #engine: ReasonInterface\n\treadonly #evaluator: EvaluatorInterface\n\treadonly #owned: boolean\n\treadonly #validate: boolean\n\treadonly #labels: Readonly<Record<string, string>> | undefined\n\t#destroyed = false\n\n\tconstructor(options?: QualifierOptions) {\n\t\tthis.#emitter = new Emitter({\n\t\t\t...(options?.on === undefined ? {} : { on: options.on }),\n\t\t\t...(options?.error === undefined ? {} : { error: options.error }),\n\t\t})\n\t\tthis.#evaluator = createEvaluator()\n\t\tthis.#owned = options?.engine === undefined\n\t\tthis.#engine =\n\t\t\toptions?.engine ??\n\t\t\tcreateReason({\n\t\t\t\treasoners: [\n\t\t\t\t\tcreateQuantitativeReasoner({ evaluator: this.#evaluator }),\n\t\t\t\t\tcreateLogicalReasoner({ evaluator: this.#evaluator }),\n\t\t\t\t],\n\t\t\t\tbail: false,\n\t\t\t})\n\t\tthis.#validate = options?.validate ?? DEFAULT_QUALIFIER_VALIDATE\n\t\tthis.#labels = options?.labels\n\t}\n\n\tget emitter(): EmitterInterface<QualifierEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tqualify(subject: Subject, definition: QualificationDefinition): QualificationResult {\n\t\tthis.#alive()\n\t\tif (this.#validate) {\n\t\t\tconst validation = this.validate(definition)\n\t\t\tif (!validation.valid) {\n\t\t\t\tthrow new QualifierError(\n\t\t\t\t\t'DEFINITION',\n\t\t\t\t\t`Qualification definition is invalid: ${validation.errors.join(', ')}`,\n\t\t\t\t\tdefinition.id,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\treturn this.#qualify(subject, definition)\n\t}\n\n\tvalidate(definition: QualificationDefinition): QualificationValidationResult {\n\t\tthis.#alive()\n\t\tconst errors: string[] = []\n\t\tconst warnings: string[] = []\n\t\tif (definition.id.length === 0) errors.push('Definition id must not be empty')\n\t\tif (definition.name.length === 0) errors.push('Definition name must not be empty')\n\t\tdefinition.passes.forEach((pass, index) => {\n\t\t\tif (!isQualificationPass(pass)) {\n\t\t\t\terrors.push(`Pass at index ${index} is not a valid qualification pass`)\n\t\t\t}\n\t\t})\n\t\t;(definition.rulings ?? []).forEach((ruling, index) => {\n\t\t\tif (!isRuling(ruling)) errors.push(`Ruling at index ${index} is not a valid ruling`)\n\t\t})\n\t\tfor (const id of findDuplicates(definition.passes)) errors.push(`Duplicate pass id '${id}'`)\n\t\tfor (const id of findDuplicates(definition.rulings ?? [])) {\n\t\t\terrors.push(`Duplicate ruling id '${id}'`)\n\t\t}\n\t\tfor (const reference of findMissingReferences(definition)) errors.push(reference)\n\t\tif (definition.passes.length === 0) warnings.push('Definition has no passes')\n\t\tfor (const warning of findEmptyLogicalPasses(definition)) warnings.push(warning)\n\t\tfor (const warning of findUnreadDerivations(definition)) warnings.push(warning)\n\t\treturn { valid: errors.length === 0, errors, warnings }\n\t}\n\n\tdestroy(): void {\n\t\tif (this.#destroyed) return\n\t\tif (this.#owned) this.#engine.destroy()\n\t\tthis.#destroyed = true\n\t\tthis.#emitter.emit('destroy')\n\t\tthis.#emitter.destroy()\n\t}\n\n\t#qualify(subject: Subject, definition: QualificationDefinition): QualificationResult {\n\t\tassertSubject(subject)\n\t\tlet context: QualificationContext = {}\n\t\tlet working: Subject = { ...subject }\n\t\tconst findings: Finding[] = []\n\t\tconst derivations: Derivation[] = []\n\t\tconst trace: string[] = []\n\t\tconst errors: string[] = []\n\t\tlet failed = false\n\n\t\tfor (const pass of definition.passes) {\n\t\t\tthis.#alive()\n\t\t\tconst evaluated = working\n\t\t\tlet result: ReasonResult\n\t\t\ttry {\n\t\t\t\tresult = this.#engine.reason(evaluated, pass)\n\t\t\t} catch (error) {\n\t\t\t\tthrow mapEngineError(error, pass.id)\n\t\t\t}\n\n\t\t\ttrace.push(...result.trace.map((entry) => `${pass.id}: ${entry}`))\n\t\t\terrors.push(...result.errors.map((entry) => `${pass.id}: ${entry}`))\n\n\t\t\tif (result.reasoning === 'quantitative') {\n\t\t\t\tconst derivation = quantitativeResultToDerivation(pass.id, result)\n\t\t\t\tderivations.push(derivation)\n\t\t\t\tthis.#emitter.emit('derive', derivation)\n\t\t\t}\n\n\t\t\tif (result.reasoning === 'logical') {\n\t\t\t\tconst rulings = (definition.rulings ?? []).filter((ruling) => ruling.pass === pass.id)\n\t\t\t\tfor (const ruling of rulings) {\n\t\t\t\t\tconst finding = rulingToFinding(\n\t\t\t\t\t\truling,\n\t\t\t\t\t\tpass,\n\t\t\t\t\t\tresult,\n\t\t\t\t\t\tevaluated,\n\t\t\t\t\t\tthis.#evaluator,\n\t\t\t\t\t\tthis.#labels,\n\t\t\t\t\t)\n\t\t\t\t\tfindings.push(finding)\n\t\t\t\t\tthis.#emitter.emit('finding', finding)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst projection = reasonResultToProjection(pass, result)\n\t\t\tcontext = mergeQualificationContext(context, pass.id, projection)\n\t\t\tworking = { ...subject, ...qualificationToRecord(context) }\n\n\t\t\tif (!result.success) {\n\t\t\t\tfailed = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif (deriveFindingEligibility(findings) === 'ineligible') break\n\t\t}\n\n\t\tconst success = !failed && errors.length === 0\n\t\tconst scopes = deriveScopeEligibilities(findings)\n\t\tconst qualification: QualificationResult = Object.freeze({\n\t\t\tid: definition.id,\n\t\t\tname: definition.name,\n\t\t\teligibility: deriveFindingEligibility(findings, !success),\n\t\t\tscopes: Object.freeze(scopes),\n\t\t\tfindings: Object.freeze(findings),\n\t\t\tderivations: Object.freeze(derivations),\n\t\t\tsuccess,\n\t\t\ttrace: Object.freeze(trace),\n\t\t\terrors: Object.freeze(errors),\n\t\t})\n\t\tthis.#emitter.emit('qualify', qualification)\n\t\treturn qualification\n\t}\n\n\t#alive(): void {\n\t\tif (this.#destroyed) {\n\t\t\tthrow new QualifierError('DESTROYED', 'Qualifier has been destroyed')\n\t\t}\n\t}\n}\n","import type { QualifierInterface, QualifierOptions } from './types.js'\nimport { Qualifier } from './Qualifier.js'\n\n/**\n * Create one qualifier over a reason engine.\n *\n * @remarks\n * A standalone qualifier creates and OWNS one shared quantitative-plus-logical\n * reason engine, destroying it on `destroy()`. An injected `options.engine`\n * remains caller-owned and is never destroyed. Validation is on by default.\n *\n * @param options - Optional injected engine, validation, labels, and emitter hooks\n * @returns A {@link QualifierInterface}\n *\n * @example\n * ```ts\n * import { createQualifier } from '@orkestrel/qualifier'\n *\n * const qualifier = createQualifier()\n * qualifier.qualify({ id: 'risk-1' }, definition)\n * qualifier.destroy()\n * ```\n */\nexport function createQualifier(options?: QualifierOptions): QualifierInterface {\n\treturn new Qualifier(options)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["#emitter","#engine","#evaluator","#owned","#validate","#labels","#alive","#qualify","#destroyed"],"sources":["../../../src/core/constants.ts","../../../src/core/errors.ts","../../../src/core/validators.ts","../../../src/core/helpers.ts","../../../src/core/Qualifier.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { Eligibility, QualificationEffect } from './types.js'\n\n/** Default definition validation policy for `createQualifier` / `Qualifier.qualify`. */\nexport const DEFAULT_QUALIFIER_VALIDATE = true\n\n/** The reserved internal projection namespace a pass's working projection is written under. */\nexport const QUALIFICATION_KEY = 'qualification'\n\n/** Eligibility severity order — most to least severe. */\nexport const ELIGIBILITY_PRECEDENCE: readonly Eligibility[] = Object.freeze([\n\t'ineligible',\n\t'referral',\n\t'eligible',\n])\n\n/** The eligibility impact of each {@link QualificationEffect}; `condition` remains eligible. */\nexport const EFFECT_ELIGIBILITIES: Readonly<Record<QualificationEffect, Eligibility>> =\n\tObject.freeze({\n\t\trestriction: 'ineligible',\n\t\treferral: 'referral',\n\t\tcondition: 'eligible',\n\t})\n","import type { QualifierErrorCode } from './types.js'\n\n/**\n * A coded programmer error thrown by the qualifier layer.\n *\n * @remarks\n * `DEFINITION` — a qualification definition failed semantic validation.\n * `MISMATCH` — a subject is not a record or already carries the reserved\n * `qualification` key. `DESTROYED` — use of a destroyed qualifier.\n * `ENGINE` — the underlying reason engine threw while running a pass (the\n * original throw is preserved as `context.cause`).\n */\nexport class QualifierError extends Error {\n\treadonly code: QualifierErrorCode\n\treadonly context: unknown\n\n\tconstructor(code: QualifierErrorCode, message: string, context?: unknown) {\n\t\tsuper(message)\n\t\tthis.name = 'QualifierError'\n\t\tthis.code = code\n\t\tthis.context = context\n\t}\n}\n\n/** Narrow a caught value to a {@link QualifierError}. */\nexport function isQualifierError(value: unknown): value is QualifierError {\n\treturn value instanceof QualifierError\n}\n","import type {\n\tDerivation,\n\tEligibility,\n\tFinding,\n\tPremise,\n\tQualificationDefinition,\n\tQualificationPass,\n\tQualificationResult,\n\tQualificationValidationResult,\n\tRuling,\n} from './types.js'\nimport type { Guard } from '@orkestrel/contract'\nimport {\n\tarrayOf,\n\tisBoolean,\n\tisNumber,\n\tisRecord,\n\tisString,\n\tliteralOf,\n\tobjectOf,\n\trecordOf,\n\twhereOf,\n} from '@orkestrel/contract'\nimport {\n\tisComparison,\n\tisFieldPath,\n\tisLogicalDefinition,\n\tisQuantitativeDefinition,\n\tisReasonValidationResult,\n} from '@orkestrel/reason'\n\n/** Determine whether a value is an {@link Eligibility} literal. */\nexport const isEligibility = literalOf('eligible', 'ineligible', 'referral')\n\n/** Determine whether a value is a {@link QualificationEffect} literal. */\nexport const isQualificationEffect = literalOf('restriction', 'referral', 'condition')\n\n/**\n * Determine whether a value is an open string-keyed record of {@link Eligibility} values.\n *\n * @remarks\n * Every own string-named property is checked, including non-enumerable properties. Inherited and\n * symbol-named members are left unchecked because they are outside the record this guard certifies.\n *\n * @param value - The value to test\n * @returns `true` when `value` is a non-array object whose own string-named values are eligibilities\n */\nexport function isEligibilityRecord(\n\tvalue: unknown,\n): value is Readonly<Record<string, Eligibility>> {\n\treturn whereOf(objectOf({}), (record): record is Readonly<Record<string, Eligibility>> =>\n\t\tObject.getOwnPropertyNames(record).every((key) => isEligibility(Reflect.get(record, key))),\n\t)(value)\n}\n\n/**\n * Determine whether a value is an open result-side {@link Premise}.\n *\n * @remarks\n * `expected` is unchecked and may be absent or contain any value because its published type is\n * `unknown`. `actual` is unchecked and may be absent or contain any value for the same reason.\n * Unknown members are admitted.\n *\n * @param value - The value to test\n * @returns `true` when every checked premise member follows its published type\n */\nexport function isPremise(value: unknown): value is Premise {\n\treturn objectOf(\n\t\t{\n\t\t\tfield: isFieldPath,\n\t\t\tlabel: isString,\n\t\t\tdescription: isString,\n\t\t\tcomparison: isComparison,\n\t\t\tmet: isBoolean,\n\t\t},\n\t\t['field', 'label', 'description', 'comparison', 'met'],\n\t)(value)\n}\n\n/**\n * Determine whether a value is an open result-side {@link Finding}.\n *\n * @remarks\n * Unknown members are admitted. Optional `scope` and `message` members may be absent or\n * `undefined`; a present defined value must be a string.\n *\n * @param value - The value to test\n * @returns `true` when every published finding member is valid\n */\nexport function isFinding(value: unknown): value is Finding {\n\treturn objectOf(\n\t\t{\n\t\t\tid: isString,\n\t\t\tpass: isString,\n\t\t\trule: isString,\n\t\t\teffect: isQualificationEffect,\n\t\t\tscope: isString,\n\t\t\tapplied: isBoolean,\n\t\t\tmessage: isString,\n\t\t\tpremises: arrayOf(isPremise),\n\t\t},\n\t\t['scope', 'message'],\n\t)(value)\n}\n\n/**\n * Determine whether a value is an open result-side {@link Derivation}.\n *\n * @remarks\n * Unknown members are admitted. `value` follows the published `number` type, including `NaN` and\n * infinities.\n *\n * @param value - The value to test\n * @returns `true` when every published derivation member is valid\n */\nexport function isDerivation(value: unknown): value is Derivation {\n\treturn objectOf({\n\t\tid: isString,\n\t\tvalue: isNumber,\n\t\tsuccess: isBoolean,\n\t\ttrace: arrayOf(isString),\n\t\terrors: arrayOf(isString),\n\t})(value)\n}\n\n/**\n * Determine whether a value is an open {@link QualificationResult} returned by a qualifier.\n *\n * @remarks\n * This guard is result-postured for values returned through a borrowed qualifier interface. It\n * admits unknown members and class instances while checking the complete published result closure.\n *\n * @param value - The value to test\n * @returns `true` when every published qualification-result member is valid\n */\nexport function isQualificationResult(value: unknown): value is QualificationResult {\n\treturn objectOf({\n\t\tid: isString,\n\t\tname: isString,\n\t\teligibility: isEligibility,\n\t\tscopes: isEligibilityRecord,\n\t\tfindings: arrayOf(isFinding),\n\t\tderivations: arrayOf(isDerivation),\n\t\tsuccess: isBoolean,\n\t\ttrace: arrayOf(isString),\n\t\terrors: arrayOf(isString),\n\t})(value)\n}\n\n/**\n * Determine whether a value is an open {@link QualificationValidationResult} returned by a qualifier.\n *\n * @remarks\n * `QualificationValidationResult` is an alias of reasons' validation-result type, so this IS\n * reasons' published open result guard by delegation — the alias tracks reasons' shape\n * automatically, and a hand-written member list here would silently stop checking a member a\n * later reason release adds.\n *\n * @param value - The value to test\n * @returns `true` when every published qualification-validation-result member is valid\n */\nexport const isQualificationValidationResult: Guard<QualificationValidationResult> =\n\tisReasonValidationResult\n\n/** Determine whether a value is a {@link QualificationPass} (a quantitative or logical definition). */\nexport function isQualificationPass(value: unknown): value is QualificationPass {\n\treturn isQuantitativeDefinition(value) || isLogicalDefinition(value)\n}\n\n/** Determine whether a value is an exact {@link Ruling} record. */\nexport function isRuling(value: unknown): value is Ruling {\n\treturn recordOf(\n\t\t{\n\t\t\tid: isString,\n\t\t\tpass: isString,\n\t\t\trule: isString,\n\t\t\teffect: isQualificationEffect,\n\t\t\tscope: isString,\n\t\t\tmessage: isString,\n\t\t},\n\t\t['scope', 'message'],\n\t)(value)\n}\n\n/** Determine whether a value is an exact {@link QualificationDefinition} record. */\nexport function isQualificationDefinition(value: unknown): value is QualificationDefinition {\n\treturn recordOf(\n\t\t{\n\t\t\tid: isString,\n\t\t\tname: isString,\n\t\t\tdescription: isString,\n\t\t\tpasses: arrayOf(isQualificationPass),\n\t\t\trulings: arrayOf(isRuling),\n\t\t\tmetadata: isRecord,\n\t\t},\n\t\t['description', 'rulings', 'metadata'],\n\t)(value)\n}\n","import type {\n\tDerivation,\n\tEligibility,\n\tFinding,\n\tPremise,\n\tQualificationContext,\n\tQualificationDefinition,\n\tQualificationEffect,\n\tQualificationInput,\n\tQualificationPass,\n\tQualificationProjection,\n\tRuling,\n\tRulingInput,\n} from './types.js'\nimport type {\n\tCheck,\n\tCheckResult,\n\tEvaluatorInterface,\n\tLogicalDefinition,\n\tLogicalResult,\n\tQuantitativeResult,\n\tReasonResult,\n\tRule,\n\tSubject,\n} from '@orkestrel/reason'\nimport { EFFECT_ELIGIBILITIES, ELIGIBILITY_PRECEDENCE, QUALIFICATION_KEY } from './constants.js'\nimport { QualifierError } from './errors.js'\nimport { isFiniteNumber, isRecord, resolveField } from '@orkestrel/contract'\nimport {\n\textractAtoms,\n\textractConclusions,\n\tformatField,\n\tisBounds,\n\tisReasonError,\n} from '@orkestrel/reason'\n\n/**\n * Interpolate `{{dotted.path}}` tokens in a message template against a subject.\n *\n * @remarks\n * Each token is split on `.` into a `FieldPath` array and resolved with\n * `resolveField` (a plain string field is ONE key, never dot-split — the split\n * here is the token-to-path bridge). A finite number renders with `en-US`\n * thousand grouping (`5010` → `5,010`); any other resolved value String-coerces.\n * An UNRESOLVED path renders as the empty string.\n *\n * @param template - The message template carrying `{{dotted.path}}` tokens\n * @param subject - The record tokens resolve against\n * @returns The template with every token replaced\n *\n * @example\n * ```ts\n * import { interpolateMessage } from '@orkestrel/qualifier'\n *\n * interpolateMessage('Limit is {{limit}}', { limit: 5010 }) // 'Limit is 5,010'\n * interpolateMessage('Missing {{gone}}', {}) // 'Missing '\n * ```\n */\nexport function interpolateMessage(\n\ttemplate: string,\n\tsubject: Readonly<Record<string, unknown>>,\n): string {\n\treturn template.replace(/\\{\\{\\s*([^}]+?)\\s*\\}\\}/g, (_match, path: string) => {\n\t\tconst value = resolveField(subject, path.split('.'))\n\t\tif (value === undefined) return ''\n\t\tif (isFiniteNumber(value)) return value.toLocaleString('en-US')\n\t\treturn String(value)\n\t})\n}\n\n/**\n * Describe a {@link Premise} comparison as a display-neutral verb phrase.\n *\n * @param comparison - The comparison to describe\n * @returns A display-neutral phrase\n *\n * @example\n * ```ts\n * import { describeComparison } from '@orkestrel/qualifier'\n *\n * describeComparison('above') // 'is more than'\n * ```\n */\nexport function describeComparison(comparison: NonNullable<Premise['comparison']>): string {\n\tswitch (comparison) {\n\t\tcase 'equals':\n\t\t\treturn 'is'\n\t\tcase 'not':\n\t\t\treturn 'is not'\n\t\tcase 'above':\n\t\t\treturn 'is more than'\n\t\tcase 'below':\n\t\t\treturn 'is less than'\n\t\tcase 'from':\n\t\t\treturn 'is at least'\n\t\tcase 'to':\n\t\t\treturn 'is at most'\n\t\tcase 'any':\n\t\t\treturn 'is any of'\n\t\tcase 'none':\n\t\t\treturn 'is none of'\n\t\tcase 'between':\n\t\t\treturn 'is between'\n\t\tcase 'outside':\n\t\t\treturn 'is outside'\n\t}\n}\n\n/**\n * Render a structured or scalar expected value display-neutrally.\n *\n * @remarks\n * An array renders as its elements, each `String`-coerced, joined with `', '`.\n * A reason `Bounds`-shaped record (the `between` / `outside` expected value)\n * renders its present sides joined with the word `and`; an absent side is\n * omitted. A finite number and everything else fall back to plain `String()` —\n * a finite number is NOT grouped here (grouping is {@link interpolateMessage}'s\n * message-token-only behavior).\n *\n * @param value - The value to render\n * @returns A display-neutral rendering\n *\n * @example\n * ```ts\n * import { describeValue } from '@orkestrel/qualifier'\n *\n * describeValue([18, 25, 40]) // '18, 25, 40'\n * describeValue({ minimum: 18, maximum: 65 }) // '18 and 65'\n * describeValue(42) // '42'\n * ```\n */\nexport function describeValue(value: unknown): string {\n\tif (Array.isArray(value)) return value.map((entry) => String(entry)).join(', ')\n\tif (isBounds(value)) {\n\t\tconst sides: string[] = []\n\t\tif (value.minimum !== undefined) sides.push(String(value.minimum))\n\t\tif (value.maximum !== undefined) sides.push(String(value.maximum))\n\t\treturn sides.join(' and ')\n\t}\n\treturn String(value)\n}\n\n/**\n * Render one {@link Premise} into a display-neutral sentence.\n *\n * @param entry - The premise to render\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A display-neutral sentence\n *\n * @example\n * ```ts\n * import { describePremise } from '@orkestrel/qualifier'\n *\n * describePremise({ field: 'age', comparison: 'above', expected: 18, actual: 25, met: true })\n * // 'age is more than 18 → met'\n * ```\n */\nexport function describePremise(entry: Premise, labels?: Readonly<Record<string, string>>): string {\n\tconst status = entry.met === undefined ? 'unknown' : entry.met ? 'met' : 'not met'\n\tif (entry.field === undefined || entry.comparison === undefined) {\n\t\treturn `${entry.description ?? 'Premise'} → ${status}`\n\t}\n\tconst field = formatField(entry.field)\n\tconst label = labels?.[field] ?? entry.label ?? field\n\tconst expected = entry.expected === undefined ? '' : ` ${describeValue(entry.expected)}`\n\treturn `${label} ${describeComparison(entry.comparison)}${expected} → ${status}`\n}\n\n/**\n * Build a {@link Premise} from an authored {@link Check} and its evaluated\n * `CheckResult`.\n *\n * @param check - The authored check\n * @param result - The evaluated check result carrying the resolved `actual` and `met`\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A fresh premise\n *\n * @example\n * ```ts\n * import { premiseCheck } from '@orkestrel/qualifier'\n * import { check } from '@orkestrel/reason'\n *\n * const authored = check('age', 'above', 18)\n * premiseCheck(authored, { field: 'age', actual: 25, met: true })\n * // { field: 'age', comparison: 'above', expected: 18, actual: 25, met: true }\n * ```\n */\nexport function premiseCheck(\n\tcheck: Check,\n\tresult: CheckResult,\n\tlabels?: Readonly<Record<string, string>>,\n): Premise {\n\tconst field = formatField(check.field)\n\treturn {\n\t\tfield: check.field,\n\t\t...(labels?.[field] === undefined ? {} : { label: labels[field] }),\n\t\tcomparison: check.operator,\n\t\texpected: check.value,\n\t\tactual: result.actual,\n\t\tmet: result.met,\n\t}\n}\n\n/**\n * Build rich premises for one fired {@link Rule} by walking its premise atoms\n * and re-evaluating each against the working subject.\n *\n * @remarks\n * A reason rule result carries only booleans, so this is the qualifier's own\n * premise-audit projection: each authored premise expression is flattened to its\n * atom leaves via `extractAtoms`, and each leaf's `Check` is re-evaluated through\n * the injected `evaluator`. A membership check (`any` / `none`) over an EMPTY\n * array value is content-free and is skipped.\n *\n * @param rule - The authored rule\n * @param working - The working subject to evaluate against\n * @param evaluator - The shared reason check evaluator\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A fresh, ordered list of rich premises\n *\n * @example\n * ```ts\n * import { logicalPremises } from '@orkestrel/qualifier'\n * import { atom, createEvaluator, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const evaluator = createEvaluator()\n * const premises = logicalPremises(gates.rules[0], { licensed: false }, evaluator)\n *\n * premises[0]?.met // true\n * ```\n */\nexport function logicalPremises(\n\trule: Rule,\n\tworking: Subject,\n\tevaluator: EvaluatorInterface,\n\tlabels?: Readonly<Record<string, string>>,\n): readonly Premise[] {\n\tconst output: Premise[] = []\n\tfor (const premise of rule.premises) {\n\t\tfor (const atom of extractAtoms(premise)) {\n\t\t\tconst { check } = atom\n\t\t\tif (\n\t\t\t\t(check.operator === 'any' || check.operator === 'none') &&\n\t\t\t\tArray.isArray(check.value) &&\n\t\t\t\tcheck.value.length === 0\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\toutput.push(premiseCheck(check, evaluator.evaluate(check, working), labels))\n\t\t}\n\t}\n\treturn output\n}\n\n/**\n * Locate an authored {@link Rule} by id.\n *\n * @param definition - The logical definition to search\n * @param id - The rule id\n * @returns The matching rule, or `undefined`\n *\n * @example\n * ```ts\n * import { findRule } from '@orkestrel/qualifier'\n * import { atom, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n *\n * findRule(gates, 'licensed')?.id // 'licensed'\n * ```\n */\nexport function findRule(definition: LogicalDefinition, id: string): Rule | undefined {\n\treturn definition.rules.find((rule) => rule.id === id)\n}\n\n/**\n * Project one reason result into the internal qualification namespace.\n *\n * @remarks\n * A quantitative result contributes its numeric `value`. A logical result\n * contributes a record of its `conclusion` plus every applied rule's asserted\n * conclusion fields (`extractConclusions`). A mismatched pairing contributes\n * `false`. The projection is pure — it never mutates the subject or the result.\n *\n * @param definition - The pass definition that produced the result\n * @param result - The evaluated reason result\n * @returns The pass's working projection\n *\n * @example\n * ```ts\n * import { reasonResultToProjection } from '@orkestrel/qualifier'\n * import { factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'\n *\n * const cap = quantitativeDefinition('cap', 'TIV cap', [\n * factorGroup('limit', 'sum', [staticFactor('base', 500_000)]),\n * ])\n * const result = {\n * reasoning: 'quantitative' as const,\n * value: 500_000,\n * groups: [],\n * count: 1,\n * success: true,\n * trace: [],\n * errors: [],\n * }\n * reasonResultToProjection(cap, result) // 500000\n * ```\n */\nexport function reasonResultToProjection(\n\tdefinition: QualificationPass,\n\tresult: ReasonResult,\n): QualificationProjection {\n\tif (definition.reasoning === 'quantitative' && result.reasoning === 'quantitative') {\n\t\treturn result.value\n\t}\n\tif (definition.reasoning !== 'logical' || result.reasoning !== 'logical') return false\n\tconst projection: Record<string, unknown> = { conclusion: result.conclusion }\n\tfor (const resolved of result.rules) {\n\t\tif (!resolved.applied) continue\n\t\tconst authored = definition.rules.find((rule) => rule.id === resolved.id)\n\t\tif (authored === undefined) continue\n\t\tconst conclusions = extractConclusions(authored.conclusion)\n\t\tfor (const [key, value] of Object.entries(conclusions)) projection[key] = value\n\t}\n\treturn projection\n}\n\n/**\n * Project a quantitative result into a {@link Derivation} audit record.\n *\n * @param id - The pass id\n * @param result - The evaluated quantitative result\n * @returns A fresh derivation\n *\n * @example\n * ```ts\n * import { quantitativeResultToDerivation } from '@orkestrel/qualifier'\n *\n * const result = { reasoning: 'quantitative' as const, value: 500_000, groups: [], count: 1, success: true, trace: ['cap: 500000'], errors: [] }\n * quantitativeResultToDerivation('cap', result)\n * // { id: 'cap', value: 500000, success: true, trace: ['cap: 500000'], errors: [] }\n * ```\n */\nexport function quantitativeResultToDerivation(id: string, result: QuantitativeResult): Derivation {\n\treturn {\n\t\tid,\n\t\tvalue: result.value,\n\t\tsuccess: result.success,\n\t\ttrace: [...result.trace],\n\t\terrors: [...result.errors],\n\t}\n}\n\n/**\n * Wrap a {@link QualificationContext} under {@link QUALIFICATION_KEY}.\n *\n * @param context - The accumulated projection context\n * @returns A record carrying the context under the reserved key\n *\n * @example\n * ```ts\n * import { qualificationToRecord } from '@orkestrel/qualifier'\n *\n * qualificationToRecord({ cap: 500_000 }) // { qualification: { cap: 500000 } }\n * ```\n */\nexport function qualificationToRecord(\n\tcontext: QualificationContext,\n): Readonly<Record<string, QualificationContext>> {\n\treturn { [QUALIFICATION_KEY]: context }\n}\n\n/**\n * Copy-on-write merge one pass projection into the context.\n *\n * @param context - The current projection context\n * @param id - The pass id to project under\n * @param projection - The pass's working projection\n * @returns A fresh context with the projection added\n *\n * @example\n * ```ts\n * import { mergeQualificationContext } from '@orkestrel/qualifier'\n *\n * mergeQualificationContext({ cap: 500_000 }, 'excess', 250_000)\n * // { cap: 500000, excess: 250000 }\n * ```\n */\nexport function mergeQualificationContext(\n\tcontext: QualificationContext,\n\tid: string,\n\tprojection: QualificationProjection,\n): QualificationContext {\n\treturn { ...context, [id]: projection }\n}\n\n/**\n * Join a ruling, its logical rule result, the pass, the pre-projection subject,\n * and an evaluator into a {@link Finding}.\n *\n * @remarks\n * `applied` reflects whether the ruling's rule fired. Premises are re-evaluated\n * against the SAME subject snapshot the pass evaluated, so a conclusion the pass\n * derived can never satisfy its own reported premise. The message interpolates\n * against that snapshot.\n *\n * @param ruling - The authored ruling\n * @param pass - The pass the ruling's rule lives in\n * @param result - The pass's evaluated logical result\n * @param subject - The pre-projection subject snapshot the pass evaluated\n * @param evaluator - The shared reason check evaluator\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A fresh finding\n *\n * @example\n * ```ts\n * import { rulingToFinding } from '@orkestrel/qualifier'\n * import { atom, createEvaluator, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const ruling = { id: 'license', pass: 'gates', rule: 'licensed', effect: 'restriction' as const }\n * const result = { reasoning: 'logical' as const, conclusion: true, rules: [{ id: 'licensed', applied: true, premises: [true], conclusion: true }], count: 1, success: true, trace: [], errors: [] }\n * const finding = rulingToFinding(ruling, gates, result, { licensed: false }, createEvaluator())\n *\n * finding.applied // true\n * ```\n */\nexport function rulingToFinding(\n\truling: Ruling,\n\tpass: QualificationPass,\n\tresult: LogicalResult,\n\tsubject: Subject,\n\tevaluator: EvaluatorInterface,\n\tlabels?: Readonly<Record<string, string>>,\n): Finding {\n\tconst entry = result.rules.find((item) => item.id === ruling.rule)\n\tconst applied = entry?.applied ?? false\n\tconst rule = pass.reasoning === 'logical' ? findRule(pass, ruling.rule) : undefined\n\tconst premises = rule === undefined ? [] : logicalPremises(rule, subject, evaluator, labels)\n\treturn {\n\t\tid: ruling.id,\n\t\tpass: ruling.pass,\n\t\trule: ruling.rule,\n\t\teffect: ruling.effect,\n\t\t...(ruling.scope === undefined ? {} : { scope: ruling.scope }),\n\t\tapplied,\n\t\t...(ruling.message === undefined\n\t\t\t? {}\n\t\t\t: { message: interpolateMessage(ruling.message, subject) }),\n\t\tpremises,\n\t}\n}\n\n/**\n * Derive global eligibility from applied, unscoped findings.\n *\n * @remarks\n * Only findings that both applied and carry no `scope` shape global eligibility.\n * A failed pass contributes a synthetic `referral` (fail-closed), passed through\n * `failed`.\n *\n * @param findings - The resolved findings\n * @param failed - Whether an operational pass failure occurred\n * @returns The most severe global eligibility\n *\n * @example\n * ```ts\n * import { deriveFindingEligibility } from '@orkestrel/qualifier'\n *\n * const finding = { id: 'license', pass: 'gates', rule: 'licensed', effect: 'restriction' as const, applied: true, premises: [] }\n * deriveFindingEligibility([finding]) // 'ineligible'\n * ```\n */\nexport function deriveFindingEligibility(findings: Finding[], failed = false): Eligibility {\n\tconst eligibilities = findings\n\t\t.filter((finding) => finding.applied && finding.scope === undefined)\n\t\t.map((finding) => EFFECT_ELIGIBILITIES[finding.effect])\n\tif (failed) eligibilities.push('referral')\n\treturn combineEligibilities(eligibilities)\n}\n\n/**\n * Return the most severe {@link Eligibility} in a list.\n *\n * @param eligibilities - The eligibilities to combine\n * @returns The most severe eligibility, or `'eligible'` for an empty list\n *\n * @example\n * ```ts\n * import { combineEligibilities } from '@orkestrel/qualifier'\n *\n * combineEligibilities(['eligible', 'referral']) // 'referral'\n * ```\n */\nexport function combineEligibilities(eligibilities: Eligibility[]): Eligibility {\n\tfor (const eligibility of ELIGIBILITY_PRECEDENCE) {\n\t\tif (eligibilities.includes(eligibility)) return eligibility\n\t}\n\treturn 'eligible'\n}\n\n/**\n * Derive one eligibility per finding scope.\n *\n * @remarks\n * Only applied, scoped findings contribute. Each named scope combines its own\n * findings' impacts by severity. An unmentioned scope is absent (eligible by\n * default at the program layer).\n *\n * @param findings - The resolved findings\n * @returns A fresh record of scope to its combined eligibility\n *\n * @example\n * ```ts\n * import { deriveScopeEligibilities } from '@orkestrel/qualifier'\n *\n * const finding = { id: 'coastal', pass: 'wind', rule: 'coastal', effect: 'restriction' as const, scope: 'wind', applied: true, premises: [] }\n * deriveScopeEligibilities([finding]) // { wind: 'ineligible' }\n * ```\n */\nexport function deriveScopeEligibilities(\n\tfindings: Finding[],\n): Readonly<Record<string, Eligibility>> {\n\tconst grouped: Record<string, Eligibility[]> = {}\n\tfor (const finding of findings) {\n\t\tif (!finding.applied || finding.scope === undefined) continue\n\t\tconst current = grouped[finding.scope] ?? []\n\t\tgrouped[finding.scope] = [...current, EFFECT_ELIGIBILITIES[finding.effect]]\n\t}\n\tconst scopes: Record<string, Eligibility> = {}\n\tfor (const [scope, eligibilities] of Object.entries(grouped)) {\n\t\tscopes[scope] = combineEligibilities(eligibilities)\n\t}\n\treturn scopes\n}\n\n/**\n * Find rulings whose pass or rule does not exist, whose pass is not logical, or\n * a pass id shadowing the reserved {@link QUALIFICATION_KEY}.\n *\n * @param definition - The qualification definition to check\n * @returns A fresh list of reference-error messages\n *\n * @example\n * ```ts\n * import { findMissingReferences, qualificationDefinition, rulingDefinition } from '@orkestrel/qualifier'\n * import { atom, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const definition = qualificationDefinition('standard', 'Standard', [gates], {\n * rulings: [rulingDefinition('license', 'gates', 'absent', 'restriction')],\n * })\n *\n * findMissingReferences(definition)\n * // [\"Ruling 'license' references missing rule 'absent' in pass 'gates'\"]\n * ```\n */\nexport function findMissingReferences(definition: QualificationDefinition): readonly string[] {\n\tconst errors: string[] = []\n\tconst passes = new Map(definition.passes.map((pass) => [pass.id, pass]))\n\tfor (const pass of definition.passes) {\n\t\tif (pass.id === QUALIFICATION_KEY) {\n\t\t\terrors.push(`Pass id must not equal reserved key '${QUALIFICATION_KEY}'`)\n\t\t}\n\t}\n\tfor (const ruling of definition.rulings ?? []) {\n\t\tconst pass = passes.get(ruling.pass)\n\t\tif (pass === undefined) {\n\t\t\terrors.push(`Ruling '${ruling.id}' references missing pass '${ruling.pass}'`)\n\t\t\tcontinue\n\t\t}\n\t\tif (pass.reasoning !== 'logical') {\n\t\t\terrors.push(`Ruling '${ruling.id}' references non-logical pass '${ruling.pass}'`)\n\t\t\tcontinue\n\t\t}\n\t\tif (findRule(pass, ruling.rule) === undefined) {\n\t\t\terrors.push(\n\t\t\t\t`Ruling '${ruling.id}' references missing rule '${ruling.rule}' in pass '${ruling.pass}'`,\n\t\t\t)\n\t\t}\n\t}\n\treturn errors\n}\n\n/**\n * Determine whether a subject already owns the reserved {@link QUALIFICATION_KEY}.\n *\n * @param subject - The subject to check\n * @returns `true` when the subject owns the reserved key\n *\n * @example\n * ```ts\n * import { hasReservedKey } from '@orkestrel/qualifier'\n *\n * hasReservedKey({ id: 's1', qualification: {} }) // true\n * ```\n */\nexport function hasReservedKey(subject: Subject): boolean {\n\treturn Object.hasOwn(subject, QUALIFICATION_KEY)\n}\n\n/**\n * Assert a value is a valid qualification {@link Subject}, narrowing it in place.\n *\n * @param value - The candidate subject to validate\n * @throws {@link QualifierError} `'MISMATCH'` when the value is not a record, or\n * when it already carries the reserved `qualification` key\n *\n * @example\n * ```ts\n * import { assertSubject } from '@orkestrel/qualifier'\n *\n * assertSubject({ id: 's1' }) // narrows to Subject\n * ```\n */\nexport function assertSubject(value: unknown): asserts value is Subject {\n\tif (!isRecord(value)) {\n\t\tthrow new QualifierError('MISMATCH', 'Qualification subject must be a record')\n\t}\n\tif (hasReservedKey(value)) {\n\t\tthrow new QualifierError(\n\t\t\t'MISMATCH',\n\t\t\t`Qualification subject must not contain reserved key '${QUALIFICATION_KEY}'`,\n\t\t)\n\t}\n}\n\n/**\n * Map an engine throw caught while running one pass to a typed {@link QualifierError}.\n *\n * @remarks\n * A reason `ReasonError('INVALID')` maps to `DEFINITION` (the engine rejected the\n * pass as an invalid definition); `ReasonError('DESTROYED')` maps to `DESTROYED`;\n * every other `ReasonError` code (e.g. `MISSING`) and every non-`ReasonError` throw\n * (including a `bail: true` reasoner rethrow) maps to `ENGINE`. The original throw\n * is preserved as `context.cause`.\n *\n * @param error - The value caught from the engine's `reason` call\n * @param pass - The id of the pass that was running\n * @returns A fresh, typed {@link QualifierError}\n *\n * @example\n * ```ts\n * import { mapEngineError } from '@orkestrel/qualifier'\n * import { ReasonError } from '@orkestrel/reason'\n *\n * mapEngineError(new ReasonError('MISSING', 'No reasoner registered'), 'cap').code // 'ENGINE'\n * ```\n */\nexport function mapEngineError(error: unknown, pass: string): QualifierError {\n\tif (isReasonError(error)) {\n\t\tif (error.code === 'INVALID') {\n\t\t\treturn new QualifierError(\n\t\t\t\t'DEFINITION',\n\t\t\t\t`Pass '${pass}' failed engine validation: ${error.message}`,\n\t\t\t\t{ pass, cause: error },\n\t\t\t)\n\t\t}\n\t\tif (error.code === 'DESTROYED') {\n\t\t\treturn new QualifierError(\n\t\t\t\t'DESTROYED',\n\t\t\t\t`Qualifier engine was destroyed during pass '${pass}'`,\n\t\t\t\t{ pass, cause: error },\n\t\t\t)\n\t\t}\n\t\treturn new QualifierError('ENGINE', `Pass '${pass}' engine failure: ${error.message}`, {\n\t\t\tpass,\n\t\t\tcause: error,\n\t\t})\n\t}\n\tconst message = error instanceof Error ? error.message : String(error)\n\treturn new QualifierError('ENGINE', `Pass '${pass}' engine failure: ${message}`, {\n\t\tpass,\n\t\tcause: error,\n\t})\n}\n\n/**\n * Find logical passes carrying no rulings.\n *\n * @remarks\n * A logical pass with no matching ruling can never contribute a finding, so its\n * rules only ever influence later passes' reads — this is very likely an authoring\n * oversight.\n *\n * @param definition - The qualification definition to check\n * @returns A fresh list of warning messages\n *\n * @example\n * ```ts\n * import { findEmptyLogicalPasses, qualificationDefinition } from '@orkestrel/qualifier'\n * import { atom, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const definition = qualificationDefinition('standard', 'Standard', [gates])\n *\n * findEmptyLogicalPasses(definition) // [\"Logical pass 'gates' has no rulings\"]\n * ```\n */\nexport function findEmptyLogicalPasses(definition: QualificationDefinition): readonly string[] {\n\tconst warnings: string[] = []\n\tfor (const pass of definition.passes) {\n\t\tif (pass.reasoning !== 'logical') continue\n\t\tconst hasRuling = (definition.rulings ?? []).some((ruling) => ruling.pass === pass.id)\n\t\tif (!hasRuling) warnings.push(`Logical pass '${pass.id}' has no rulings`)\n\t}\n\treturn warnings\n}\n\n/**\n * Find quantitative passes never read by a later pass.\n *\n * @remarks\n * A quantitative pass's projected value lives under `qualification.<id>` — if no\n * later logical premise or quantitative factor field reads that key, the\n * derivation is dead weight. Reads are collected per later pass: a logical pass's\n * premise atoms (conclusion atoms are writes, not reads), and a quantitative\n * pass's field/lookup/range factor sources plus factor checks.\n *\n * @param definition - The qualification definition to check\n * @returns A fresh list of warning messages\n *\n * @example\n * ```ts\n * import { findUnreadDerivations, qualificationDefinition } from '@orkestrel/qualifier'\n * import { factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'\n *\n * const cap = quantitativeDefinition('cap', 'TIV cap', [\n * factorGroup('limit', 'sum', [staticFactor('base', 500_000)]),\n * ])\n * const definition = qualificationDefinition('standard', 'Standard', [cap])\n *\n * findUnreadDerivations(definition) // [\"Quantitative pass 'cap' is never read by a later pass\"]\n * ```\n */\nexport function findUnreadDerivations(definition: QualificationDefinition): readonly string[] {\n\tconst warnings: string[] = []\n\tdefinition.passes.forEach((pass, index) => {\n\t\tif (pass.reasoning !== 'quantitative') return\n\t\tconst key = formatField(['qualification', pass.id])\n\t\tconst reads = new Set<string>()\n\t\tfor (const later of definition.passes.slice(index + 1)) {\n\t\t\tif (later.reasoning === 'logical') {\n\t\t\t\tfor (const rule of later.rules) {\n\t\t\t\t\tfor (const premise of rule.premises) {\n\t\t\t\t\t\tfor (const atom of extractAtoms(premise)) reads.add(formatField(atom.check.field))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (later.reasoning === 'quantitative') {\n\t\t\t\tfor (const group of later.groups) {\n\t\t\t\t\tfor (const factor of group.factors) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tfactor.source.origin === 'field' ||\n\t\t\t\t\t\t\tfactor.source.origin === 'lookup' ||\n\t\t\t\t\t\t\tfactor.source.origin === 'range'\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\treads.add(formatField(factor.source.field))\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfor (const check of factor.checks ?? []) reads.add(formatField(check.field))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!reads.has(key)) {\n\t\t\twarnings.push(`Quantitative pass '${pass.id}' is never read by a later pass`)\n\t\t}\n\t})\n\treturn warnings\n}\n\n/**\n * Build a {@link QualificationDefinition}.\n *\n * @remarks\n * Returns a fresh top-level definition, omitting absent optional keys;\n * `passes` and `rulings` arrays are copied, and record `metadata` is\n * shallow-copied so nested values are not deep-cloned.\n *\n * @param id - The definition id\n * @param name - The display name\n * @param passes - The ordered qualification passes\n * @param input - Optional description, rulings, and metadata\n * @returns A fresh qualification definition\n *\n * @example\n * ```ts\n * import { qualificationDefinition } from '@orkestrel/qualifier'\n *\n * qualificationDefinition('standard', 'Standard', [gates], { rulings: [ruling] })\n * ```\n */\nexport function qualificationDefinition(\n\tid: string,\n\tname: string,\n\tpasses: QualificationPass[],\n\tinput?: QualificationInput,\n): QualificationDefinition {\n\treturn {\n\t\tid,\n\t\tname,\n\t\tpasses: [...passes],\n\t\t...(input?.description === undefined ? {} : { description: input.description }),\n\t\t...(input?.rulings === undefined ? {} : { rulings: [...input.rulings] }),\n\t\t...(input?.metadata === undefined ? {} : { metadata: { ...input.metadata } }),\n\t}\n}\n\n/**\n * Build a {@link Ruling} — one authored consequence for one rule in one pass.\n *\n * @param id - The ruling id\n * @param pass - The logical pass id the rule lives in\n * @param rule - The rule id whose firing this ruling reacts to\n * @param effect - The eligibility impact when the rule fires\n * @param input - Optional selection scope and message template\n * @returns A fresh ruling\n *\n * @example\n * ```ts\n * import { rulingDefinition } from '@orkestrel/qualifier'\n *\n * rulingDefinition('license', 'gates', 'licensed', 'restriction', { message: 'A license is required' })\n * ```\n */\nexport function rulingDefinition(\n\tid: string,\n\tpass: string,\n\trule: string,\n\teffect: QualificationEffect,\n\tinput?: RulingInput,\n): Ruling {\n\treturn {\n\t\tid,\n\t\tpass,\n\t\trule,\n\t\teffect,\n\t\t...(input?.scope === undefined ? {} : { scope: input.scope }),\n\t\t...(input?.message === undefined ? {} : { message: input.message }),\n\t}\n}\n","import type {\n\tDerivation,\n\tFinding,\n\tQualificationContext,\n\tQualificationDefinition,\n\tQualificationResult,\n\tQualificationValidationResult,\n\tQualifierEventMap,\n\tQualifierInterface,\n\tQualifierOptions,\n} from './types.js'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { EvaluatorInterface, ReasonInterface, ReasonResult, Subject } from '@orkestrel/reason'\nimport { DEFAULT_QUALIFIER_VALIDATE } from './constants.js'\nimport { QualifierError } from './errors.js'\nimport {\n\tassertSubject,\n\tderiveFindingEligibility,\n\tderiveScopeEligibilities,\n\tfindEmptyLogicalPasses,\n\tfindMissingReferences,\n\tfindUnreadDerivations,\n\tmapEngineError,\n\tmergeQualificationContext,\n\tqualificationToRecord,\n\tquantitativeResultToDerivation,\n\treasonResultToProjection,\n\trulingToFinding,\n} from './helpers.js'\nimport { isQualificationPass, isRuling } from './validators.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport {\n\tcreateEvaluator,\n\tcreateLogicalReasoner,\n\tcreateQuantitativeReasoner,\n\tcreateReason,\n\tfindDuplicates,\n} from '@orkestrel/reason'\n\n/**\n * A qualifier — runs ordered passes over one reason engine and returns\n * eligibility.\n *\n * @remarks\n * The engine is OWNED when self-created (destroyed on `destroy()`) and borrowed\n * when injected (never destroyed). `qualify` builds its working subject through\n * copy-on-write overlays only — the caller's `subject` is never mutated, and the\n * qualifier's internal projection is discarded after each call.\n */\nexport class Qualifier implements QualifierInterface {\n\treadonly #emitter: Emitter<QualifierEventMap>\n\treadonly #engine: ReasonInterface\n\treadonly #evaluator: EvaluatorInterface\n\treadonly #owned: boolean\n\treadonly #validate: boolean\n\treadonly #labels: Readonly<Record<string, string>> | undefined\n\t#destroyed = false\n\n\tconstructor(options?: QualifierOptions) {\n\t\tthis.#emitter = new Emitter({\n\t\t\t...(options?.on === undefined ? {} : { on: options.on }),\n\t\t\t...(options?.error === undefined ? {} : { error: options.error }),\n\t\t})\n\t\tthis.#evaluator = createEvaluator()\n\t\tthis.#owned = options?.engine === undefined\n\t\tthis.#engine =\n\t\t\toptions?.engine ??\n\t\t\tcreateReason({\n\t\t\t\treasoners: [\n\t\t\t\t\tcreateQuantitativeReasoner({ evaluator: this.#evaluator }),\n\t\t\t\t\tcreateLogicalReasoner({ evaluator: this.#evaluator }),\n\t\t\t\t],\n\t\t\t\tbail: false,\n\t\t\t})\n\t\tthis.#validate = options?.validate ?? DEFAULT_QUALIFIER_VALIDATE\n\t\tthis.#labels = options?.labels\n\t}\n\n\tget emitter(): EmitterInterface<QualifierEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tqualify(subject: Subject, definition: QualificationDefinition): QualificationResult {\n\t\tthis.#alive()\n\t\tif (this.#validate) {\n\t\t\tconst validation = this.validate(definition)\n\t\t\tif (!validation.valid) {\n\t\t\t\tthrow new QualifierError(\n\t\t\t\t\t'DEFINITION',\n\t\t\t\t\t`Qualification definition is invalid: ${validation.errors.join(', ')}`,\n\t\t\t\t\tdefinition.id,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\treturn this.#qualify(subject, definition)\n\t}\n\n\tvalidate(definition: QualificationDefinition): QualificationValidationResult {\n\t\tthis.#alive()\n\t\tconst errors: string[] = []\n\t\tconst warnings: string[] = []\n\t\tif (definition.id.length === 0) errors.push('Definition id must not be empty')\n\t\tif (definition.name.length === 0) errors.push('Definition name must not be empty')\n\t\tdefinition.passes.forEach((pass, index) => {\n\t\t\tif (!isQualificationPass(pass)) {\n\t\t\t\terrors.push(`Pass at index ${index} is not a valid qualification pass`)\n\t\t\t}\n\t\t})\n\t\t;(definition.rulings ?? []).forEach((ruling, index) => {\n\t\t\tif (!isRuling(ruling)) errors.push(`Ruling at index ${index} is not a valid ruling`)\n\t\t})\n\t\tfor (const id of findDuplicates(definition.passes)) errors.push(`Duplicate pass id '${id}'`)\n\t\tfor (const id of findDuplicates(definition.rulings ?? [])) {\n\t\t\terrors.push(`Duplicate ruling id '${id}'`)\n\t\t}\n\t\tfor (const reference of findMissingReferences(definition)) errors.push(reference)\n\t\tif (definition.passes.length === 0) warnings.push('Definition has no passes')\n\t\tfor (const warning of findEmptyLogicalPasses(definition)) warnings.push(warning)\n\t\tfor (const warning of findUnreadDerivations(definition)) warnings.push(warning)\n\t\treturn { valid: errors.length === 0, errors, warnings }\n\t}\n\n\tdestroy(): void {\n\t\tif (this.#destroyed) return\n\t\tif (this.#owned) this.#engine.destroy()\n\t\tthis.#destroyed = true\n\t\tthis.#emitter.emit('destroy')\n\t\tthis.#emitter.destroy()\n\t}\n\n\t#qualify(subject: Subject, definition: QualificationDefinition): QualificationResult {\n\t\tassertSubject(subject)\n\t\tlet context: QualificationContext = {}\n\t\tlet working: Subject = { ...subject }\n\t\tconst findings: Finding[] = []\n\t\tconst derivations: Derivation[] = []\n\t\tconst trace: string[] = []\n\t\tconst errors: string[] = []\n\t\tlet failed = false\n\n\t\tfor (const pass of definition.passes) {\n\t\t\tthis.#alive()\n\t\t\tconst evaluated = working\n\t\t\tlet result: ReasonResult\n\t\t\ttry {\n\t\t\t\tresult = this.#engine.reason(evaluated, pass)\n\t\t\t} catch (error) {\n\t\t\t\tthrow mapEngineError(error, pass.id)\n\t\t\t}\n\n\t\t\ttrace.push(...result.trace.map((entry) => `${pass.id}: ${entry}`))\n\t\t\terrors.push(...result.errors.map((entry) => `${pass.id}: ${entry}`))\n\n\t\t\tif (result.reasoning === 'quantitative') {\n\t\t\t\tconst derivation = quantitativeResultToDerivation(pass.id, result)\n\t\t\t\tderivations.push(derivation)\n\t\t\t\tthis.#emitter.emit('derive', derivation)\n\t\t\t}\n\n\t\t\tif (result.reasoning === 'logical') {\n\t\t\t\tconst rulings = (definition.rulings ?? []).filter((ruling) => ruling.pass === pass.id)\n\t\t\t\tfor (const ruling of rulings) {\n\t\t\t\t\tconst finding = rulingToFinding(\n\t\t\t\t\t\truling,\n\t\t\t\t\t\tpass,\n\t\t\t\t\t\tresult,\n\t\t\t\t\t\tevaluated,\n\t\t\t\t\t\tthis.#evaluator,\n\t\t\t\t\t\tthis.#labels,\n\t\t\t\t\t)\n\t\t\t\t\tfindings.push(finding)\n\t\t\t\t\tthis.#emitter.emit('finding', finding)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst projection = reasonResultToProjection(pass, result)\n\t\t\tcontext = mergeQualificationContext(context, pass.id, projection)\n\t\t\tworking = { ...subject, ...qualificationToRecord(context) }\n\n\t\t\tif (!result.success) {\n\t\t\t\tfailed = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif (deriveFindingEligibility(findings) === 'ineligible') break\n\t\t}\n\n\t\tconst success = !failed && errors.length === 0\n\t\tconst scopes = deriveScopeEligibilities(findings)\n\t\tconst qualification: QualificationResult = Object.freeze({\n\t\t\tid: definition.id,\n\t\t\tname: definition.name,\n\t\t\teligibility: deriveFindingEligibility(findings, !success),\n\t\t\tscopes: Object.freeze(scopes),\n\t\t\tfindings: Object.freeze(findings),\n\t\t\tderivations: Object.freeze(derivations),\n\t\t\tsuccess,\n\t\t\ttrace: Object.freeze(trace),\n\t\t\terrors: Object.freeze(errors),\n\t\t})\n\t\tthis.#emitter.emit('qualify', qualification)\n\t\treturn qualification\n\t}\n\n\t#alive(): void {\n\t\tif (this.#destroyed) {\n\t\t\tthrow new QualifierError('DESTROYED', 'Qualifier has been destroyed')\n\t\t}\n\t}\n}\n","import type { QualifierInterface, QualifierOptions } from './types.js'\nimport { Qualifier } from './Qualifier.js'\n\n/**\n * Create one qualifier over a reason engine.\n *\n * @remarks\n * A standalone qualifier creates and OWNS one shared quantitative-plus-logical\n * reason engine, destroying it on `destroy()`. An injected `options.engine`\n * remains caller-owned and is never destroyed. Validation is on by default.\n *\n * @param options - Optional injected engine, validation, labels, and emitter hooks\n * @returns A {@link QualifierInterface}\n *\n * @example\n * ```ts\n * import { createQualifier } from '@orkestrel/qualifier'\n *\n * const qualifier = createQualifier()\n * qualifier.qualify({ id: 'risk-1' }, definition)\n * qualifier.destroy()\n * ```\n */\nexport function createQualifier(options?: QualifierOptions): QualifierInterface {\n\treturn new Qualifier(options)\n}\n"],"mappings":"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"}
@@ -343,9 +343,58 @@ import { Subject } from '@orkestrel/reason';
343
343
  */
344
344
  export declare function interpolateMessage(template: string, subject: Readonly<Record<string, unknown>>): string;
345
345
 
346
+ /**
347
+ * Determine whether a value is an open result-side {@link Derivation}.
348
+ *
349
+ * @remarks
350
+ * Unknown members are admitted. `value` follows the published `number` type, including `NaN` and
351
+ * infinities.
352
+ *
353
+ * @param value - The value to test
354
+ * @returns `true` when every published derivation member is valid
355
+ */
356
+ export declare function isDerivation(value: unknown): value is Derivation;
357
+
346
358
  /** Determine whether a value is an {@link Eligibility} literal. */
347
359
  export declare const isEligibility: Guard<"eligible" | "ineligible" | "referral">;
348
360
 
361
+ /**
362
+ * Determine whether a value is an open string-keyed record of {@link Eligibility} values.
363
+ *
364
+ * @remarks
365
+ * Every own string-named property is checked, including non-enumerable properties. Inherited and
366
+ * symbol-named members are left unchecked because they are outside the record this guard certifies.
367
+ *
368
+ * @param value - The value to test
369
+ * @returns `true` when `value` is a non-array object whose own string-named values are eligibilities
370
+ */
371
+ export declare function isEligibilityRecord(value: unknown): value is Readonly<Record<string, Eligibility>>;
372
+
373
+ /**
374
+ * Determine whether a value is an open result-side {@link Finding}.
375
+ *
376
+ * @remarks
377
+ * Unknown members are admitted. Optional `scope` and `message` members may be absent or
378
+ * `undefined`; a present defined value must be a string.
379
+ *
380
+ * @param value - The value to test
381
+ * @returns `true` when every published finding member is valid
382
+ */
383
+ export declare function isFinding(value: unknown): value is Finding;
384
+
385
+ /**
386
+ * Determine whether a value is an open result-side {@link Premise}.
387
+ *
388
+ * @remarks
389
+ * `expected` is unchecked and may be absent or contain any value because its published type is
390
+ * `unknown`. `actual` is unchecked and may be absent or contain any value for the same reason.
391
+ * Unknown members are admitted.
392
+ *
393
+ * @param value - The value to test
394
+ * @returns `true` when every checked premise member follows its published type
395
+ */
396
+ export declare function isPremise(value: unknown): value is Premise;
397
+
349
398
  /** Determine whether a value is an exact {@link QualificationDefinition} record. */
350
399
  export declare function isQualificationDefinition(value: unknown): value is QualificationDefinition;
351
400
 
@@ -355,6 +404,32 @@ import { Subject } from '@orkestrel/reason';
355
404
  /** Determine whether a value is a {@link QualificationPass} (a quantitative or logical definition). */
356
405
  export declare function isQualificationPass(value: unknown): value is QualificationPass;
357
406
 
407
+ /**
408
+ * Determine whether a value is an open {@link QualificationResult} returned by a qualifier.
409
+ *
410
+ * @remarks
411
+ * This guard is result-postured for values returned through a borrowed qualifier interface. It
412
+ * admits unknown members and class instances while checking the complete published result closure.
413
+ *
414
+ * @param value - The value to test
415
+ * @returns `true` when every published qualification-result member is valid
416
+ */
417
+ export declare function isQualificationResult(value: unknown): value is QualificationResult;
418
+
419
+ /**
420
+ * Determine whether a value is an open {@link QualificationValidationResult} returned by a qualifier.
421
+ *
422
+ * @remarks
423
+ * `QualificationValidationResult` is an alias of reasons' validation-result type, so this IS
424
+ * reasons' published open result guard by delegation — the alias tracks reasons' shape
425
+ * automatically, and a hand-written member list here would silently stop checking a member a
426
+ * later reason release adds.
427
+ *
428
+ * @param value - The value to test
429
+ * @returns `true` when every published qualification-validation-result member is valid
430
+ */
431
+ export declare const isQualificationValidationResult: Guard<QualificationValidationResult>;
432
+
358
433
  /** Narrow a caught value to a {@link QualifierError}. */
359
434
  export declare function isQualifierError(value: unknown): value is QualifierError;
360
435
 
@@ -343,9 +343,58 @@ import { Subject } from '@orkestrel/reason';
343
343
  */
344
344
  export declare function interpolateMessage(template: string, subject: Readonly<Record<string, unknown>>): string;
345
345
 
346
+ /**
347
+ * Determine whether a value is an open result-side {@link Derivation}.
348
+ *
349
+ * @remarks
350
+ * Unknown members are admitted. `value` follows the published `number` type, including `NaN` and
351
+ * infinities.
352
+ *
353
+ * @param value - The value to test
354
+ * @returns `true` when every published derivation member is valid
355
+ */
356
+ export declare function isDerivation(value: unknown): value is Derivation;
357
+
346
358
  /** Determine whether a value is an {@link Eligibility} literal. */
347
359
  export declare const isEligibility: Guard<"eligible" | "ineligible" | "referral">;
348
360
 
361
+ /**
362
+ * Determine whether a value is an open string-keyed record of {@link Eligibility} values.
363
+ *
364
+ * @remarks
365
+ * Every own string-named property is checked, including non-enumerable properties. Inherited and
366
+ * symbol-named members are left unchecked because they are outside the record this guard certifies.
367
+ *
368
+ * @param value - The value to test
369
+ * @returns `true` when `value` is a non-array object whose own string-named values are eligibilities
370
+ */
371
+ export declare function isEligibilityRecord(value: unknown): value is Readonly<Record<string, Eligibility>>;
372
+
373
+ /**
374
+ * Determine whether a value is an open result-side {@link Finding}.
375
+ *
376
+ * @remarks
377
+ * Unknown members are admitted. Optional `scope` and `message` members may be absent or
378
+ * `undefined`; a present defined value must be a string.
379
+ *
380
+ * @param value - The value to test
381
+ * @returns `true` when every published finding member is valid
382
+ */
383
+ export declare function isFinding(value: unknown): value is Finding;
384
+
385
+ /**
386
+ * Determine whether a value is an open result-side {@link Premise}.
387
+ *
388
+ * @remarks
389
+ * `expected` is unchecked and may be absent or contain any value because its published type is
390
+ * `unknown`. `actual` is unchecked and may be absent or contain any value for the same reason.
391
+ * Unknown members are admitted.
392
+ *
393
+ * @param value - The value to test
394
+ * @returns `true` when every checked premise member follows its published type
395
+ */
396
+ export declare function isPremise(value: unknown): value is Premise;
397
+
349
398
  /** Determine whether a value is an exact {@link QualificationDefinition} record. */
350
399
  export declare function isQualificationDefinition(value: unknown): value is QualificationDefinition;
351
400
 
@@ -355,6 +404,32 @@ import { Subject } from '@orkestrel/reason';
355
404
  /** Determine whether a value is a {@link QualificationPass} (a quantitative or logical definition). */
356
405
  export declare function isQualificationPass(value: unknown): value is QualificationPass;
357
406
 
407
+ /**
408
+ * Determine whether a value is an open {@link QualificationResult} returned by a qualifier.
409
+ *
410
+ * @remarks
411
+ * This guard is result-postured for values returned through a borrowed qualifier interface. It
412
+ * admits unknown members and class instances while checking the complete published result closure.
413
+ *
414
+ * @param value - The value to test
415
+ * @returns `true` when every published qualification-result member is valid
416
+ */
417
+ export declare function isQualificationResult(value: unknown): value is QualificationResult;
418
+
419
+ /**
420
+ * Determine whether a value is an open {@link QualificationValidationResult} returned by a qualifier.
421
+ *
422
+ * @remarks
423
+ * `QualificationValidationResult` is an alias of reasons' validation-result type, so this IS
424
+ * reasons' published open result guard by delegation — the alias tracks reasons' shape
425
+ * automatically, and a hand-written member list here would silently stop checking a member a
426
+ * later reason release adds.
427
+ *
428
+ * @param value - The value to test
429
+ * @returns `true` when every published qualification-validation-result member is valid
430
+ */
431
+ export declare const isQualificationValidationResult: Guard<QualificationValidationResult>;
432
+
358
433
  /** Narrow a caught value to a {@link QualifierError}. */
359
434
  export declare function isQualifierError(value: unknown): value is QualifierError;
360
435
 
@@ -1,5 +1,5 @@
1
- import { arrayOf, isFiniteNumber, isRecord, isString, literalOf, recordOf, resolveField } from "@orkestrel/contract";
2
- import { createEvaluator, createLogicalReasoner, createQuantitativeReasoner, createReason, extractAtoms, extractConclusions, findDuplicates, formatField, isBounds, isLogicalDefinition, isQuantitativeDefinition, isReasonError } from "@orkestrel/reason";
1
+ import { arrayOf, isBoolean, isFiniteNumber, isNumber, isRecord, isString, literalOf, objectOf, recordOf, resolveField, whereOf } from "@orkestrel/contract";
2
+ import { createEvaluator, createLogicalReasoner, createQuantitativeReasoner, createReason, extractAtoms, extractConclusions, findDuplicates, formatField, isBounds, isComparison, isFieldPath, isLogicalDefinition, isQuantitativeDefinition, isReasonError, isReasonValidationResult } 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`. */
@@ -50,6 +50,122 @@ function isQualifierError(value) {
50
50
  var isEligibility = literalOf("eligible", "ineligible", "referral");
51
51
  /** Determine whether a value is a {@link QualificationEffect} literal. */
52
52
  var isQualificationEffect = literalOf("restriction", "referral", "condition");
53
+ /**
54
+ * Determine whether a value is an open string-keyed record of {@link Eligibility} values.
55
+ *
56
+ * @remarks
57
+ * Every own string-named property is checked, including non-enumerable properties. Inherited and
58
+ * symbol-named members are left unchecked because they are outside the record this guard certifies.
59
+ *
60
+ * @param value - The value to test
61
+ * @returns `true` when `value` is a non-array object whose own string-named values are eligibilities
62
+ */
63
+ function isEligibilityRecord(value) {
64
+ return whereOf(objectOf({}), (record) => Object.getOwnPropertyNames(record).every((key) => isEligibility(Reflect.get(record, key))))(value);
65
+ }
66
+ /**
67
+ * Determine whether a value is an open result-side {@link Premise}.
68
+ *
69
+ * @remarks
70
+ * `expected` is unchecked and may be absent or contain any value because its published type is
71
+ * `unknown`. `actual` is unchecked and may be absent or contain any value for the same reason.
72
+ * Unknown members are admitted.
73
+ *
74
+ * @param value - The value to test
75
+ * @returns `true` when every checked premise member follows its published type
76
+ */
77
+ function isPremise(value) {
78
+ return objectOf({
79
+ field: isFieldPath,
80
+ label: isString,
81
+ description: isString,
82
+ comparison: isComparison,
83
+ met: isBoolean
84
+ }, [
85
+ "field",
86
+ "label",
87
+ "description",
88
+ "comparison",
89
+ "met"
90
+ ])(value);
91
+ }
92
+ /**
93
+ * Determine whether a value is an open result-side {@link Finding}.
94
+ *
95
+ * @remarks
96
+ * Unknown members are admitted. Optional `scope` and `message` members may be absent or
97
+ * `undefined`; a present defined value must be a string.
98
+ *
99
+ * @param value - The value to test
100
+ * @returns `true` when every published finding member is valid
101
+ */
102
+ function isFinding(value) {
103
+ return objectOf({
104
+ id: isString,
105
+ pass: isString,
106
+ rule: isString,
107
+ effect: isQualificationEffect,
108
+ scope: isString,
109
+ applied: isBoolean,
110
+ message: isString,
111
+ premises: arrayOf(isPremise)
112
+ }, ["scope", "message"])(value);
113
+ }
114
+ /**
115
+ * Determine whether a value is an open result-side {@link Derivation}.
116
+ *
117
+ * @remarks
118
+ * Unknown members are admitted. `value` follows the published `number` type, including `NaN` and
119
+ * infinities.
120
+ *
121
+ * @param value - The value to test
122
+ * @returns `true` when every published derivation member is valid
123
+ */
124
+ function isDerivation(value) {
125
+ return objectOf({
126
+ id: isString,
127
+ value: isNumber,
128
+ success: isBoolean,
129
+ trace: arrayOf(isString),
130
+ errors: arrayOf(isString)
131
+ })(value);
132
+ }
133
+ /**
134
+ * Determine whether a value is an open {@link QualificationResult} returned by a qualifier.
135
+ *
136
+ * @remarks
137
+ * This guard is result-postured for values returned through a borrowed qualifier interface. It
138
+ * admits unknown members and class instances while checking the complete published result closure.
139
+ *
140
+ * @param value - The value to test
141
+ * @returns `true` when every published qualification-result member is valid
142
+ */
143
+ function isQualificationResult(value) {
144
+ return objectOf({
145
+ id: isString,
146
+ name: isString,
147
+ eligibility: isEligibility,
148
+ scopes: isEligibilityRecord,
149
+ findings: arrayOf(isFinding),
150
+ derivations: arrayOf(isDerivation),
151
+ success: isBoolean,
152
+ trace: arrayOf(isString),
153
+ errors: arrayOf(isString)
154
+ })(value);
155
+ }
156
+ /**
157
+ * Determine whether a value is an open {@link QualificationValidationResult} returned by a qualifier.
158
+ *
159
+ * @remarks
160
+ * `QualificationValidationResult` is an alias of reasons' validation-result type, so this IS
161
+ * reasons' published open result guard by delegation — the alias tracks reasons' shape
162
+ * automatically, and a hand-written member list here would silently stop checking a member a
163
+ * later reason release adds.
164
+ *
165
+ * @param value - The value to test
166
+ * @returns `true` when every published qualification-validation-result member is valid
167
+ */
168
+ var isQualificationValidationResult = isReasonValidationResult;
53
169
  /** Determine whether a value is a {@link QualificationPass} (a quantitative or logical definition). */
54
170
  function isQualificationPass(value) {
55
171
  return isQuantitativeDefinition(value) || isLogicalDefinition(value);
@@ -222,7 +338,7 @@ function premiseCheck(check, result, labels) {
222
338
  comparison: check.operator,
223
339
  expected: check.value,
224
340
  actual: result.actual,
225
- ...result.met === void 0 ? {} : { met: result.met }
341
+ met: result.met
226
342
  };
227
343
  }
228
344
  /**
@@ -933,6 +1049,6 @@ function createQualifier(options) {
933
1049
  return new Qualifier(options);
934
1050
  }
935
1051
  //#endregion
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 };
1052
+ 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, isDerivation, isEligibility, isEligibilityRecord, isFinding, isPremise, isQualificationDefinition, isQualificationEffect, isQualificationPass, isQualificationResult, isQualificationValidationResult, isQualifierError, isRuling, logicalPremises, mapEngineError, mergeQualificationContext, premiseCheck, qualificationDefinition, qualificationToRecord, quantitativeResultToDerivation, reasonResultToProjection, rulingDefinition, rulingToFinding };
937
1053
 
938
1054
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#emitter","#engine","#evaluator","#owned","#validate","#labels","#alive","#qualify","#destroyed"],"sources":["../../../src/core/constants.ts","../../../src/core/errors.ts","../../../src/core/validators.ts","../../../src/core/helpers.ts","../../../src/core/Qualifier.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { Eligibility, QualificationEffect } from './types.js'\n\n/** Default definition validation policy for `createQualifier` / `Qualifier.qualify`. */\nexport const DEFAULT_QUALIFIER_VALIDATE = true\n\n/** The reserved internal projection namespace a pass's working projection is written under. */\nexport const QUALIFICATION_KEY = 'qualification'\n\n/** Eligibility severity order — most to least severe. */\nexport const ELIGIBILITY_PRECEDENCE: readonly Eligibility[] = Object.freeze([\n\t'ineligible',\n\t'referral',\n\t'eligible',\n])\n\n/** The eligibility impact of each {@link QualificationEffect}; `condition` remains eligible. */\nexport const EFFECT_ELIGIBILITIES: Readonly<Record<QualificationEffect, Eligibility>> =\n\tObject.freeze({\n\t\trestriction: 'ineligible',\n\t\treferral: 'referral',\n\t\tcondition: 'eligible',\n\t})\n","import type { QualifierErrorCode } from './types.js'\n\n/**\n * A coded programmer error thrown by the qualifier layer.\n *\n * @remarks\n * `DEFINITION` — a qualification definition failed semantic validation.\n * `MISMATCH` — a subject is not a record or already carries the reserved\n * `qualification` key. `DESTROYED` — use of a destroyed qualifier.\n * `ENGINE` — the underlying reason engine threw while running a pass (the\n * original throw is preserved as `context.cause`).\n */\nexport class QualifierError extends Error {\n\treadonly code: QualifierErrorCode\n\treadonly context: unknown\n\n\tconstructor(code: QualifierErrorCode, message: string, context?: unknown) {\n\t\tsuper(message)\n\t\tthis.name = 'QualifierError'\n\t\tthis.code = code\n\t\tthis.context = context\n\t}\n}\n\n/** Narrow a caught value to a {@link QualifierError}. */\nexport function isQualifierError(value: unknown): value is QualifierError {\n\treturn value instanceof QualifierError\n}\n","import type { QualificationDefinition, QualificationPass, Ruling } from './types.js'\nimport { arrayOf, isRecord, isString, literalOf, recordOf } from '@orkestrel/contract'\nimport { isLogicalDefinition, isQuantitativeDefinition } from '@orkestrel/reason'\n\n/** Determine whether a value is an {@link Eligibility} literal. */\nexport const isEligibility = literalOf('eligible', 'ineligible', 'referral')\n\n/** Determine whether a value is a {@link QualificationEffect} literal. */\nexport const isQualificationEffect = literalOf('restriction', 'referral', 'condition')\n\n/** Determine whether a value is a {@link QualificationPass} (a quantitative or logical definition). */\nexport function isQualificationPass(value: unknown): value is QualificationPass {\n\treturn isQuantitativeDefinition(value) || isLogicalDefinition(value)\n}\n\n/** Determine whether a value is an exact {@link Ruling} record. */\nexport function isRuling(value: unknown): value is Ruling {\n\treturn recordOf(\n\t\t{\n\t\t\tid: isString,\n\t\t\tpass: isString,\n\t\t\trule: isString,\n\t\t\teffect: isQualificationEffect,\n\t\t\tscope: isString,\n\t\t\tmessage: isString,\n\t\t},\n\t\t['scope', 'message'],\n\t)(value)\n}\n\n/** Determine whether a value is an exact {@link QualificationDefinition} record. */\nexport function isQualificationDefinition(value: unknown): value is QualificationDefinition {\n\treturn recordOf(\n\t\t{\n\t\t\tid: isString,\n\t\t\tname: isString,\n\t\t\tdescription: isString,\n\t\t\tpasses: arrayOf(isQualificationPass),\n\t\t\trulings: arrayOf(isRuling),\n\t\t\tmetadata: isRecord,\n\t\t},\n\t\t['description', 'rulings', 'metadata'],\n\t)(value)\n}\n","import type {\n\tDerivation,\n\tEligibility,\n\tFinding,\n\tPremise,\n\tQualificationContext,\n\tQualificationDefinition,\n\tQualificationEffect,\n\tQualificationInput,\n\tQualificationPass,\n\tQualificationProjection,\n\tRuling,\n\tRulingInput,\n} from './types.js'\nimport type {\n\tCheck,\n\tCheckResult,\n\tEvaluatorInterface,\n\tLogicalDefinition,\n\tLogicalResult,\n\tQuantitativeResult,\n\tReasonResult,\n\tRule,\n\tSubject,\n} from '@orkestrel/reason'\nimport { EFFECT_ELIGIBILITIES, ELIGIBILITY_PRECEDENCE, QUALIFICATION_KEY } from './constants.js'\nimport { QualifierError } from './errors.js'\nimport { isFiniteNumber, isRecord, resolveField } from '@orkestrel/contract'\nimport {\n\textractAtoms,\n\textractConclusions,\n\tformatField,\n\tisBounds,\n\tisReasonError,\n} from '@orkestrel/reason'\n\n/**\n * Interpolate `{{dotted.path}}` tokens in a message template against a subject.\n *\n * @remarks\n * Each token is split on `.` into a `FieldPath` array and resolved with\n * `resolveField` (a plain string field is ONE key, never dot-split — the split\n * here is the token-to-path bridge). A finite number renders with `en-US`\n * thousand grouping (`5010` → `5,010`); any other resolved value String-coerces.\n * An UNRESOLVED path renders as the empty string.\n *\n * @param template - The message template carrying `{{dotted.path}}` tokens\n * @param subject - The record tokens resolve against\n * @returns The template with every token replaced\n *\n * @example\n * ```ts\n * import { interpolateMessage } from '@orkestrel/qualifier'\n *\n * interpolateMessage('Limit is {{limit}}', { limit: 5010 }) // 'Limit is 5,010'\n * interpolateMessage('Missing {{gone}}', {}) // 'Missing '\n * ```\n */\nexport function interpolateMessage(\n\ttemplate: string,\n\tsubject: Readonly<Record<string, unknown>>,\n): string {\n\treturn template.replace(/\\{\\{\\s*([^}]+?)\\s*\\}\\}/g, (_match, path: string) => {\n\t\tconst value = resolveField(subject, path.split('.'))\n\t\tif (value === undefined) return ''\n\t\tif (isFiniteNumber(value)) return value.toLocaleString('en-US')\n\t\treturn String(value)\n\t})\n}\n\n/**\n * Describe a {@link Premise} comparison as a display-neutral verb phrase.\n *\n * @param comparison - The comparison to describe\n * @returns A display-neutral phrase\n *\n * @example\n * ```ts\n * import { describeComparison } from '@orkestrel/qualifier'\n *\n * describeComparison('above') // 'is more than'\n * ```\n */\nexport function describeComparison(comparison: NonNullable<Premise['comparison']>): string {\n\tswitch (comparison) {\n\t\tcase 'equals':\n\t\t\treturn 'is'\n\t\tcase 'not':\n\t\t\treturn 'is not'\n\t\tcase 'above':\n\t\t\treturn 'is more than'\n\t\tcase 'below':\n\t\t\treturn 'is less than'\n\t\tcase 'from':\n\t\t\treturn 'is at least'\n\t\tcase 'to':\n\t\t\treturn 'is at most'\n\t\tcase 'any':\n\t\t\treturn 'is any of'\n\t\tcase 'none':\n\t\t\treturn 'is none of'\n\t\tcase 'between':\n\t\t\treturn 'is between'\n\t\tcase 'outside':\n\t\t\treturn 'is outside'\n\t}\n}\n\n/**\n * Render a structured or scalar expected value display-neutrally.\n *\n * @remarks\n * An array renders as its elements, each `String`-coerced, joined with `', '`.\n * A reason `Bounds`-shaped record (the `between` / `outside` expected value)\n * renders its present sides joined with the word `and`; an absent side is\n * omitted. A finite number and everything else fall back to plain `String()` —\n * a finite number is NOT grouped here (grouping is {@link interpolateMessage}'s\n * message-token-only behavior).\n *\n * @param value - The value to render\n * @returns A display-neutral rendering\n *\n * @example\n * ```ts\n * import { describeValue } from '@orkestrel/qualifier'\n *\n * describeValue([18, 25, 40]) // '18, 25, 40'\n * describeValue({ minimum: 18, maximum: 65 }) // '18 and 65'\n * describeValue(42) // '42'\n * ```\n */\nexport function describeValue(value: unknown): string {\n\tif (Array.isArray(value)) return value.map((entry) => String(entry)).join(', ')\n\tif (isBounds(value)) {\n\t\tconst sides: string[] = []\n\t\tif (value.minimum !== undefined) sides.push(String(value.minimum))\n\t\tif (value.maximum !== undefined) sides.push(String(value.maximum))\n\t\treturn sides.join(' and ')\n\t}\n\treturn String(value)\n}\n\n/**\n * Render one {@link Premise} into a display-neutral sentence.\n *\n * @param entry - The premise to render\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A display-neutral sentence\n *\n * @example\n * ```ts\n * import { describePremise } from '@orkestrel/qualifier'\n *\n * describePremise({ field: 'age', comparison: 'above', expected: 18, actual: 25, met: true })\n * // 'age is more than 18 → met'\n * ```\n */\nexport function describePremise(entry: Premise, labels?: Readonly<Record<string, string>>): string {\n\tconst status = entry.met === undefined ? 'unknown' : entry.met ? 'met' : 'not met'\n\tif (entry.field === undefined || entry.comparison === undefined) {\n\t\treturn `${entry.description ?? 'Premise'} → ${status}`\n\t}\n\tconst field = formatField(entry.field)\n\tconst label = labels?.[field] ?? entry.label ?? field\n\tconst expected = entry.expected === undefined ? '' : ` ${describeValue(entry.expected)}`\n\treturn `${label} ${describeComparison(entry.comparison)}${expected} → ${status}`\n}\n\n/**\n * Build a {@link Premise} from an authored {@link Check} and its evaluated\n * `CheckResult`.\n *\n * @param check - The authored check\n * @param result - The evaluated check result carrying the resolved `actual` and `met`\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A fresh premise\n *\n * @example\n * ```ts\n * import { premiseCheck } from '@orkestrel/qualifier'\n * import { check } from '@orkestrel/reason'\n *\n * const authored = check('age', 'above', 18)\n * premiseCheck(authored, { field: 'age', actual: 25, met: true })\n * // { field: 'age', comparison: 'above', expected: 18, actual: 25, met: true }\n * ```\n */\nexport function premiseCheck(\n\tcheck: Check,\n\tresult: CheckResult,\n\tlabels?: Readonly<Record<string, string>>,\n): Premise {\n\tconst field = formatField(check.field)\n\treturn {\n\t\tfield: check.field,\n\t\t...(labels?.[field] === undefined ? {} : { label: labels[field] }),\n\t\tcomparison: check.operator,\n\t\texpected: check.value,\n\t\tactual: result.actual,\n\t\t...(result.met === undefined ? {} : { met: result.met }),\n\t}\n}\n\n/**\n * Build rich premises for one fired {@link Rule} by walking its premise atoms\n * and re-evaluating each against the working subject.\n *\n * @remarks\n * A reason rule result carries only booleans, so this is the qualifier's own\n * premise-audit projection: each authored premise expression is flattened to its\n * atom leaves via `extractAtoms`, and each leaf's `Check` is re-evaluated through\n * the injected `evaluator`. A membership check (`any` / `none`) over an EMPTY\n * array value is content-free and is skipped.\n *\n * @param rule - The authored rule\n * @param working - The working subject to evaluate against\n * @param evaluator - The shared reason check evaluator\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A fresh, ordered list of rich premises\n *\n * @example\n * ```ts\n * import { logicalPremises } from '@orkestrel/qualifier'\n * import { atom, createEvaluator, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const evaluator = createEvaluator()\n * const premises = logicalPremises(gates.rules[0], { licensed: false }, evaluator)\n *\n * premises[0]?.met // true\n * ```\n */\nexport function logicalPremises(\n\trule: Rule,\n\tworking: Subject,\n\tevaluator: EvaluatorInterface,\n\tlabels?: Readonly<Record<string, string>>,\n): readonly Premise[] {\n\tconst output: Premise[] = []\n\tfor (const premise of rule.premises) {\n\t\tfor (const atom of extractAtoms(premise)) {\n\t\t\tconst { check } = atom\n\t\t\tif (\n\t\t\t\t(check.operator === 'any' || check.operator === 'none') &&\n\t\t\t\tArray.isArray(check.value) &&\n\t\t\t\tcheck.value.length === 0\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\toutput.push(premiseCheck(check, evaluator.evaluate(check, working), labels))\n\t\t}\n\t}\n\treturn output\n}\n\n/**\n * Locate an authored {@link Rule} by id.\n *\n * @param definition - The logical definition to search\n * @param id - The rule id\n * @returns The matching rule, or `undefined`\n *\n * @example\n * ```ts\n * import { findRule } from '@orkestrel/qualifier'\n * import { atom, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n *\n * findRule(gates, 'licensed')?.id // 'licensed'\n * ```\n */\nexport function findRule(definition: LogicalDefinition, id: string): Rule | undefined {\n\treturn definition.rules.find((rule) => rule.id === id)\n}\n\n/**\n * Project one reason result into the internal qualification namespace.\n *\n * @remarks\n * A quantitative result contributes its numeric `value`. A logical result\n * contributes a record of its `conclusion` plus every applied rule's asserted\n * conclusion fields (`extractConclusions`). A mismatched pairing contributes\n * `false`. The projection is pure — it never mutates the subject or the result.\n *\n * @param definition - The pass definition that produced the result\n * @param result - The evaluated reason result\n * @returns The pass's working projection\n *\n * @example\n * ```ts\n * import { reasonResultToProjection } from '@orkestrel/qualifier'\n * import { factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'\n *\n * const cap = quantitativeDefinition('cap', 'TIV cap', [\n * factorGroup('limit', 'sum', [staticFactor('base', 500_000)]),\n * ])\n * const result = {\n * reasoning: 'quantitative' as const,\n * value: 500_000,\n * groups: [],\n * count: 1,\n * success: true,\n * trace: [],\n * errors: [],\n * }\n * reasonResultToProjection(cap, result) // 500000\n * ```\n */\nexport function reasonResultToProjection(\n\tdefinition: QualificationPass,\n\tresult: ReasonResult,\n): QualificationProjection {\n\tif (definition.reasoning === 'quantitative' && result.reasoning === 'quantitative') {\n\t\treturn result.value\n\t}\n\tif (definition.reasoning !== 'logical' || result.reasoning !== 'logical') return false\n\tconst projection: Record<string, unknown> = { conclusion: result.conclusion }\n\tfor (const resolved of result.rules) {\n\t\tif (!resolved.applied) continue\n\t\tconst authored = definition.rules.find((rule) => rule.id === resolved.id)\n\t\tif (authored === undefined) continue\n\t\tconst conclusions = extractConclusions(authored.conclusion)\n\t\tfor (const [key, value] of Object.entries(conclusions)) projection[key] = value\n\t}\n\treturn projection\n}\n\n/**\n * Project a quantitative result into a {@link Derivation} audit record.\n *\n * @param id - The pass id\n * @param result - The evaluated quantitative result\n * @returns A fresh derivation\n *\n * @example\n * ```ts\n * import { quantitativeResultToDerivation } from '@orkestrel/qualifier'\n *\n * const result = { reasoning: 'quantitative' as const, value: 500_000, groups: [], count: 1, success: true, trace: ['cap: 500000'], errors: [] }\n * quantitativeResultToDerivation('cap', result)\n * // { id: 'cap', value: 500000, success: true, trace: ['cap: 500000'], errors: [] }\n * ```\n */\nexport function quantitativeResultToDerivation(id: string, result: QuantitativeResult): Derivation {\n\treturn {\n\t\tid,\n\t\tvalue: result.value,\n\t\tsuccess: result.success,\n\t\ttrace: [...result.trace],\n\t\terrors: [...result.errors],\n\t}\n}\n\n/**\n * Wrap a {@link QualificationContext} under {@link QUALIFICATION_KEY}.\n *\n * @param context - The accumulated projection context\n * @returns A record carrying the context under the reserved key\n *\n * @example\n * ```ts\n * import { qualificationToRecord } from '@orkestrel/qualifier'\n *\n * qualificationToRecord({ cap: 500_000 }) // { qualification: { cap: 500000 } }\n * ```\n */\nexport function qualificationToRecord(\n\tcontext: QualificationContext,\n): Readonly<Record<string, QualificationContext>> {\n\treturn { [QUALIFICATION_KEY]: context }\n}\n\n/**\n * Copy-on-write merge one pass projection into the context.\n *\n * @param context - The current projection context\n * @param id - The pass id to project under\n * @param projection - The pass's working projection\n * @returns A fresh context with the projection added\n *\n * @example\n * ```ts\n * import { mergeQualificationContext } from '@orkestrel/qualifier'\n *\n * mergeQualificationContext({ cap: 500_000 }, 'excess', 250_000)\n * // { cap: 500000, excess: 250000 }\n * ```\n */\nexport function mergeQualificationContext(\n\tcontext: QualificationContext,\n\tid: string,\n\tprojection: QualificationProjection,\n): QualificationContext {\n\treturn { ...context, [id]: projection }\n}\n\n/**\n * Join a ruling, its logical rule result, the pass, the pre-projection subject,\n * and an evaluator into a {@link Finding}.\n *\n * @remarks\n * `applied` reflects whether the ruling's rule fired. Premises are re-evaluated\n * against the SAME subject snapshot the pass evaluated, so a conclusion the pass\n * derived can never satisfy its own reported premise. The message interpolates\n * against that snapshot.\n *\n * @param ruling - The authored ruling\n * @param pass - The pass the ruling's rule lives in\n * @param result - The pass's evaluated logical result\n * @param subject - The pre-projection subject snapshot the pass evaluated\n * @param evaluator - The shared reason check evaluator\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A fresh finding\n *\n * @example\n * ```ts\n * import { rulingToFinding } from '@orkestrel/qualifier'\n * import { atom, createEvaluator, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const ruling = { id: 'license', pass: 'gates', rule: 'licensed', effect: 'restriction' as const }\n * const result = { reasoning: 'logical' as const, conclusion: true, rules: [{ id: 'licensed', applied: true, premises: [true], conclusion: true }], count: 1, success: true, trace: [], errors: [] }\n * const finding = rulingToFinding(ruling, gates, result, { licensed: false }, createEvaluator())\n *\n * finding.applied // true\n * ```\n */\nexport function rulingToFinding(\n\truling: Ruling,\n\tpass: QualificationPass,\n\tresult: LogicalResult,\n\tsubject: Subject,\n\tevaluator: EvaluatorInterface,\n\tlabels?: Readonly<Record<string, string>>,\n): Finding {\n\tconst entry = result.rules.find((item) => item.id === ruling.rule)\n\tconst applied = entry?.applied ?? false\n\tconst rule = pass.reasoning === 'logical' ? findRule(pass, ruling.rule) : undefined\n\tconst premises = rule === undefined ? [] : logicalPremises(rule, subject, evaluator, labels)\n\treturn {\n\t\tid: ruling.id,\n\t\tpass: ruling.pass,\n\t\trule: ruling.rule,\n\t\teffect: ruling.effect,\n\t\t...(ruling.scope === undefined ? {} : { scope: ruling.scope }),\n\t\tapplied,\n\t\t...(ruling.message === undefined\n\t\t\t? {}\n\t\t\t: { message: interpolateMessage(ruling.message, subject) }),\n\t\tpremises,\n\t}\n}\n\n/**\n * Derive global eligibility from applied, unscoped findings.\n *\n * @remarks\n * Only findings that both applied and carry no `scope` shape global eligibility.\n * A failed pass contributes a synthetic `referral` (fail-closed), passed through\n * `failed`.\n *\n * @param findings - The resolved findings\n * @param failed - Whether an operational pass failure occurred\n * @returns The most severe global eligibility\n *\n * @example\n * ```ts\n * import { deriveFindingEligibility } from '@orkestrel/qualifier'\n *\n * const finding = { id: 'license', pass: 'gates', rule: 'licensed', effect: 'restriction' as const, applied: true, premises: [] }\n * deriveFindingEligibility([finding]) // 'ineligible'\n * ```\n */\nexport function deriveFindingEligibility(findings: Finding[], failed = false): Eligibility {\n\tconst eligibilities = findings\n\t\t.filter((finding) => finding.applied && finding.scope === undefined)\n\t\t.map((finding) => EFFECT_ELIGIBILITIES[finding.effect])\n\tif (failed) eligibilities.push('referral')\n\treturn combineEligibilities(eligibilities)\n}\n\n/**\n * Return the most severe {@link Eligibility} in a list.\n *\n * @param eligibilities - The eligibilities to combine\n * @returns The most severe eligibility, or `'eligible'` for an empty list\n *\n * @example\n * ```ts\n * import { combineEligibilities } from '@orkestrel/qualifier'\n *\n * combineEligibilities(['eligible', 'referral']) // 'referral'\n * ```\n */\nexport function combineEligibilities(eligibilities: Eligibility[]): Eligibility {\n\tfor (const eligibility of ELIGIBILITY_PRECEDENCE) {\n\t\tif (eligibilities.includes(eligibility)) return eligibility\n\t}\n\treturn 'eligible'\n}\n\n/**\n * Derive one eligibility per finding scope.\n *\n * @remarks\n * Only applied, scoped findings contribute. Each named scope combines its own\n * findings' impacts by severity. An unmentioned scope is absent (eligible by\n * default at the program layer).\n *\n * @param findings - The resolved findings\n * @returns A fresh record of scope to its combined eligibility\n *\n * @example\n * ```ts\n * import { deriveScopeEligibilities } from '@orkestrel/qualifier'\n *\n * const finding = { id: 'coastal', pass: 'wind', rule: 'coastal', effect: 'restriction' as const, scope: 'wind', applied: true, premises: [] }\n * deriveScopeEligibilities([finding]) // { wind: 'ineligible' }\n * ```\n */\nexport function deriveScopeEligibilities(\n\tfindings: Finding[],\n): Readonly<Record<string, Eligibility>> {\n\tconst grouped: Record<string, Eligibility[]> = {}\n\tfor (const finding of findings) {\n\t\tif (!finding.applied || finding.scope === undefined) continue\n\t\tconst current = grouped[finding.scope] ?? []\n\t\tgrouped[finding.scope] = [...current, EFFECT_ELIGIBILITIES[finding.effect]]\n\t}\n\tconst scopes: Record<string, Eligibility> = {}\n\tfor (const [scope, eligibilities] of Object.entries(grouped)) {\n\t\tscopes[scope] = combineEligibilities(eligibilities)\n\t}\n\treturn scopes\n}\n\n/**\n * Find rulings whose pass or rule does not exist, whose pass is not logical, or\n * a pass id shadowing the reserved {@link QUALIFICATION_KEY}.\n *\n * @param definition - The qualification definition to check\n * @returns A fresh list of reference-error messages\n *\n * @example\n * ```ts\n * import { findMissingReferences, qualificationDefinition, rulingDefinition } from '@orkestrel/qualifier'\n * import { atom, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const definition = qualificationDefinition('standard', 'Standard', [gates], {\n * rulings: [rulingDefinition('license', 'gates', 'absent', 'restriction')],\n * })\n *\n * findMissingReferences(definition)\n * // [\"Ruling 'license' references missing rule 'absent' in pass 'gates'\"]\n * ```\n */\nexport function findMissingReferences(definition: QualificationDefinition): readonly string[] {\n\tconst errors: string[] = []\n\tconst passes = new Map(definition.passes.map((pass) => [pass.id, pass]))\n\tfor (const pass of definition.passes) {\n\t\tif (pass.id === QUALIFICATION_KEY) {\n\t\t\terrors.push(`Pass id must not equal reserved key '${QUALIFICATION_KEY}'`)\n\t\t}\n\t}\n\tfor (const ruling of definition.rulings ?? []) {\n\t\tconst pass = passes.get(ruling.pass)\n\t\tif (pass === undefined) {\n\t\t\terrors.push(`Ruling '${ruling.id}' references missing pass '${ruling.pass}'`)\n\t\t\tcontinue\n\t\t}\n\t\tif (pass.reasoning !== 'logical') {\n\t\t\terrors.push(`Ruling '${ruling.id}' references non-logical pass '${ruling.pass}'`)\n\t\t\tcontinue\n\t\t}\n\t\tif (findRule(pass, ruling.rule) === undefined) {\n\t\t\terrors.push(\n\t\t\t\t`Ruling '${ruling.id}' references missing rule '${ruling.rule}' in pass '${ruling.pass}'`,\n\t\t\t)\n\t\t}\n\t}\n\treturn errors\n}\n\n/**\n * Determine whether a subject already owns the reserved {@link QUALIFICATION_KEY}.\n *\n * @param subject - The subject to check\n * @returns `true` when the subject owns the reserved key\n *\n * @example\n * ```ts\n * import { hasReservedKey } from '@orkestrel/qualifier'\n *\n * hasReservedKey({ id: 's1', qualification: {} }) // true\n * ```\n */\nexport function hasReservedKey(subject: Subject): boolean {\n\treturn Object.hasOwn(subject, QUALIFICATION_KEY)\n}\n\n/**\n * Assert a value is a valid qualification {@link Subject}, narrowing it in place.\n *\n * @param value - The candidate subject to validate\n * @throws {@link QualifierError} `'MISMATCH'` when the value is not a record, or\n * when it already carries the reserved `qualification` key\n *\n * @example\n * ```ts\n * import { assertSubject } from '@orkestrel/qualifier'\n *\n * assertSubject({ id: 's1' }) // narrows to Subject\n * ```\n */\nexport function assertSubject(value: unknown): asserts value is Subject {\n\tif (!isRecord(value)) {\n\t\tthrow new QualifierError('MISMATCH', 'Qualification subject must be a record')\n\t}\n\tif (hasReservedKey(value)) {\n\t\tthrow new QualifierError(\n\t\t\t'MISMATCH',\n\t\t\t`Qualification subject must not contain reserved key '${QUALIFICATION_KEY}'`,\n\t\t)\n\t}\n}\n\n/**\n * Map an engine throw caught while running one pass to a typed {@link QualifierError}.\n *\n * @remarks\n * A reason `ReasonError('INVALID')` maps to `DEFINITION` (the engine rejected the\n * pass as an invalid definition); `ReasonError('DESTROYED')` maps to `DESTROYED`;\n * every other `ReasonError` code (e.g. `MISSING`) and every non-`ReasonError` throw\n * (including a `bail: true` reasoner rethrow) maps to `ENGINE`. The original throw\n * is preserved as `context.cause`.\n *\n * @param error - The value caught from the engine's `reason` call\n * @param pass - The id of the pass that was running\n * @returns A fresh, typed {@link QualifierError}\n *\n * @example\n * ```ts\n * import { mapEngineError } from '@orkestrel/qualifier'\n * import { ReasonError } from '@orkestrel/reason'\n *\n * mapEngineError(new ReasonError('MISSING', 'No reasoner registered'), 'cap').code // 'ENGINE'\n * ```\n */\nexport function mapEngineError(error: unknown, pass: string): QualifierError {\n\tif (isReasonError(error)) {\n\t\tif (error.code === 'INVALID') {\n\t\t\treturn new QualifierError(\n\t\t\t\t'DEFINITION',\n\t\t\t\t`Pass '${pass}' failed engine validation: ${error.message}`,\n\t\t\t\t{ pass, cause: error },\n\t\t\t)\n\t\t}\n\t\tif (error.code === 'DESTROYED') {\n\t\t\treturn new QualifierError(\n\t\t\t\t'DESTROYED',\n\t\t\t\t`Qualifier engine was destroyed during pass '${pass}'`,\n\t\t\t\t{ pass, cause: error },\n\t\t\t)\n\t\t}\n\t\treturn new QualifierError('ENGINE', `Pass '${pass}' engine failure: ${error.message}`, {\n\t\t\tpass,\n\t\t\tcause: error,\n\t\t})\n\t}\n\tconst message = error instanceof Error ? error.message : String(error)\n\treturn new QualifierError('ENGINE', `Pass '${pass}' engine failure: ${message}`, {\n\t\tpass,\n\t\tcause: error,\n\t})\n}\n\n/**\n * Find logical passes carrying no rulings.\n *\n * @remarks\n * A logical pass with no matching ruling can never contribute a finding, so its\n * rules only ever influence later passes' reads — this is very likely an authoring\n * oversight.\n *\n * @param definition - The qualification definition to check\n * @returns A fresh list of warning messages\n *\n * @example\n * ```ts\n * import { findEmptyLogicalPasses, qualificationDefinition } from '@orkestrel/qualifier'\n * import { atom, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const definition = qualificationDefinition('standard', 'Standard', [gates])\n *\n * findEmptyLogicalPasses(definition) // [\"Logical pass 'gates' has no rulings\"]\n * ```\n */\nexport function findEmptyLogicalPasses(definition: QualificationDefinition): readonly string[] {\n\tconst warnings: string[] = []\n\tfor (const pass of definition.passes) {\n\t\tif (pass.reasoning !== 'logical') continue\n\t\tconst hasRuling = (definition.rulings ?? []).some((ruling) => ruling.pass === pass.id)\n\t\tif (!hasRuling) warnings.push(`Logical pass '${pass.id}' has no rulings`)\n\t}\n\treturn warnings\n}\n\n/**\n * Find quantitative passes never read by a later pass.\n *\n * @remarks\n * A quantitative pass's projected value lives under `qualification.<id>` — if no\n * later logical premise or quantitative factor field reads that key, the\n * derivation is dead weight. Reads are collected per later pass: a logical pass's\n * premise atoms (conclusion atoms are writes, not reads), and a quantitative\n * pass's field/lookup/range factor sources plus factor checks.\n *\n * @param definition - The qualification definition to check\n * @returns A fresh list of warning messages\n *\n * @example\n * ```ts\n * import { findUnreadDerivations, qualificationDefinition } from '@orkestrel/qualifier'\n * import { factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'\n *\n * const cap = quantitativeDefinition('cap', 'TIV cap', [\n * factorGroup('limit', 'sum', [staticFactor('base', 500_000)]),\n * ])\n * const definition = qualificationDefinition('standard', 'Standard', [cap])\n *\n * findUnreadDerivations(definition) // [\"Quantitative pass 'cap' is never read by a later pass\"]\n * ```\n */\nexport function findUnreadDerivations(definition: QualificationDefinition): readonly string[] {\n\tconst warnings: string[] = []\n\tdefinition.passes.forEach((pass, index) => {\n\t\tif (pass.reasoning !== 'quantitative') return\n\t\tconst key = formatField(['qualification', pass.id])\n\t\tconst reads = new Set<string>()\n\t\tfor (const later of definition.passes.slice(index + 1)) {\n\t\t\tif (later.reasoning === 'logical') {\n\t\t\t\tfor (const rule of later.rules) {\n\t\t\t\t\tfor (const premise of rule.premises) {\n\t\t\t\t\t\tfor (const atom of extractAtoms(premise)) reads.add(formatField(atom.check.field))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (later.reasoning === 'quantitative') {\n\t\t\t\tfor (const group of later.groups) {\n\t\t\t\t\tfor (const factor of group.factors) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tfactor.source.origin === 'field' ||\n\t\t\t\t\t\t\tfactor.source.origin === 'lookup' ||\n\t\t\t\t\t\t\tfactor.source.origin === 'range'\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\treads.add(formatField(factor.source.field))\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfor (const check of factor.checks ?? []) reads.add(formatField(check.field))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!reads.has(key)) {\n\t\t\twarnings.push(`Quantitative pass '${pass.id}' is never read by a later pass`)\n\t\t}\n\t})\n\treturn warnings\n}\n\n/**\n * Build a {@link QualificationDefinition}.\n *\n * @remarks\n * Returns a fresh top-level definition, omitting absent optional keys;\n * `passes` and `rulings` arrays are copied, and record `metadata` is\n * shallow-copied so nested values are not deep-cloned.\n *\n * @param id - The definition id\n * @param name - The display name\n * @param passes - The ordered qualification passes\n * @param input - Optional description, rulings, and metadata\n * @returns A fresh qualification definition\n *\n * @example\n * ```ts\n * import { qualificationDefinition } from '@orkestrel/qualifier'\n *\n * qualificationDefinition('standard', 'Standard', [gates], { rulings: [ruling] })\n * ```\n */\nexport function qualificationDefinition(\n\tid: string,\n\tname: string,\n\tpasses: QualificationPass[],\n\tinput?: QualificationInput,\n): QualificationDefinition {\n\treturn {\n\t\tid,\n\t\tname,\n\t\tpasses: [...passes],\n\t\t...(input?.description === undefined ? {} : { description: input.description }),\n\t\t...(input?.rulings === undefined ? {} : { rulings: [...input.rulings] }),\n\t\t...(input?.metadata === undefined ? {} : { metadata: { ...input.metadata } }),\n\t}\n}\n\n/**\n * Build a {@link Ruling} — one authored consequence for one rule in one pass.\n *\n * @param id - The ruling id\n * @param pass - The logical pass id the rule lives in\n * @param rule - The rule id whose firing this ruling reacts to\n * @param effect - The eligibility impact when the rule fires\n * @param input - Optional selection scope and message template\n * @returns A fresh ruling\n *\n * @example\n * ```ts\n * import { rulingDefinition } from '@orkestrel/qualifier'\n *\n * rulingDefinition('license', 'gates', 'licensed', 'restriction', { message: 'A license is required' })\n * ```\n */\nexport function rulingDefinition(\n\tid: string,\n\tpass: string,\n\trule: string,\n\teffect: QualificationEffect,\n\tinput?: RulingInput,\n): Ruling {\n\treturn {\n\t\tid,\n\t\tpass,\n\t\trule,\n\t\teffect,\n\t\t...(input?.scope === undefined ? {} : { scope: input.scope }),\n\t\t...(input?.message === undefined ? {} : { message: input.message }),\n\t}\n}\n","import type {\n\tDerivation,\n\tFinding,\n\tQualificationContext,\n\tQualificationDefinition,\n\tQualificationResult,\n\tQualificationValidationResult,\n\tQualifierEventMap,\n\tQualifierInterface,\n\tQualifierOptions,\n} from './types.js'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { EvaluatorInterface, ReasonInterface, ReasonResult, Subject } from '@orkestrel/reason'\nimport { DEFAULT_QUALIFIER_VALIDATE } from './constants.js'\nimport { QualifierError } from './errors.js'\nimport {\n\tassertSubject,\n\tderiveFindingEligibility,\n\tderiveScopeEligibilities,\n\tfindEmptyLogicalPasses,\n\tfindMissingReferences,\n\tfindUnreadDerivations,\n\tmapEngineError,\n\tmergeQualificationContext,\n\tqualificationToRecord,\n\tquantitativeResultToDerivation,\n\treasonResultToProjection,\n\trulingToFinding,\n} from './helpers.js'\nimport { isQualificationPass, isRuling } from './validators.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport {\n\tcreateEvaluator,\n\tcreateLogicalReasoner,\n\tcreateQuantitativeReasoner,\n\tcreateReason,\n\tfindDuplicates,\n} from '@orkestrel/reason'\n\n/**\n * A qualifier — runs ordered passes over one reason engine and returns\n * eligibility.\n *\n * @remarks\n * The engine is OWNED when self-created (destroyed on `destroy()`) and borrowed\n * when injected (never destroyed). `qualify` builds its working subject through\n * copy-on-write overlays only — the caller's `subject` is never mutated, and the\n * qualifier's internal projection is discarded after each call.\n */\nexport class Qualifier implements QualifierInterface {\n\treadonly #emitter: Emitter<QualifierEventMap>\n\treadonly #engine: ReasonInterface\n\treadonly #evaluator: EvaluatorInterface\n\treadonly #owned: boolean\n\treadonly #validate: boolean\n\treadonly #labels: Readonly<Record<string, string>> | undefined\n\t#destroyed = false\n\n\tconstructor(options?: QualifierOptions) {\n\t\tthis.#emitter = new Emitter({\n\t\t\t...(options?.on === undefined ? {} : { on: options.on }),\n\t\t\t...(options?.error === undefined ? {} : { error: options.error }),\n\t\t})\n\t\tthis.#evaluator = createEvaluator()\n\t\tthis.#owned = options?.engine === undefined\n\t\tthis.#engine =\n\t\t\toptions?.engine ??\n\t\t\tcreateReason({\n\t\t\t\treasoners: [\n\t\t\t\t\tcreateQuantitativeReasoner({ evaluator: this.#evaluator }),\n\t\t\t\t\tcreateLogicalReasoner({ evaluator: this.#evaluator }),\n\t\t\t\t],\n\t\t\t\tbail: false,\n\t\t\t})\n\t\tthis.#validate = options?.validate ?? DEFAULT_QUALIFIER_VALIDATE\n\t\tthis.#labels = options?.labels\n\t}\n\n\tget emitter(): EmitterInterface<QualifierEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tqualify(subject: Subject, definition: QualificationDefinition): QualificationResult {\n\t\tthis.#alive()\n\t\tif (this.#validate) {\n\t\t\tconst validation = this.validate(definition)\n\t\t\tif (!validation.valid) {\n\t\t\t\tthrow new QualifierError(\n\t\t\t\t\t'DEFINITION',\n\t\t\t\t\t`Qualification definition is invalid: ${validation.errors.join(', ')}`,\n\t\t\t\t\tdefinition.id,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\treturn this.#qualify(subject, definition)\n\t}\n\n\tvalidate(definition: QualificationDefinition): QualificationValidationResult {\n\t\tthis.#alive()\n\t\tconst errors: string[] = []\n\t\tconst warnings: string[] = []\n\t\tif (definition.id.length === 0) errors.push('Definition id must not be empty')\n\t\tif (definition.name.length === 0) errors.push('Definition name must not be empty')\n\t\tdefinition.passes.forEach((pass, index) => {\n\t\t\tif (!isQualificationPass(pass)) {\n\t\t\t\terrors.push(`Pass at index ${index} is not a valid qualification pass`)\n\t\t\t}\n\t\t})\n\t\t;(definition.rulings ?? []).forEach((ruling, index) => {\n\t\t\tif (!isRuling(ruling)) errors.push(`Ruling at index ${index} is not a valid ruling`)\n\t\t})\n\t\tfor (const id of findDuplicates(definition.passes)) errors.push(`Duplicate pass id '${id}'`)\n\t\tfor (const id of findDuplicates(definition.rulings ?? [])) {\n\t\t\terrors.push(`Duplicate ruling id '${id}'`)\n\t\t}\n\t\tfor (const reference of findMissingReferences(definition)) errors.push(reference)\n\t\tif (definition.passes.length === 0) warnings.push('Definition has no passes')\n\t\tfor (const warning of findEmptyLogicalPasses(definition)) warnings.push(warning)\n\t\tfor (const warning of findUnreadDerivations(definition)) warnings.push(warning)\n\t\treturn { valid: errors.length === 0, errors, warnings }\n\t}\n\n\tdestroy(): void {\n\t\tif (this.#destroyed) return\n\t\tif (this.#owned) this.#engine.destroy()\n\t\tthis.#destroyed = true\n\t\tthis.#emitter.emit('destroy')\n\t\tthis.#emitter.destroy()\n\t}\n\n\t#qualify(subject: Subject, definition: QualificationDefinition): QualificationResult {\n\t\tassertSubject(subject)\n\t\tlet context: QualificationContext = {}\n\t\tlet working: Subject = { ...subject }\n\t\tconst findings: Finding[] = []\n\t\tconst derivations: Derivation[] = []\n\t\tconst trace: string[] = []\n\t\tconst errors: string[] = []\n\t\tlet failed = false\n\n\t\tfor (const pass of definition.passes) {\n\t\t\tthis.#alive()\n\t\t\tconst evaluated = working\n\t\t\tlet result: ReasonResult\n\t\t\ttry {\n\t\t\t\tresult = this.#engine.reason(evaluated, pass)\n\t\t\t} catch (error) {\n\t\t\t\tthrow mapEngineError(error, pass.id)\n\t\t\t}\n\n\t\t\ttrace.push(...result.trace.map((entry) => `${pass.id}: ${entry}`))\n\t\t\terrors.push(...result.errors.map((entry) => `${pass.id}: ${entry}`))\n\n\t\t\tif (result.reasoning === 'quantitative') {\n\t\t\t\tconst derivation = quantitativeResultToDerivation(pass.id, result)\n\t\t\t\tderivations.push(derivation)\n\t\t\t\tthis.#emitter.emit('derive', derivation)\n\t\t\t}\n\n\t\t\tif (result.reasoning === 'logical') {\n\t\t\t\tconst rulings = (definition.rulings ?? []).filter((ruling) => ruling.pass === pass.id)\n\t\t\t\tfor (const ruling of rulings) {\n\t\t\t\t\tconst finding = rulingToFinding(\n\t\t\t\t\t\truling,\n\t\t\t\t\t\tpass,\n\t\t\t\t\t\tresult,\n\t\t\t\t\t\tevaluated,\n\t\t\t\t\t\tthis.#evaluator,\n\t\t\t\t\t\tthis.#labels,\n\t\t\t\t\t)\n\t\t\t\t\tfindings.push(finding)\n\t\t\t\t\tthis.#emitter.emit('finding', finding)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst projection = reasonResultToProjection(pass, result)\n\t\t\tcontext = mergeQualificationContext(context, pass.id, projection)\n\t\t\tworking = { ...subject, ...qualificationToRecord(context) }\n\n\t\t\tif (!result.success) {\n\t\t\t\tfailed = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif (deriveFindingEligibility(findings) === 'ineligible') break\n\t\t}\n\n\t\tconst success = !failed && errors.length === 0\n\t\tconst scopes = deriveScopeEligibilities(findings)\n\t\tconst qualification: QualificationResult = Object.freeze({\n\t\t\tid: definition.id,\n\t\t\tname: definition.name,\n\t\t\teligibility: deriveFindingEligibility(findings, !success),\n\t\t\tscopes: Object.freeze(scopes),\n\t\t\tfindings: Object.freeze(findings),\n\t\t\tderivations: Object.freeze(derivations),\n\t\t\tsuccess,\n\t\t\ttrace: Object.freeze(trace),\n\t\t\terrors: Object.freeze(errors),\n\t\t})\n\t\tthis.#emitter.emit('qualify', qualification)\n\t\treturn qualification\n\t}\n\n\t#alive(): void {\n\t\tif (this.#destroyed) {\n\t\t\tthrow new QualifierError('DESTROYED', 'Qualifier has been destroyed')\n\t\t}\n\t}\n}\n","import type { QualifierInterface, QualifierOptions } from './types.js'\nimport { Qualifier } from './Qualifier.js'\n\n/**\n * Create one qualifier over a reason engine.\n *\n * @remarks\n * A standalone qualifier creates and OWNS one shared quantitative-plus-logical\n * reason engine, destroying it on `destroy()`. An injected `options.engine`\n * remains caller-owned and is never destroyed. Validation is on by default.\n *\n * @param options - Optional injected engine, validation, labels, and emitter hooks\n * @returns A {@link QualifierInterface}\n *\n * @example\n * ```ts\n * import { createQualifier } from '@orkestrel/qualifier'\n *\n * const qualifier = createQualifier()\n * qualifier.qualify({ id: 'risk-1' }, definition)\n * qualifier.destroy()\n * ```\n */\nexport function createQualifier(options?: QualifierOptions): QualifierInterface {\n\treturn new Qualifier(options)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#emitter","#engine","#evaluator","#owned","#validate","#labels","#alive","#qualify","#destroyed"],"sources":["../../../src/core/constants.ts","../../../src/core/errors.ts","../../../src/core/validators.ts","../../../src/core/helpers.ts","../../../src/core/Qualifier.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { Eligibility, QualificationEffect } from './types.js'\n\n/** Default definition validation policy for `createQualifier` / `Qualifier.qualify`. */\nexport const DEFAULT_QUALIFIER_VALIDATE = true\n\n/** The reserved internal projection namespace a pass's working projection is written under. */\nexport const QUALIFICATION_KEY = 'qualification'\n\n/** Eligibility severity order — most to least severe. */\nexport const ELIGIBILITY_PRECEDENCE: readonly Eligibility[] = Object.freeze([\n\t'ineligible',\n\t'referral',\n\t'eligible',\n])\n\n/** The eligibility impact of each {@link QualificationEffect}; `condition` remains eligible. */\nexport const EFFECT_ELIGIBILITIES: Readonly<Record<QualificationEffect, Eligibility>> =\n\tObject.freeze({\n\t\trestriction: 'ineligible',\n\t\treferral: 'referral',\n\t\tcondition: 'eligible',\n\t})\n","import type { QualifierErrorCode } from './types.js'\n\n/**\n * A coded programmer error thrown by the qualifier layer.\n *\n * @remarks\n * `DEFINITION` — a qualification definition failed semantic validation.\n * `MISMATCH` — a subject is not a record or already carries the reserved\n * `qualification` key. `DESTROYED` — use of a destroyed qualifier.\n * `ENGINE` — the underlying reason engine threw while running a pass (the\n * original throw is preserved as `context.cause`).\n */\nexport class QualifierError extends Error {\n\treadonly code: QualifierErrorCode\n\treadonly context: unknown\n\n\tconstructor(code: QualifierErrorCode, message: string, context?: unknown) {\n\t\tsuper(message)\n\t\tthis.name = 'QualifierError'\n\t\tthis.code = code\n\t\tthis.context = context\n\t}\n}\n\n/** Narrow a caught value to a {@link QualifierError}. */\nexport function isQualifierError(value: unknown): value is QualifierError {\n\treturn value instanceof QualifierError\n}\n","import type {\n\tDerivation,\n\tEligibility,\n\tFinding,\n\tPremise,\n\tQualificationDefinition,\n\tQualificationPass,\n\tQualificationResult,\n\tQualificationValidationResult,\n\tRuling,\n} from './types.js'\nimport type { Guard } from '@orkestrel/contract'\nimport {\n\tarrayOf,\n\tisBoolean,\n\tisNumber,\n\tisRecord,\n\tisString,\n\tliteralOf,\n\tobjectOf,\n\trecordOf,\n\twhereOf,\n} from '@orkestrel/contract'\nimport {\n\tisComparison,\n\tisFieldPath,\n\tisLogicalDefinition,\n\tisQuantitativeDefinition,\n\tisReasonValidationResult,\n} from '@orkestrel/reason'\n\n/** Determine whether a value is an {@link Eligibility} literal. */\nexport const isEligibility = literalOf('eligible', 'ineligible', 'referral')\n\n/** Determine whether a value is a {@link QualificationEffect} literal. */\nexport const isQualificationEffect = literalOf('restriction', 'referral', 'condition')\n\n/**\n * Determine whether a value is an open string-keyed record of {@link Eligibility} values.\n *\n * @remarks\n * Every own string-named property is checked, including non-enumerable properties. Inherited and\n * symbol-named members are left unchecked because they are outside the record this guard certifies.\n *\n * @param value - The value to test\n * @returns `true` when `value` is a non-array object whose own string-named values are eligibilities\n */\nexport function isEligibilityRecord(\n\tvalue: unknown,\n): value is Readonly<Record<string, Eligibility>> {\n\treturn whereOf(objectOf({}), (record): record is Readonly<Record<string, Eligibility>> =>\n\t\tObject.getOwnPropertyNames(record).every((key) => isEligibility(Reflect.get(record, key))),\n\t)(value)\n}\n\n/**\n * Determine whether a value is an open result-side {@link Premise}.\n *\n * @remarks\n * `expected` is unchecked and may be absent or contain any value because its published type is\n * `unknown`. `actual` is unchecked and may be absent or contain any value for the same reason.\n * Unknown members are admitted.\n *\n * @param value - The value to test\n * @returns `true` when every checked premise member follows its published type\n */\nexport function isPremise(value: unknown): value is Premise {\n\treturn objectOf(\n\t\t{\n\t\t\tfield: isFieldPath,\n\t\t\tlabel: isString,\n\t\t\tdescription: isString,\n\t\t\tcomparison: isComparison,\n\t\t\tmet: isBoolean,\n\t\t},\n\t\t['field', 'label', 'description', 'comparison', 'met'],\n\t)(value)\n}\n\n/**\n * Determine whether a value is an open result-side {@link Finding}.\n *\n * @remarks\n * Unknown members are admitted. Optional `scope` and `message` members may be absent or\n * `undefined`; a present defined value must be a string.\n *\n * @param value - The value to test\n * @returns `true` when every published finding member is valid\n */\nexport function isFinding(value: unknown): value is Finding {\n\treturn objectOf(\n\t\t{\n\t\t\tid: isString,\n\t\t\tpass: isString,\n\t\t\trule: isString,\n\t\t\teffect: isQualificationEffect,\n\t\t\tscope: isString,\n\t\t\tapplied: isBoolean,\n\t\t\tmessage: isString,\n\t\t\tpremises: arrayOf(isPremise),\n\t\t},\n\t\t['scope', 'message'],\n\t)(value)\n}\n\n/**\n * Determine whether a value is an open result-side {@link Derivation}.\n *\n * @remarks\n * Unknown members are admitted. `value` follows the published `number` type, including `NaN` and\n * infinities.\n *\n * @param value - The value to test\n * @returns `true` when every published derivation member is valid\n */\nexport function isDerivation(value: unknown): value is Derivation {\n\treturn objectOf({\n\t\tid: isString,\n\t\tvalue: isNumber,\n\t\tsuccess: isBoolean,\n\t\ttrace: arrayOf(isString),\n\t\terrors: arrayOf(isString),\n\t})(value)\n}\n\n/**\n * Determine whether a value is an open {@link QualificationResult} returned by a qualifier.\n *\n * @remarks\n * This guard is result-postured for values returned through a borrowed qualifier interface. It\n * admits unknown members and class instances while checking the complete published result closure.\n *\n * @param value - The value to test\n * @returns `true` when every published qualification-result member is valid\n */\nexport function isQualificationResult(value: unknown): value is QualificationResult {\n\treturn objectOf({\n\t\tid: isString,\n\t\tname: isString,\n\t\teligibility: isEligibility,\n\t\tscopes: isEligibilityRecord,\n\t\tfindings: arrayOf(isFinding),\n\t\tderivations: arrayOf(isDerivation),\n\t\tsuccess: isBoolean,\n\t\ttrace: arrayOf(isString),\n\t\terrors: arrayOf(isString),\n\t})(value)\n}\n\n/**\n * Determine whether a value is an open {@link QualificationValidationResult} returned by a qualifier.\n *\n * @remarks\n * `QualificationValidationResult` is an alias of reasons' validation-result type, so this IS\n * reasons' published open result guard by delegation — the alias tracks reasons' shape\n * automatically, and a hand-written member list here would silently stop checking a member a\n * later reason release adds.\n *\n * @param value - The value to test\n * @returns `true` when every published qualification-validation-result member is valid\n */\nexport const isQualificationValidationResult: Guard<QualificationValidationResult> =\n\tisReasonValidationResult\n\n/** Determine whether a value is a {@link QualificationPass} (a quantitative or logical definition). */\nexport function isQualificationPass(value: unknown): value is QualificationPass {\n\treturn isQuantitativeDefinition(value) || isLogicalDefinition(value)\n}\n\n/** Determine whether a value is an exact {@link Ruling} record. */\nexport function isRuling(value: unknown): value is Ruling {\n\treturn recordOf(\n\t\t{\n\t\t\tid: isString,\n\t\t\tpass: isString,\n\t\t\trule: isString,\n\t\t\teffect: isQualificationEffect,\n\t\t\tscope: isString,\n\t\t\tmessage: isString,\n\t\t},\n\t\t['scope', 'message'],\n\t)(value)\n}\n\n/** Determine whether a value is an exact {@link QualificationDefinition} record. */\nexport function isQualificationDefinition(value: unknown): value is QualificationDefinition {\n\treturn recordOf(\n\t\t{\n\t\t\tid: isString,\n\t\t\tname: isString,\n\t\t\tdescription: isString,\n\t\t\tpasses: arrayOf(isQualificationPass),\n\t\t\trulings: arrayOf(isRuling),\n\t\t\tmetadata: isRecord,\n\t\t},\n\t\t['description', 'rulings', 'metadata'],\n\t)(value)\n}\n","import type {\n\tDerivation,\n\tEligibility,\n\tFinding,\n\tPremise,\n\tQualificationContext,\n\tQualificationDefinition,\n\tQualificationEffect,\n\tQualificationInput,\n\tQualificationPass,\n\tQualificationProjection,\n\tRuling,\n\tRulingInput,\n} from './types.js'\nimport type {\n\tCheck,\n\tCheckResult,\n\tEvaluatorInterface,\n\tLogicalDefinition,\n\tLogicalResult,\n\tQuantitativeResult,\n\tReasonResult,\n\tRule,\n\tSubject,\n} from '@orkestrel/reason'\nimport { EFFECT_ELIGIBILITIES, ELIGIBILITY_PRECEDENCE, QUALIFICATION_KEY } from './constants.js'\nimport { QualifierError } from './errors.js'\nimport { isFiniteNumber, isRecord, resolveField } from '@orkestrel/contract'\nimport {\n\textractAtoms,\n\textractConclusions,\n\tformatField,\n\tisBounds,\n\tisReasonError,\n} from '@orkestrel/reason'\n\n/**\n * Interpolate `{{dotted.path}}` tokens in a message template against a subject.\n *\n * @remarks\n * Each token is split on `.` into a `FieldPath` array and resolved with\n * `resolveField` (a plain string field is ONE key, never dot-split — the split\n * here is the token-to-path bridge). A finite number renders with `en-US`\n * thousand grouping (`5010` → `5,010`); any other resolved value String-coerces.\n * An UNRESOLVED path renders as the empty string.\n *\n * @param template - The message template carrying `{{dotted.path}}` tokens\n * @param subject - The record tokens resolve against\n * @returns The template with every token replaced\n *\n * @example\n * ```ts\n * import { interpolateMessage } from '@orkestrel/qualifier'\n *\n * interpolateMessage('Limit is {{limit}}', { limit: 5010 }) // 'Limit is 5,010'\n * interpolateMessage('Missing {{gone}}', {}) // 'Missing '\n * ```\n */\nexport function interpolateMessage(\n\ttemplate: string,\n\tsubject: Readonly<Record<string, unknown>>,\n): string {\n\treturn template.replace(/\\{\\{\\s*([^}]+?)\\s*\\}\\}/g, (_match, path: string) => {\n\t\tconst value = resolveField(subject, path.split('.'))\n\t\tif (value === undefined) return ''\n\t\tif (isFiniteNumber(value)) return value.toLocaleString('en-US')\n\t\treturn String(value)\n\t})\n}\n\n/**\n * Describe a {@link Premise} comparison as a display-neutral verb phrase.\n *\n * @param comparison - The comparison to describe\n * @returns A display-neutral phrase\n *\n * @example\n * ```ts\n * import { describeComparison } from '@orkestrel/qualifier'\n *\n * describeComparison('above') // 'is more than'\n * ```\n */\nexport function describeComparison(comparison: NonNullable<Premise['comparison']>): string {\n\tswitch (comparison) {\n\t\tcase 'equals':\n\t\t\treturn 'is'\n\t\tcase 'not':\n\t\t\treturn 'is not'\n\t\tcase 'above':\n\t\t\treturn 'is more than'\n\t\tcase 'below':\n\t\t\treturn 'is less than'\n\t\tcase 'from':\n\t\t\treturn 'is at least'\n\t\tcase 'to':\n\t\t\treturn 'is at most'\n\t\tcase 'any':\n\t\t\treturn 'is any of'\n\t\tcase 'none':\n\t\t\treturn 'is none of'\n\t\tcase 'between':\n\t\t\treturn 'is between'\n\t\tcase 'outside':\n\t\t\treturn 'is outside'\n\t}\n}\n\n/**\n * Render a structured or scalar expected value display-neutrally.\n *\n * @remarks\n * An array renders as its elements, each `String`-coerced, joined with `', '`.\n * A reason `Bounds`-shaped record (the `between` / `outside` expected value)\n * renders its present sides joined with the word `and`; an absent side is\n * omitted. A finite number and everything else fall back to plain `String()` —\n * a finite number is NOT grouped here (grouping is {@link interpolateMessage}'s\n * message-token-only behavior).\n *\n * @param value - The value to render\n * @returns A display-neutral rendering\n *\n * @example\n * ```ts\n * import { describeValue } from '@orkestrel/qualifier'\n *\n * describeValue([18, 25, 40]) // '18, 25, 40'\n * describeValue({ minimum: 18, maximum: 65 }) // '18 and 65'\n * describeValue(42) // '42'\n * ```\n */\nexport function describeValue(value: unknown): string {\n\tif (Array.isArray(value)) return value.map((entry) => String(entry)).join(', ')\n\tif (isBounds(value)) {\n\t\tconst sides: string[] = []\n\t\tif (value.minimum !== undefined) sides.push(String(value.minimum))\n\t\tif (value.maximum !== undefined) sides.push(String(value.maximum))\n\t\treturn sides.join(' and ')\n\t}\n\treturn String(value)\n}\n\n/**\n * Render one {@link Premise} into a display-neutral sentence.\n *\n * @param entry - The premise to render\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A display-neutral sentence\n *\n * @example\n * ```ts\n * import { describePremise } from '@orkestrel/qualifier'\n *\n * describePremise({ field: 'age', comparison: 'above', expected: 18, actual: 25, met: true })\n * // 'age is more than 18 → met'\n * ```\n */\nexport function describePremise(entry: Premise, labels?: Readonly<Record<string, string>>): string {\n\tconst status = entry.met === undefined ? 'unknown' : entry.met ? 'met' : 'not met'\n\tif (entry.field === undefined || entry.comparison === undefined) {\n\t\treturn `${entry.description ?? 'Premise'} → ${status}`\n\t}\n\tconst field = formatField(entry.field)\n\tconst label = labels?.[field] ?? entry.label ?? field\n\tconst expected = entry.expected === undefined ? '' : ` ${describeValue(entry.expected)}`\n\treturn `${label} ${describeComparison(entry.comparison)}${expected} → ${status}`\n}\n\n/**\n * Build a {@link Premise} from an authored {@link Check} and its evaluated\n * `CheckResult`.\n *\n * @param check - The authored check\n * @param result - The evaluated check result carrying the resolved `actual` and `met`\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A fresh premise\n *\n * @example\n * ```ts\n * import { premiseCheck } from '@orkestrel/qualifier'\n * import { check } from '@orkestrel/reason'\n *\n * const authored = check('age', 'above', 18)\n * premiseCheck(authored, { field: 'age', actual: 25, met: true })\n * // { field: 'age', comparison: 'above', expected: 18, actual: 25, met: true }\n * ```\n */\nexport function premiseCheck(\n\tcheck: Check,\n\tresult: CheckResult,\n\tlabels?: Readonly<Record<string, string>>,\n): Premise {\n\tconst field = formatField(check.field)\n\treturn {\n\t\tfield: check.field,\n\t\t...(labels?.[field] === undefined ? {} : { label: labels[field] }),\n\t\tcomparison: check.operator,\n\t\texpected: check.value,\n\t\tactual: result.actual,\n\t\tmet: result.met,\n\t}\n}\n\n/**\n * Build rich premises for one fired {@link Rule} by walking its premise atoms\n * and re-evaluating each against the working subject.\n *\n * @remarks\n * A reason rule result carries only booleans, so this is the qualifier's own\n * premise-audit projection: each authored premise expression is flattened to its\n * atom leaves via `extractAtoms`, and each leaf's `Check` is re-evaluated through\n * the injected `evaluator`. A membership check (`any` / `none`) over an EMPTY\n * array value is content-free and is skipped.\n *\n * @param rule - The authored rule\n * @param working - The working subject to evaluate against\n * @param evaluator - The shared reason check evaluator\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A fresh, ordered list of rich premises\n *\n * @example\n * ```ts\n * import { logicalPremises } from '@orkestrel/qualifier'\n * import { atom, createEvaluator, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const evaluator = createEvaluator()\n * const premises = logicalPremises(gates.rules[0], { licensed: false }, evaluator)\n *\n * premises[0]?.met // true\n * ```\n */\nexport function logicalPremises(\n\trule: Rule,\n\tworking: Subject,\n\tevaluator: EvaluatorInterface,\n\tlabels?: Readonly<Record<string, string>>,\n): readonly Premise[] {\n\tconst output: Premise[] = []\n\tfor (const premise of rule.premises) {\n\t\tfor (const atom of extractAtoms(premise)) {\n\t\t\tconst { check } = atom\n\t\t\tif (\n\t\t\t\t(check.operator === 'any' || check.operator === 'none') &&\n\t\t\t\tArray.isArray(check.value) &&\n\t\t\t\tcheck.value.length === 0\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\toutput.push(premiseCheck(check, evaluator.evaluate(check, working), labels))\n\t\t}\n\t}\n\treturn output\n}\n\n/**\n * Locate an authored {@link Rule} by id.\n *\n * @param definition - The logical definition to search\n * @param id - The rule id\n * @returns The matching rule, or `undefined`\n *\n * @example\n * ```ts\n * import { findRule } from '@orkestrel/qualifier'\n * import { atom, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n *\n * findRule(gates, 'licensed')?.id // 'licensed'\n * ```\n */\nexport function findRule(definition: LogicalDefinition, id: string): Rule | undefined {\n\treturn definition.rules.find((rule) => rule.id === id)\n}\n\n/**\n * Project one reason result into the internal qualification namespace.\n *\n * @remarks\n * A quantitative result contributes its numeric `value`. A logical result\n * contributes a record of its `conclusion` plus every applied rule's asserted\n * conclusion fields (`extractConclusions`). A mismatched pairing contributes\n * `false`. The projection is pure — it never mutates the subject or the result.\n *\n * @param definition - The pass definition that produced the result\n * @param result - The evaluated reason result\n * @returns The pass's working projection\n *\n * @example\n * ```ts\n * import { reasonResultToProjection } from '@orkestrel/qualifier'\n * import { factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'\n *\n * const cap = quantitativeDefinition('cap', 'TIV cap', [\n * factorGroup('limit', 'sum', [staticFactor('base', 500_000)]),\n * ])\n * const result = {\n * reasoning: 'quantitative' as const,\n * value: 500_000,\n * groups: [],\n * count: 1,\n * success: true,\n * trace: [],\n * errors: [],\n * }\n * reasonResultToProjection(cap, result) // 500000\n * ```\n */\nexport function reasonResultToProjection(\n\tdefinition: QualificationPass,\n\tresult: ReasonResult,\n): QualificationProjection {\n\tif (definition.reasoning === 'quantitative' && result.reasoning === 'quantitative') {\n\t\treturn result.value\n\t}\n\tif (definition.reasoning !== 'logical' || result.reasoning !== 'logical') return false\n\tconst projection: Record<string, unknown> = { conclusion: result.conclusion }\n\tfor (const resolved of result.rules) {\n\t\tif (!resolved.applied) continue\n\t\tconst authored = definition.rules.find((rule) => rule.id === resolved.id)\n\t\tif (authored === undefined) continue\n\t\tconst conclusions = extractConclusions(authored.conclusion)\n\t\tfor (const [key, value] of Object.entries(conclusions)) projection[key] = value\n\t}\n\treturn projection\n}\n\n/**\n * Project a quantitative result into a {@link Derivation} audit record.\n *\n * @param id - The pass id\n * @param result - The evaluated quantitative result\n * @returns A fresh derivation\n *\n * @example\n * ```ts\n * import { quantitativeResultToDerivation } from '@orkestrel/qualifier'\n *\n * const result = { reasoning: 'quantitative' as const, value: 500_000, groups: [], count: 1, success: true, trace: ['cap: 500000'], errors: [] }\n * quantitativeResultToDerivation('cap', result)\n * // { id: 'cap', value: 500000, success: true, trace: ['cap: 500000'], errors: [] }\n * ```\n */\nexport function quantitativeResultToDerivation(id: string, result: QuantitativeResult): Derivation {\n\treturn {\n\t\tid,\n\t\tvalue: result.value,\n\t\tsuccess: result.success,\n\t\ttrace: [...result.trace],\n\t\terrors: [...result.errors],\n\t}\n}\n\n/**\n * Wrap a {@link QualificationContext} under {@link QUALIFICATION_KEY}.\n *\n * @param context - The accumulated projection context\n * @returns A record carrying the context under the reserved key\n *\n * @example\n * ```ts\n * import { qualificationToRecord } from '@orkestrel/qualifier'\n *\n * qualificationToRecord({ cap: 500_000 }) // { qualification: { cap: 500000 } }\n * ```\n */\nexport function qualificationToRecord(\n\tcontext: QualificationContext,\n): Readonly<Record<string, QualificationContext>> {\n\treturn { [QUALIFICATION_KEY]: context }\n}\n\n/**\n * Copy-on-write merge one pass projection into the context.\n *\n * @param context - The current projection context\n * @param id - The pass id to project under\n * @param projection - The pass's working projection\n * @returns A fresh context with the projection added\n *\n * @example\n * ```ts\n * import { mergeQualificationContext } from '@orkestrel/qualifier'\n *\n * mergeQualificationContext({ cap: 500_000 }, 'excess', 250_000)\n * // { cap: 500000, excess: 250000 }\n * ```\n */\nexport function mergeQualificationContext(\n\tcontext: QualificationContext,\n\tid: string,\n\tprojection: QualificationProjection,\n): QualificationContext {\n\treturn { ...context, [id]: projection }\n}\n\n/**\n * Join a ruling, its logical rule result, the pass, the pre-projection subject,\n * and an evaluator into a {@link Finding}.\n *\n * @remarks\n * `applied` reflects whether the ruling's rule fired. Premises are re-evaluated\n * against the SAME subject snapshot the pass evaluated, so a conclusion the pass\n * derived can never satisfy its own reported premise. The message interpolates\n * against that snapshot.\n *\n * @param ruling - The authored ruling\n * @param pass - The pass the ruling's rule lives in\n * @param result - The pass's evaluated logical result\n * @param subject - The pre-projection subject snapshot the pass evaluated\n * @param evaluator - The shared reason check evaluator\n * @param labels - Optional field-to-label overrides, keyed by dot-joined field\n * @returns A fresh finding\n *\n * @example\n * ```ts\n * import { rulingToFinding } from '@orkestrel/qualifier'\n * import { atom, createEvaluator, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const ruling = { id: 'license', pass: 'gates', rule: 'licensed', effect: 'restriction' as const }\n * const result = { reasoning: 'logical' as const, conclusion: true, rules: [{ id: 'licensed', applied: true, premises: [true], conclusion: true }], count: 1, success: true, trace: [], errors: [] }\n * const finding = rulingToFinding(ruling, gates, result, { licensed: false }, createEvaluator())\n *\n * finding.applied // true\n * ```\n */\nexport function rulingToFinding(\n\truling: Ruling,\n\tpass: QualificationPass,\n\tresult: LogicalResult,\n\tsubject: Subject,\n\tevaluator: EvaluatorInterface,\n\tlabels?: Readonly<Record<string, string>>,\n): Finding {\n\tconst entry = result.rules.find((item) => item.id === ruling.rule)\n\tconst applied = entry?.applied ?? false\n\tconst rule = pass.reasoning === 'logical' ? findRule(pass, ruling.rule) : undefined\n\tconst premises = rule === undefined ? [] : logicalPremises(rule, subject, evaluator, labels)\n\treturn {\n\t\tid: ruling.id,\n\t\tpass: ruling.pass,\n\t\trule: ruling.rule,\n\t\teffect: ruling.effect,\n\t\t...(ruling.scope === undefined ? {} : { scope: ruling.scope }),\n\t\tapplied,\n\t\t...(ruling.message === undefined\n\t\t\t? {}\n\t\t\t: { message: interpolateMessage(ruling.message, subject) }),\n\t\tpremises,\n\t}\n}\n\n/**\n * Derive global eligibility from applied, unscoped findings.\n *\n * @remarks\n * Only findings that both applied and carry no `scope` shape global eligibility.\n * A failed pass contributes a synthetic `referral` (fail-closed), passed through\n * `failed`.\n *\n * @param findings - The resolved findings\n * @param failed - Whether an operational pass failure occurred\n * @returns The most severe global eligibility\n *\n * @example\n * ```ts\n * import { deriveFindingEligibility } from '@orkestrel/qualifier'\n *\n * const finding = { id: 'license', pass: 'gates', rule: 'licensed', effect: 'restriction' as const, applied: true, premises: [] }\n * deriveFindingEligibility([finding]) // 'ineligible'\n * ```\n */\nexport function deriveFindingEligibility(findings: Finding[], failed = false): Eligibility {\n\tconst eligibilities = findings\n\t\t.filter((finding) => finding.applied && finding.scope === undefined)\n\t\t.map((finding) => EFFECT_ELIGIBILITIES[finding.effect])\n\tif (failed) eligibilities.push('referral')\n\treturn combineEligibilities(eligibilities)\n}\n\n/**\n * Return the most severe {@link Eligibility} in a list.\n *\n * @param eligibilities - The eligibilities to combine\n * @returns The most severe eligibility, or `'eligible'` for an empty list\n *\n * @example\n * ```ts\n * import { combineEligibilities } from '@orkestrel/qualifier'\n *\n * combineEligibilities(['eligible', 'referral']) // 'referral'\n * ```\n */\nexport function combineEligibilities(eligibilities: Eligibility[]): Eligibility {\n\tfor (const eligibility of ELIGIBILITY_PRECEDENCE) {\n\t\tif (eligibilities.includes(eligibility)) return eligibility\n\t}\n\treturn 'eligible'\n}\n\n/**\n * Derive one eligibility per finding scope.\n *\n * @remarks\n * Only applied, scoped findings contribute. Each named scope combines its own\n * findings' impacts by severity. An unmentioned scope is absent (eligible by\n * default at the program layer).\n *\n * @param findings - The resolved findings\n * @returns A fresh record of scope to its combined eligibility\n *\n * @example\n * ```ts\n * import { deriveScopeEligibilities } from '@orkestrel/qualifier'\n *\n * const finding = { id: 'coastal', pass: 'wind', rule: 'coastal', effect: 'restriction' as const, scope: 'wind', applied: true, premises: [] }\n * deriveScopeEligibilities([finding]) // { wind: 'ineligible' }\n * ```\n */\nexport function deriveScopeEligibilities(\n\tfindings: Finding[],\n): Readonly<Record<string, Eligibility>> {\n\tconst grouped: Record<string, Eligibility[]> = {}\n\tfor (const finding of findings) {\n\t\tif (!finding.applied || finding.scope === undefined) continue\n\t\tconst current = grouped[finding.scope] ?? []\n\t\tgrouped[finding.scope] = [...current, EFFECT_ELIGIBILITIES[finding.effect]]\n\t}\n\tconst scopes: Record<string, Eligibility> = {}\n\tfor (const [scope, eligibilities] of Object.entries(grouped)) {\n\t\tscopes[scope] = combineEligibilities(eligibilities)\n\t}\n\treturn scopes\n}\n\n/**\n * Find rulings whose pass or rule does not exist, whose pass is not logical, or\n * a pass id shadowing the reserved {@link QUALIFICATION_KEY}.\n *\n * @param definition - The qualification definition to check\n * @returns A fresh list of reference-error messages\n *\n * @example\n * ```ts\n * import { findMissingReferences, qualificationDefinition, rulingDefinition } from '@orkestrel/qualifier'\n * import { atom, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const definition = qualificationDefinition('standard', 'Standard', [gates], {\n * rulings: [rulingDefinition('license', 'gates', 'absent', 'restriction')],\n * })\n *\n * findMissingReferences(definition)\n * // [\"Ruling 'license' references missing rule 'absent' in pass 'gates'\"]\n * ```\n */\nexport function findMissingReferences(definition: QualificationDefinition): readonly string[] {\n\tconst errors: string[] = []\n\tconst passes = new Map(definition.passes.map((pass) => [pass.id, pass]))\n\tfor (const pass of definition.passes) {\n\t\tif (pass.id === QUALIFICATION_KEY) {\n\t\t\terrors.push(`Pass id must not equal reserved key '${QUALIFICATION_KEY}'`)\n\t\t}\n\t}\n\tfor (const ruling of definition.rulings ?? []) {\n\t\tconst pass = passes.get(ruling.pass)\n\t\tif (pass === undefined) {\n\t\t\terrors.push(`Ruling '${ruling.id}' references missing pass '${ruling.pass}'`)\n\t\t\tcontinue\n\t\t}\n\t\tif (pass.reasoning !== 'logical') {\n\t\t\terrors.push(`Ruling '${ruling.id}' references non-logical pass '${ruling.pass}'`)\n\t\t\tcontinue\n\t\t}\n\t\tif (findRule(pass, ruling.rule) === undefined) {\n\t\t\terrors.push(\n\t\t\t\t`Ruling '${ruling.id}' references missing rule '${ruling.rule}' in pass '${ruling.pass}'`,\n\t\t\t)\n\t\t}\n\t}\n\treturn errors\n}\n\n/**\n * Determine whether a subject already owns the reserved {@link QUALIFICATION_KEY}.\n *\n * @param subject - The subject to check\n * @returns `true` when the subject owns the reserved key\n *\n * @example\n * ```ts\n * import { hasReservedKey } from '@orkestrel/qualifier'\n *\n * hasReservedKey({ id: 's1', qualification: {} }) // true\n * ```\n */\nexport function hasReservedKey(subject: Subject): boolean {\n\treturn Object.hasOwn(subject, QUALIFICATION_KEY)\n}\n\n/**\n * Assert a value is a valid qualification {@link Subject}, narrowing it in place.\n *\n * @param value - The candidate subject to validate\n * @throws {@link QualifierError} `'MISMATCH'` when the value is not a record, or\n * when it already carries the reserved `qualification` key\n *\n * @example\n * ```ts\n * import { assertSubject } from '@orkestrel/qualifier'\n *\n * assertSubject({ id: 's1' }) // narrows to Subject\n * ```\n */\nexport function assertSubject(value: unknown): asserts value is Subject {\n\tif (!isRecord(value)) {\n\t\tthrow new QualifierError('MISMATCH', 'Qualification subject must be a record')\n\t}\n\tif (hasReservedKey(value)) {\n\t\tthrow new QualifierError(\n\t\t\t'MISMATCH',\n\t\t\t`Qualification subject must not contain reserved key '${QUALIFICATION_KEY}'`,\n\t\t)\n\t}\n}\n\n/**\n * Map an engine throw caught while running one pass to a typed {@link QualifierError}.\n *\n * @remarks\n * A reason `ReasonError('INVALID')` maps to `DEFINITION` (the engine rejected the\n * pass as an invalid definition); `ReasonError('DESTROYED')` maps to `DESTROYED`;\n * every other `ReasonError` code (e.g. `MISSING`) and every non-`ReasonError` throw\n * (including a `bail: true` reasoner rethrow) maps to `ENGINE`. The original throw\n * is preserved as `context.cause`.\n *\n * @param error - The value caught from the engine's `reason` call\n * @param pass - The id of the pass that was running\n * @returns A fresh, typed {@link QualifierError}\n *\n * @example\n * ```ts\n * import { mapEngineError } from '@orkestrel/qualifier'\n * import { ReasonError } from '@orkestrel/reason'\n *\n * mapEngineError(new ReasonError('MISSING', 'No reasoner registered'), 'cap').code // 'ENGINE'\n * ```\n */\nexport function mapEngineError(error: unknown, pass: string): QualifierError {\n\tif (isReasonError(error)) {\n\t\tif (error.code === 'INVALID') {\n\t\t\treturn new QualifierError(\n\t\t\t\t'DEFINITION',\n\t\t\t\t`Pass '${pass}' failed engine validation: ${error.message}`,\n\t\t\t\t{ pass, cause: error },\n\t\t\t)\n\t\t}\n\t\tif (error.code === 'DESTROYED') {\n\t\t\treturn new QualifierError(\n\t\t\t\t'DESTROYED',\n\t\t\t\t`Qualifier engine was destroyed during pass '${pass}'`,\n\t\t\t\t{ pass, cause: error },\n\t\t\t)\n\t\t}\n\t\treturn new QualifierError('ENGINE', `Pass '${pass}' engine failure: ${error.message}`, {\n\t\t\tpass,\n\t\t\tcause: error,\n\t\t})\n\t}\n\tconst message = error instanceof Error ? error.message : String(error)\n\treturn new QualifierError('ENGINE', `Pass '${pass}' engine failure: ${message}`, {\n\t\tpass,\n\t\tcause: error,\n\t})\n}\n\n/**\n * Find logical passes carrying no rulings.\n *\n * @remarks\n * A logical pass with no matching ruling can never contribute a finding, so its\n * rules only ever influence later passes' reads — this is very likely an authoring\n * oversight.\n *\n * @param definition - The qualification definition to check\n * @returns A fresh list of warning messages\n *\n * @example\n * ```ts\n * import { findEmptyLogicalPasses, qualificationDefinition } from '@orkestrel/qualifier'\n * import { atom, logicalDefinition, rule } from '@orkestrel/reason'\n *\n * const gates = logicalDefinition('gates', 'Gates', [\n * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),\n * ])\n * const definition = qualificationDefinition('standard', 'Standard', [gates])\n *\n * findEmptyLogicalPasses(definition) // [\"Logical pass 'gates' has no rulings\"]\n * ```\n */\nexport function findEmptyLogicalPasses(definition: QualificationDefinition): readonly string[] {\n\tconst warnings: string[] = []\n\tfor (const pass of definition.passes) {\n\t\tif (pass.reasoning !== 'logical') continue\n\t\tconst hasRuling = (definition.rulings ?? []).some((ruling) => ruling.pass === pass.id)\n\t\tif (!hasRuling) warnings.push(`Logical pass '${pass.id}' has no rulings`)\n\t}\n\treturn warnings\n}\n\n/**\n * Find quantitative passes never read by a later pass.\n *\n * @remarks\n * A quantitative pass's projected value lives under `qualification.<id>` — if no\n * later logical premise or quantitative factor field reads that key, the\n * derivation is dead weight. Reads are collected per later pass: a logical pass's\n * premise atoms (conclusion atoms are writes, not reads), and a quantitative\n * pass's field/lookup/range factor sources plus factor checks.\n *\n * @param definition - The qualification definition to check\n * @returns A fresh list of warning messages\n *\n * @example\n * ```ts\n * import { findUnreadDerivations, qualificationDefinition } from '@orkestrel/qualifier'\n * import { factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'\n *\n * const cap = quantitativeDefinition('cap', 'TIV cap', [\n * factorGroup('limit', 'sum', [staticFactor('base', 500_000)]),\n * ])\n * const definition = qualificationDefinition('standard', 'Standard', [cap])\n *\n * findUnreadDerivations(definition) // [\"Quantitative pass 'cap' is never read by a later pass\"]\n * ```\n */\nexport function findUnreadDerivations(definition: QualificationDefinition): readonly string[] {\n\tconst warnings: string[] = []\n\tdefinition.passes.forEach((pass, index) => {\n\t\tif (pass.reasoning !== 'quantitative') return\n\t\tconst key = formatField(['qualification', pass.id])\n\t\tconst reads = new Set<string>()\n\t\tfor (const later of definition.passes.slice(index + 1)) {\n\t\t\tif (later.reasoning === 'logical') {\n\t\t\t\tfor (const rule of later.rules) {\n\t\t\t\t\tfor (const premise of rule.premises) {\n\t\t\t\t\t\tfor (const atom of extractAtoms(premise)) reads.add(formatField(atom.check.field))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (later.reasoning === 'quantitative') {\n\t\t\t\tfor (const group of later.groups) {\n\t\t\t\t\tfor (const factor of group.factors) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tfactor.source.origin === 'field' ||\n\t\t\t\t\t\t\tfactor.source.origin === 'lookup' ||\n\t\t\t\t\t\t\tfactor.source.origin === 'range'\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\treads.add(formatField(factor.source.field))\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfor (const check of factor.checks ?? []) reads.add(formatField(check.field))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!reads.has(key)) {\n\t\t\twarnings.push(`Quantitative pass '${pass.id}' is never read by a later pass`)\n\t\t}\n\t})\n\treturn warnings\n}\n\n/**\n * Build a {@link QualificationDefinition}.\n *\n * @remarks\n * Returns a fresh top-level definition, omitting absent optional keys;\n * `passes` and `rulings` arrays are copied, and record `metadata` is\n * shallow-copied so nested values are not deep-cloned.\n *\n * @param id - The definition id\n * @param name - The display name\n * @param passes - The ordered qualification passes\n * @param input - Optional description, rulings, and metadata\n * @returns A fresh qualification definition\n *\n * @example\n * ```ts\n * import { qualificationDefinition } from '@orkestrel/qualifier'\n *\n * qualificationDefinition('standard', 'Standard', [gates], { rulings: [ruling] })\n * ```\n */\nexport function qualificationDefinition(\n\tid: string,\n\tname: string,\n\tpasses: QualificationPass[],\n\tinput?: QualificationInput,\n): QualificationDefinition {\n\treturn {\n\t\tid,\n\t\tname,\n\t\tpasses: [...passes],\n\t\t...(input?.description === undefined ? {} : { description: input.description }),\n\t\t...(input?.rulings === undefined ? {} : { rulings: [...input.rulings] }),\n\t\t...(input?.metadata === undefined ? {} : { metadata: { ...input.metadata } }),\n\t}\n}\n\n/**\n * Build a {@link Ruling} — one authored consequence for one rule in one pass.\n *\n * @param id - The ruling id\n * @param pass - The logical pass id the rule lives in\n * @param rule - The rule id whose firing this ruling reacts to\n * @param effect - The eligibility impact when the rule fires\n * @param input - Optional selection scope and message template\n * @returns A fresh ruling\n *\n * @example\n * ```ts\n * import { rulingDefinition } from '@orkestrel/qualifier'\n *\n * rulingDefinition('license', 'gates', 'licensed', 'restriction', { message: 'A license is required' })\n * ```\n */\nexport function rulingDefinition(\n\tid: string,\n\tpass: string,\n\trule: string,\n\teffect: QualificationEffect,\n\tinput?: RulingInput,\n): Ruling {\n\treturn {\n\t\tid,\n\t\tpass,\n\t\trule,\n\t\teffect,\n\t\t...(input?.scope === undefined ? {} : { scope: input.scope }),\n\t\t...(input?.message === undefined ? {} : { message: input.message }),\n\t}\n}\n","import type {\n\tDerivation,\n\tFinding,\n\tQualificationContext,\n\tQualificationDefinition,\n\tQualificationResult,\n\tQualificationValidationResult,\n\tQualifierEventMap,\n\tQualifierInterface,\n\tQualifierOptions,\n} from './types.js'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { EvaluatorInterface, ReasonInterface, ReasonResult, Subject } from '@orkestrel/reason'\nimport { DEFAULT_QUALIFIER_VALIDATE } from './constants.js'\nimport { QualifierError } from './errors.js'\nimport {\n\tassertSubject,\n\tderiveFindingEligibility,\n\tderiveScopeEligibilities,\n\tfindEmptyLogicalPasses,\n\tfindMissingReferences,\n\tfindUnreadDerivations,\n\tmapEngineError,\n\tmergeQualificationContext,\n\tqualificationToRecord,\n\tquantitativeResultToDerivation,\n\treasonResultToProjection,\n\trulingToFinding,\n} from './helpers.js'\nimport { isQualificationPass, isRuling } from './validators.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport {\n\tcreateEvaluator,\n\tcreateLogicalReasoner,\n\tcreateQuantitativeReasoner,\n\tcreateReason,\n\tfindDuplicates,\n} from '@orkestrel/reason'\n\n/**\n * A qualifier — runs ordered passes over one reason engine and returns\n * eligibility.\n *\n * @remarks\n * The engine is OWNED when self-created (destroyed on `destroy()`) and borrowed\n * when injected (never destroyed). `qualify` builds its working subject through\n * copy-on-write overlays only — the caller's `subject` is never mutated, and the\n * qualifier's internal projection is discarded after each call.\n */\nexport class Qualifier implements QualifierInterface {\n\treadonly #emitter: Emitter<QualifierEventMap>\n\treadonly #engine: ReasonInterface\n\treadonly #evaluator: EvaluatorInterface\n\treadonly #owned: boolean\n\treadonly #validate: boolean\n\treadonly #labels: Readonly<Record<string, string>> | undefined\n\t#destroyed = false\n\n\tconstructor(options?: QualifierOptions) {\n\t\tthis.#emitter = new Emitter({\n\t\t\t...(options?.on === undefined ? {} : { on: options.on }),\n\t\t\t...(options?.error === undefined ? {} : { error: options.error }),\n\t\t})\n\t\tthis.#evaluator = createEvaluator()\n\t\tthis.#owned = options?.engine === undefined\n\t\tthis.#engine =\n\t\t\toptions?.engine ??\n\t\t\tcreateReason({\n\t\t\t\treasoners: [\n\t\t\t\t\tcreateQuantitativeReasoner({ evaluator: this.#evaluator }),\n\t\t\t\t\tcreateLogicalReasoner({ evaluator: this.#evaluator }),\n\t\t\t\t],\n\t\t\t\tbail: false,\n\t\t\t})\n\t\tthis.#validate = options?.validate ?? DEFAULT_QUALIFIER_VALIDATE\n\t\tthis.#labels = options?.labels\n\t}\n\n\tget emitter(): EmitterInterface<QualifierEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tqualify(subject: Subject, definition: QualificationDefinition): QualificationResult {\n\t\tthis.#alive()\n\t\tif (this.#validate) {\n\t\t\tconst validation = this.validate(definition)\n\t\t\tif (!validation.valid) {\n\t\t\t\tthrow new QualifierError(\n\t\t\t\t\t'DEFINITION',\n\t\t\t\t\t`Qualification definition is invalid: ${validation.errors.join(', ')}`,\n\t\t\t\t\tdefinition.id,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\treturn this.#qualify(subject, definition)\n\t}\n\n\tvalidate(definition: QualificationDefinition): QualificationValidationResult {\n\t\tthis.#alive()\n\t\tconst errors: string[] = []\n\t\tconst warnings: string[] = []\n\t\tif (definition.id.length === 0) errors.push('Definition id must not be empty')\n\t\tif (definition.name.length === 0) errors.push('Definition name must not be empty')\n\t\tdefinition.passes.forEach((pass, index) => {\n\t\t\tif (!isQualificationPass(pass)) {\n\t\t\t\terrors.push(`Pass at index ${index} is not a valid qualification pass`)\n\t\t\t}\n\t\t})\n\t\t;(definition.rulings ?? []).forEach((ruling, index) => {\n\t\t\tif (!isRuling(ruling)) errors.push(`Ruling at index ${index} is not a valid ruling`)\n\t\t})\n\t\tfor (const id of findDuplicates(definition.passes)) errors.push(`Duplicate pass id '${id}'`)\n\t\tfor (const id of findDuplicates(definition.rulings ?? [])) {\n\t\t\terrors.push(`Duplicate ruling id '${id}'`)\n\t\t}\n\t\tfor (const reference of findMissingReferences(definition)) errors.push(reference)\n\t\tif (definition.passes.length === 0) warnings.push('Definition has no passes')\n\t\tfor (const warning of findEmptyLogicalPasses(definition)) warnings.push(warning)\n\t\tfor (const warning of findUnreadDerivations(definition)) warnings.push(warning)\n\t\treturn { valid: errors.length === 0, errors, warnings }\n\t}\n\n\tdestroy(): void {\n\t\tif (this.#destroyed) return\n\t\tif (this.#owned) this.#engine.destroy()\n\t\tthis.#destroyed = true\n\t\tthis.#emitter.emit('destroy')\n\t\tthis.#emitter.destroy()\n\t}\n\n\t#qualify(subject: Subject, definition: QualificationDefinition): QualificationResult {\n\t\tassertSubject(subject)\n\t\tlet context: QualificationContext = {}\n\t\tlet working: Subject = { ...subject }\n\t\tconst findings: Finding[] = []\n\t\tconst derivations: Derivation[] = []\n\t\tconst trace: string[] = []\n\t\tconst errors: string[] = []\n\t\tlet failed = false\n\n\t\tfor (const pass of definition.passes) {\n\t\t\tthis.#alive()\n\t\t\tconst evaluated = working\n\t\t\tlet result: ReasonResult\n\t\t\ttry {\n\t\t\t\tresult = this.#engine.reason(evaluated, pass)\n\t\t\t} catch (error) {\n\t\t\t\tthrow mapEngineError(error, pass.id)\n\t\t\t}\n\n\t\t\ttrace.push(...result.trace.map((entry) => `${pass.id}: ${entry}`))\n\t\t\terrors.push(...result.errors.map((entry) => `${pass.id}: ${entry}`))\n\n\t\t\tif (result.reasoning === 'quantitative') {\n\t\t\t\tconst derivation = quantitativeResultToDerivation(pass.id, result)\n\t\t\t\tderivations.push(derivation)\n\t\t\t\tthis.#emitter.emit('derive', derivation)\n\t\t\t}\n\n\t\t\tif (result.reasoning === 'logical') {\n\t\t\t\tconst rulings = (definition.rulings ?? []).filter((ruling) => ruling.pass === pass.id)\n\t\t\t\tfor (const ruling of rulings) {\n\t\t\t\t\tconst finding = rulingToFinding(\n\t\t\t\t\t\truling,\n\t\t\t\t\t\tpass,\n\t\t\t\t\t\tresult,\n\t\t\t\t\t\tevaluated,\n\t\t\t\t\t\tthis.#evaluator,\n\t\t\t\t\t\tthis.#labels,\n\t\t\t\t\t)\n\t\t\t\t\tfindings.push(finding)\n\t\t\t\t\tthis.#emitter.emit('finding', finding)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst projection = reasonResultToProjection(pass, result)\n\t\t\tcontext = mergeQualificationContext(context, pass.id, projection)\n\t\t\tworking = { ...subject, ...qualificationToRecord(context) }\n\n\t\t\tif (!result.success) {\n\t\t\t\tfailed = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif (deriveFindingEligibility(findings) === 'ineligible') break\n\t\t}\n\n\t\tconst success = !failed && errors.length === 0\n\t\tconst scopes = deriveScopeEligibilities(findings)\n\t\tconst qualification: QualificationResult = Object.freeze({\n\t\t\tid: definition.id,\n\t\t\tname: definition.name,\n\t\t\teligibility: deriveFindingEligibility(findings, !success),\n\t\t\tscopes: Object.freeze(scopes),\n\t\t\tfindings: Object.freeze(findings),\n\t\t\tderivations: Object.freeze(derivations),\n\t\t\tsuccess,\n\t\t\ttrace: Object.freeze(trace),\n\t\t\terrors: Object.freeze(errors),\n\t\t})\n\t\tthis.#emitter.emit('qualify', qualification)\n\t\treturn qualification\n\t}\n\n\t#alive(): void {\n\t\tif (this.#destroyed) {\n\t\t\tthrow new QualifierError('DESTROYED', 'Qualifier has been destroyed')\n\t\t}\n\t}\n}\n","import type { QualifierInterface, QualifierOptions } from './types.js'\nimport { Qualifier } from './Qualifier.js'\n\n/**\n * Create one qualifier over a reason engine.\n *\n * @remarks\n * A standalone qualifier creates and OWNS one shared quantitative-plus-logical\n * reason engine, destroying it on `destroy()`. An injected `options.engine`\n * remains caller-owned and is never destroyed. Validation is on by default.\n *\n * @param options - Optional injected engine, validation, labels, and emitter hooks\n * @returns A {@link QualifierInterface}\n *\n * @example\n * ```ts\n * import { createQualifier } from '@orkestrel/qualifier'\n *\n * const qualifier = createQualifier()\n * qualifier.qualify({ id: 'risk-1' }, definition)\n * qualifier.destroy()\n * ```\n */\nexport function createQualifier(options?: QualifierOptions): QualifierInterface {\n\treturn new Qualifier(options)\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orkestrel/qualifier",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "A typed eligibility engine: ordered quantitative and logical passes qualify a subject into program-level and scoped eligibility with evidence-rich findings. Part of the @orkestrel line.",
5
5
  "keywords": [
6
6
  "eligibility",
@@ -52,10 +52,11 @@
52
52
  "format": "oxfmt --config .oxfmtrc.json --write .",
53
53
  "format:check": "oxfmt --config .oxfmtrc.json --check .",
54
54
  "lint:check": "oxlint --config .oxlintrc.json --deny-warnings .",
55
- "test": "npm run test:src && npm run test:policy && npm run test:guides",
55
+ "test": "npm run test:src && npm run test:policy && npm run test:config && npm run test:guides",
56
56
  "test:src": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core",
57
57
  "test:src:core": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core",
58
58
  "test:policy": "vitest run --config vite.config.ts --no-cache --reporter=dot --project policy",
59
+ "test:config": "vitest run --config vite.config.ts --no-cache --reporter=dot --project config",
59
60
  "test:guides": "vitest run --config vite.config.ts --reporter=dot --project guides",
60
61
  "build": "npm run clean && npm run build:src",
61
62
  "build:src": "npm run build:src:core",
@@ -63,20 +64,21 @@
63
64
  "prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test"
64
65
  },
65
66
  "dependencies": {
66
- "@orkestrel/contract": "^0.0.11",
67
+ "@orkestrel/contract": "^0.0.12",
67
68
  "@orkestrel/emitter": "^0.0.6",
68
- "@orkestrel/reason": "^0.0.5"
69
+ "@orkestrel/reason": "^0.0.6"
69
70
  },
70
71
  "devDependencies": {
71
72
  "@microsoft/api-extractor": "^7.58.12",
72
- "@orkestrel/guide": "^0.0.9",
73
- "@orkestrel/scaffold": "^0.0.26",
74
- "@types/node": "^26.1.2",
73
+ "@orkestrel/guide": "^0.0.11",
74
+ "@orkestrel/scaffold": "^0.0.38",
75
+ "@orkestrel/test": "^0.0.6",
76
+ "@types/node": "^26.2.0",
75
77
  "@vitest/browser-playwright": "^4.1.10",
76
- "oxfmt": "^0.61.0",
77
- "oxlint": "^1.76.0",
78
+ "oxfmt": "^0.63.0",
79
+ "oxlint": "^1.78.0",
78
80
  "typescript": "^6.0.3",
79
- "vite": "^8.1.5",
81
+ "vite": "^8.2.1",
80
82
  "vite-plugin-dts": "^5.0.3",
81
83
  "vitest": "^4.1.10"
82
84
  },