@finsys/core 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -570,4 +570,632 @@ interface ExtractionStatusResult {
570
570
  */
571
571
  declare function resolveExtractionStatus(ihsRecord: Record<string, unknown>, jobRecords?: ExtractionJobRecord[]): ExtractionStatusResult;
572
572
 
573
- export { BASE_FIELD_SPECS, BasicFormField, type Category, type Choice, DEFAULT_VALIDATOR_DEFINITIONS, type DocExtractionResult, DocExtractionStatus, type DropdownOption, type EditorValidator, ExtractionFileType, type ExtractionJobRecord, ExtractionJobStatus, type ExtractionStatusResult, FIELD_TYPE_DEFINITIONS, type FieldData, type FieldGroup, type FieldReference, FieldType, type FileFieldTableData, type FileFieldTableItem, FileFieldTableType, FileFormField, FormField, FormFieldCategory, type FormFieldInputType, type FormFieldType, type FormFieldTypeDefinitions, FormFieldValidator, FormSpec, type FormValidatorDefinitions, IHS_FAILURE_STATUSES, IHS_TERMINAL_STATUSES, IHS_VALID_STATUSES, type IhsDetailCategory, type IhsFieldDetail, IhsStatus, IhsValueFormat, type PageConfig, type RHFSchemaOutput, type RHFStep, type ResolvedField, Role, type SurveyElementJSON, type SurveyJSON, type SurveyPageJSON, type UnifiedFormConfig, type Validator, applyDynamicTitles, buildFileFieldTables, evaluateExpression, extractTimePeriods, generateRHFSchema, generateSurveyJson, getBaseCategories, getBaseFieldNames, getBaseFieldSpecMap, getBaseFieldSpecs, getCategoryName, getDisplayName, getDisplayNames, getGroupDisplayNames, getPastMonthLabel, getPastYearLabel, getStepDefaultValues, getStepSchema, groupColumnsByTimePeriod, groupDetailsByCategory, groupFieldsByCategory, groupFieldsByPattern, isFailureIhsStatus, isTerminalIhsStatus, isValidIhsStatus, processIhsDetails, resolveExtractionStatus, resolvePageFields, validateFormConfig, validateFormSpec, validatePagesConfig };
573
+ /**
574
+ * Generic categories declared in this package. The set is union-typed
575
+ * so consumers get TypeScript autocomplete (`'telco-carrier' |
576
+ * 'payment-network' | ...`) and a compile-time error if they reference
577
+ * a category that doesn't exist.
578
+ *
579
+ * NB: adding a new category is a finsys-core minor version bump.
580
+ * Vendor adapters can ship freely (deployment-time, no core change)
581
+ * but the CATEGORY they implement must exist here.
582
+ */
583
+ type AdapterCategory = "telco-carrier" | "payment-network" | "bank-statement";
584
+ /**
585
+ * Per-field metadata for a canonical field declared by a category.
586
+ * Frozen at module load — the data file is authoritative.
587
+ */
588
+ interface CanonicalFieldSpec {
589
+ readonly name: CanonicalFieldName;
590
+ readonly type: "number" | "boolean" | "string";
591
+ readonly unit?: string;
592
+ readonly range?: readonly [number, number];
593
+ readonly description: string;
594
+ }
595
+ /**
596
+ * Per-category schema bundle. Used by:
597
+ * - host app: validate adapter `produces` lists at registration
598
+ * - clients (finhub, finsys-client): render UI conditionally on
599
+ * category activation
600
+ * - CRA report: render provenance metadata
601
+ */
602
+ interface CategorySchema {
603
+ readonly id: AdapterCategory;
604
+ readonly displayName: string;
605
+ readonly description: string;
606
+ readonly canonicalTable: string;
607
+ readonly fields: ReadonlyArray<CanonicalFieldSpec>;
608
+ }
609
+ /**
610
+ * Union of all canonical field names declared by any category in this
611
+ * package. Loose-typed at compile time (string) because the actual
612
+ * union grows with each category-added minor; runtime helpers below
613
+ * narrow against the data.
614
+ *
615
+ * Adapter `produces` lists are typed as ReadonlyArray<CanonicalFieldName>
616
+ * — the host validates membership against the adapter's category at
617
+ * registration time, refusing adapters that promise fields outside
618
+ * their category.
619
+ */
620
+ type CanonicalFieldName = string;
621
+ /**
622
+ * Look up a category schema by id. Throws if the id is unknown — this
623
+ * is a programmer error (the type system should prevent it).
624
+ */
625
+ declare function categorySchemaOf(id: AdapterCategory): CategorySchema;
626
+ /**
627
+ * The canonical field names a given category produces. Convenience
628
+ * for callers that don't need the full schema — common case in the
629
+ * eval engine ("does this policy reference any fields from a category
630
+ * that isn't loaded?").
631
+ */
632
+ declare function categoryFieldsOf(id: AdapterCategory): ReadonlyArray<CanonicalFieldName>;
633
+ /**
634
+ * Every category currently declared. Order is data-file order; treat
635
+ * as unstable across versions.
636
+ */
637
+ declare function allCategories(): ReadonlyArray<CategorySchema>;
638
+ /**
639
+ * Reverse lookup: which category declares a given canonical field?
640
+ * Returns null if the field name isn't declared by any category in
641
+ * this version of finsys-core. Useful when the host app is reading
642
+ * canonical field values back from storage + wants to identify the
643
+ * producing category for rendering.
644
+ */
645
+ declare function categoryForField(field: CanonicalFieldName): AdapterCategory | null;
646
+
647
+ /**
648
+ * Source Adapter contract — the framework for ingesting unstructured
649
+ * or partner-specific inputs and producing canonical field values that
650
+ * the eval engine + CRA report consume.
651
+ *
652
+ * The contract has no vendor-specific knowledge. Generic categories
653
+ * (telco-carrier, payment-network, document-extractor, social-presence,
654
+ * …) are declared here; specific vendor implementations live OUTSIDE
655
+ * this open-source package, in private extension directories loaded
656
+ * by the host app at runtime (see SYS-2444 plugin discovery).
657
+ *
658
+ * Design notes:
659
+ * - extract() is async — every real adapter does I/O (HTTP, OCR, LLM).
660
+ * - Errors are thrown as `AdapterError` instances. We DELIBERATELY
661
+ * don't use a Result<> type — the rest of finsys-core uses throw/
662
+ * catch, and bringing a Result idiom in for one subsystem creates
663
+ * dialect inconsistency.
664
+ * - The contract makes no statement about persistence. The host app's
665
+ * adapter runner catches the return value, persists canonical fields
666
+ * + raw payload to the storage tables (SYS-2441), and writes
667
+ * provenance to `ihs_extraction_runs`. Adapters are pure functions
668
+ * of (raw payload) → (canonical fields). This separation lets
669
+ * adapters be swapped, replayed, and tested without DB state.
670
+ */
671
+
672
+ /**
673
+ * An adapter receives a raw payload — opaque to the framework. The
674
+ * shape is whatever the vendor's source produces: a JSON object from a
675
+ * partner API, a base64 PDF blob, a web-scrape result. Vendor adapters
676
+ * narrow this type internally. The host app's adapter runner is
677
+ * responsible for putting the bytes/structure in front of the adapter;
678
+ * what those bytes are is the adapter's problem.
679
+ */
680
+ type RawPayload = unknown;
681
+ /**
682
+ * Canonical field values produced by an adapter for ONE instance.
683
+ * Keys MUST be drawn from the field set declared by the adapter's
684
+ * category (see `categoryFieldsOf` in adapter-categories.ts). Values
685
+ * are typed narrowly per field by the category schema; this loose TS
686
+ * type is runtime-validated against the category schema by the host
687
+ * app at registration time, AND at every extract() call result.
688
+ *
689
+ * The framework rejects:
690
+ * - Field names not declared by the adapter's category
691
+ * - Values whose runtime type doesn't match the category schema
692
+ *
693
+ * Adapters MAY return a partial set — not every category field has to
694
+ * be produced on every applicant (telco fields may be absent if the
695
+ * borrower didn't opt in, for instance). Missing fields are aggregated
696
+ * as zero / null in the eval engine; that's a feature, not a defect.
697
+ */
698
+ type CanonicalFieldValues = Partial<Record<CanonicalFieldName, CanonicalFieldValue>>;
699
+ /**
700
+ * One extraction "instance" — the framework supports multi-instance
701
+ * per applicant per adapter. Examples:
702
+ * - 6 bank statements from one bank (one adapter call, 6 instances)
703
+ * - 12 monthly payment-summary snapshots from one payment network
704
+ * - 3 mobile lines from one telco carrier
705
+ *
706
+ * For multi-VENDOR cases (3 different telco carriers), use separate
707
+ * adapter registrations — each vendor is its own SourceAdapter with
708
+ * its own id. For multi-INSTANCE-from-one-vendor (6 statements from
709
+ * one bank), the adapter returns multiple AdapterExtraction entries
710
+ * in a single extract() call.
711
+ *
712
+ * `instanceKey` is the within-adapter discriminator: free-form, but
713
+ * MUST be stable across re-extractions so the storage layer can
714
+ * replace-in-place rather than accumulating duplicates. Conventions:
715
+ * - For periodic data: encode the period (`'2026-Q1'`, `'2026-03'`)
716
+ * - For per-line data: encode the line id (`'msisdn-60123456789'`,
717
+ * `'card-last4-1234'`)
718
+ * - For natively single-instance adapters: use the empty string `''`
719
+ * (the framework treats `''` as "this adapter only ever has one
720
+ * instance per applicant").
721
+ */
722
+ interface AdapterExtraction {
723
+ /**
724
+ * Stable within-adapter instance discriminator. Empty string for
725
+ * adapters that only ever produce one instance per applicant.
726
+ */
727
+ readonly instanceKey: string;
728
+ /**
729
+ * The canonical field values for this instance.
730
+ */
731
+ readonly values: CanonicalFieldValues;
732
+ }
733
+ /**
734
+ * Allowed value shapes for canonical fields. Per-field type narrowing
735
+ * happens at the category schema level — telcoOnTimePaymentRatio24m is
736
+ * a number 0..1; telcoHandsetFinancingActive is a boolean; etc.
737
+ * Numbers + booleans + strings cover everything we expect from credit
738
+ * signals; nested objects are intentionally excluded to keep canonical
739
+ * fields atomic + queryable.
740
+ */
741
+ type CanonicalFieldValue = number | string | boolean | null;
742
+ /**
743
+ * The interface every adapter implements. Adapters are loaded by the
744
+ * host app's discovery mechanism (SYS-2444); the host app constructs
745
+ * the adapter from its manifest + extract module, then registers it
746
+ * against the registry by `id`.
747
+ *
748
+ * Type parameter `E` lets adapter authors declare the partner-specific
749
+ * extension shape on ApplicantIdentity. v2.7.0+ — defaults to `{}` so
750
+ * v2.6.0 adapters (which didn't have fetch and never read identity)
751
+ * stay assignment-compatible. A telco adapter that needs an MSISDN
752
+ * declares it once at the interface level:
753
+ *
754
+ * interface CelcomExt { msisdn: string }
755
+ * const celcomTelco: SourceAdapter<CelcomExt> = {
756
+ * async fetch(identity) {
757
+ * identity.msisdn // string (typed via E)
758
+ * identity.ic // string (core)
759
+ * },
760
+ * ...
761
+ * }
762
+ *
763
+ * The generic flows from interface → method, so implementations
764
+ * don't have to redeclare it on fetch.
765
+ */
766
+ interface SourceAdapter<E extends Record<string, unknown> = {}> {
767
+ /**
768
+ * Globally unique id for this adapter instance. By convention:
769
+ * `<vendor>-<category-short>-v<n>` — e.g. `celcom-telco-v1`. Vendor
770
+ * names appear here in deployment-specific code, NEVER in
771
+ * finsys-core. The id is what the registry uses to dedupe + what
772
+ * provenance records pin to.
773
+ */
774
+ readonly id: string;
775
+ /**
776
+ * Which generic category this adapter implements. Determines which
777
+ * canonical fields the adapter MAY produce + how its output is
778
+ * persisted (each category has its own canonical sibling table —
779
+ * `ihs_alt_data_telco`, `ihs_alt_data_payments`, etc.).
780
+ */
781
+ readonly category: AdapterCategory;
782
+ /**
783
+ * Monotonic version per adapter. Bumped whenever the adapter's
784
+ * payload shape, field-mapping logic, or output semantics change.
785
+ * Versions are stored alongside each extraction run for replay /
786
+ * audit; the framework allows multiple versions of the same adapter
787
+ * to coexist in the registry (deployment-controlled).
788
+ */
789
+ readonly version: number;
790
+ /**
791
+ * Subset of the adapter's category fields that this adapter promises
792
+ * to produce. Used by the host app at boot to log adapter capability
793
+ * + by the CRA report to render "provided by adapter X" badges.
794
+ * MUST be a subset of `categoryFieldsOf(category)`; the host app
795
+ * validates this at registration time and refuses to load adapters
796
+ * that promise fields outside their category.
797
+ */
798
+ readonly produces: ReadonlyArray<CanonicalFieldName>;
799
+ /**
800
+ * Transform a raw payload into canonical field values. The host app
801
+ * calls this once per applicant per adapter, passes the result to
802
+ * the persistence layer (one row per AdapterExtraction in the
803
+ * canonical sibling table), and writes provenance.
804
+ *
805
+ * The return is ALWAYS a list, even when the adapter produces only
806
+ * one instance per applicant — return a one-element array in that
807
+ * case with `instanceKey: ''`. The list shape exists to support
808
+ * inherently multi-instance sources (6 bank statements, 12 monthly
809
+ * payment snapshots, 3 mobile lines) without forcing adapters to
810
+ * batch their own calls.
811
+ *
812
+ * Throw `AdapterError` for any failure path — network timeout,
813
+ * schema mismatch, partner API rate limit, malformed payload. The
814
+ * host's adapter runner catches AdapterError + records the failure
815
+ * on `ihs_extraction_runs` so the operator sees what happened
816
+ * without losing the applicant.
817
+ *
818
+ * Adapters MUST NOT throw plain `Error` or unknown types — the
819
+ * runner will treat those as adapter implementation bugs and surface
820
+ * them to logs separately. Stick to AdapterError for the
821
+ * expected-failure path.
822
+ */
823
+ extract(raw: RawPayload): Promise<AdapterExtraction[]>;
824
+ /**
825
+ * Optional partner-data fetch path. v2.7.0+ — adapters that own their
826
+ * partner-API integration declare this method; the host invokes it
827
+ * before extract() and passes the result through. Adapters that
828
+ * receive raw payload from elsewhere (e.g., bank-statement adapters
829
+ * reading FinXtract OCR output, or webhook-driven push ingest) omit
830
+ * fetch() entirely and the host treats raw as already-staged.
831
+ *
832
+ * The host builds `identity` from the IHS row (ic, fullName, etc.)
833
+ * plus any partner-specific fields the adapter declared via the
834
+ * manifest's `requiredIdentityFields`. If a required field is missing
835
+ * from the IHS, the host logs + skips this adapter for the run
836
+ * (no extract call, no canonical rows).
837
+ *
838
+ * Implementations should:
839
+ * - Throw AdapterError('source_unavailable') ONLY on transient,
840
+ * retryable upstream failures (timeouts, 5xx, rate limits). The
841
+ * host runner is free to retry these; misclassifying a permanent
842
+ * failure here will burn retry budget for nothing.
843
+ * - Throw AdapterError('not_applicable') on permanent
844
+ * no-relationship signals — 404 ("applicant unknown to partner"),
845
+ * 410, an explicit "not a subscriber" response. Communicates
846
+ * 'we asked and they said no' so the host records the run + skips
847
+ * retry. This is the correct reason for most non-retryable
848
+ * upstream conditions.
849
+ * - Throw AdapterError('payload_invalid') if the partner returned
850
+ * a malformed response (200 with garbage body, schema violation).
851
+ * - Return a RawPayload that extract() will translate. The same
852
+ * adapter's extract() is the only consumer; the host doesn't
853
+ * inspect the shape.
854
+ *
855
+ * Strict additive — v2.6.0 read-only adapters that omit fetch()
856
+ * keep working without change.
857
+ *
858
+ * @example WRONG — recurses into the adapter method:
859
+ * ```ts
860
+ * const adapter: SourceAdapter = {
861
+ * async fetch(identity) {
862
+ * const res = await fetch(`/api/lookup/${identity.ic}`) // infinite recursion
863
+ * return res.json()
864
+ * },
865
+ * ...
866
+ * }
867
+ * ```
868
+ *
869
+ * @example RIGHT — explicit globalThis access:
870
+ * ```ts
871
+ * const adapter: SourceAdapter = {
872
+ * async fetch(identity) {
873
+ * const res = await globalThis.fetch(`/api/lookup/${identity.ic}`)
874
+ * return res.json()
875
+ * },
876
+ * ...
877
+ * }
878
+ * ```
879
+ *
880
+ * Implementation note: this method is named `fetch` which shadows
881
+ * the global `fetch()` inside the method body — see the @example
882
+ * blocks above. The fake-* reference adapters in
883
+ * @finsys/adapter-toolkit use the explicit-globalThis pattern.
884
+ * Partner repos can also pin this with the lint rule
885
+ * `no-restricted-syntax`: `CallExpression[callee.name='fetch']`
886
+ * inside any method literal named `fetch`.
887
+ */
888
+ fetch?(identity: ApplicantIdentity<E>): Promise<RawPayload>;
889
+ }
890
+ /**
891
+ * Identity payload the host hands to fetch() so adapters can call
892
+ * partner APIs with the right per-applicant identifiers.
893
+ *
894
+ * The shape is an intersection of three pieces:
895
+ * - Core fields (`ihsId`, `ic`, `fullName`) — always present, the
896
+ * host populates them from the IHS row before invoking fetch().
897
+ * - Partner extensions (`E`) — typed declaratively per adapter.
898
+ * Defaults to `{}` (no extra typed fields) for adapters that only
899
+ * need the core trio.
900
+ * - Open index signature (`[k: string]: unknown`) — catches any
901
+ * ad-hoc field a host might pass through; reads land on `unknown`
902
+ * so partners must validate before use.
903
+ *
904
+ * Narrowing example (the ergonomics win this type is designed to deliver):
905
+ *
906
+ * interface CelcomExt { msisdn: string; accountRef: string }
907
+ *
908
+ * // declare adapter with the extension shape baked in:
909
+ * const celcom: SourceAdapter<CelcomExt> = {
910
+ * async fetch(identity) {
911
+ * identity.ic // string
912
+ * identity.msisdn // string (NOT unknown — narrowed by E)
913
+ * identity.foo // unknown (falls through to index signature)
914
+ * // ...
915
+ * },
916
+ * // ...
917
+ * }
918
+ *
919
+ * The intersection puts narrowed members at the root, so partners get
920
+ * `identity.msisdn` (not `identity.extensions?.msisdn`) — matching the
921
+ * pattern the host actually uses (it spreads partner-required fields
922
+ * onto the root identity object, no nested 'extensions' envelope).
923
+ *
924
+ * Examples:
925
+ * - Telco adapter declares `requiredIdentityFields: ['msisdn']`.
926
+ * Identity arrives with msisdn populated; adapter calls partner API.
927
+ * (Don't include 'ihsId', 'ic', or 'fullName' in
928
+ * `requiredIdentityFields` — those are core fields. `ic` in
929
+ * particular can be legitimately empty for non-MY scope, so
930
+ * declaring it required would cause the host to skip every applicant.)
931
+ * - Payment-network adapter declares `requiredIdentityFields:
932
+ * ['businessRegistrationNumber']`. Adapter looks it up + calls.
933
+ * - An adapter that only needs the IHS id declares no extra
934
+ * fields; identity has just the core trio.
935
+ */
936
+ type ApplicantIdentity<E extends Record<string, unknown> = {}> = {
937
+ /** Internal IHS id — always present. */
938
+ readonly ihsId: number;
939
+ /** Malaysian IC (or analogous national id). May be empty for non-MY scope. */
940
+ readonly ic: string;
941
+ /** Applicant full legal name. */
942
+ readonly fullName: string;
943
+ } & E & {
944
+ /** Catch-all for ad-hoc reads (typed `unknown` — validate before use). */
945
+ readonly [key: string]: unknown;
946
+ };
947
+ /**
948
+ * Typed error thrown by adapters on expected failure paths. The
949
+ * `reason` discriminator lets the host app's adapter runner classify
950
+ * failures (transient vs permanent, partner-side vs schema-side)
951
+ * without parsing strings.
952
+ */
953
+ declare class AdapterError extends Error {
954
+ readonly reason: AdapterErrorReason;
955
+ readonly cause?: unknown;
956
+ constructor(reason: AdapterErrorReason, message: string, cause?: unknown);
957
+ }
958
+ /**
959
+ * Discriminator for adapter failure modes. The runner uses this to
960
+ * decide whether to retry, surface to the operator, or write off the
961
+ * extraction run.
962
+ *
963
+ * payload_invalid — the raw payload didn't conform to the
964
+ * adapter's expected source shape. Permanent;
965
+ * re-running won't help. Likely a partner-side
966
+ * schema change → bump adapter version.
967
+ *
968
+ * source_unavailable — the upstream source was unreachable or
969
+ * returned a transient error (timeout, 5xx,
970
+ * rate limit). Transient; safe to retry.
971
+ *
972
+ * mapping_failed — extraction logic raised a domain-specific
973
+ * failure (e.g. value out of expected range).
974
+ * Permanent for this payload; surface to the
975
+ * operator.
976
+ *
977
+ * not_applicable — the applicant doesn't have data for this
978
+ * adapter (e.g. no telco opt-in). Not a
979
+ * failure per se — the runner records a
980
+ * no-op extraction run, eval engine sees no
981
+ * canonical fields, components score zero.
982
+ */
983
+ type AdapterErrorReason = "payload_invalid" | "source_unavailable" | "mapping_failed" | "not_applicable";
984
+
985
+ /**
986
+ * Aggregation operators applied by the eval engine when a scoring
987
+ * component reads a canonical field that has multiple instances
988
+ * persisted for the same applicant.
989
+ *
990
+ * Multi-instance is the framework's first-class shape — an adapter
991
+ * may produce 6 bank-statement instances, 12 monthly payment
992
+ * snapshots, 3 mobile lines, etc., per applicant. Eval components
993
+ * must declare HOW they want those instances collapsed into a single
994
+ * scoring value. This module declares the operator set + the runtime
995
+ * helper that applies one to a value list.
996
+ *
997
+ * Consumer is the eval engine (today: finsys-client + FinSim sim-
998
+ * runner; tomorrow: anywhere else evaluating against canonical
999
+ * fields). finsys-core publishes the operator surface so policy
1000
+ * fixtures across all consumers reference the same set.
1001
+ *
1002
+ * v1 operator set, in order of expected use frequency:
1003
+ *
1004
+ * sum — total across instances. Bank balance sum across t1..tN.
1005
+ * mean — arithmetic mean. Average monthly payment volume.
1006
+ * latest — the instance with the most recent observed_at. Telco
1007
+ * snapshot when only the latest matters.
1008
+ * max — maximum value across instances. Worst-case suspension
1009
+ * count when multiple telco lines exist.
1010
+ * count — number of instances with a non-null value for this
1011
+ * field. Useful for "how many bank statements does this
1012
+ * applicant have on file" as a score input.
1013
+ */
1014
+
1015
+ type AggregationOp = "sum" | "mean" | "latest" | "max" | "count";
1016
+ /** Every operator declared in this version of finsys-core. */
1017
+ declare const ALL_AGGREGATION_OPS: ReadonlyArray<AggregationOp>;
1018
+ /**
1019
+ * One value contributed by one instance. The eval engine builds a
1020
+ * list of these (one per instance, in observed-at order — the
1021
+ * persistence layer is the source of truth on ordering) and passes
1022
+ * to applyAggregation.
1023
+ */
1024
+ interface InstanceValue {
1025
+ readonly value: CanonicalFieldValue;
1026
+ /**
1027
+ * The instance's observed-at timestamp. MUST be an ISO-8601 UTC
1028
+ * string (`Z`-suffixed, e.g. `2026-04-01T00:00:00Z`). The `latest`
1029
+ * operator compares these lexicographically, which is only
1030
+ * monotonic with wall-time when all timestamps share the UTC
1031
+ * offset — mixed offsets (e.g. `+08:00` vs `Z`) can yield
1032
+ * incorrect ordering. Other operators ignore this field; pass an
1033
+ * empty string if the operator in question doesn't need it.
1034
+ */
1035
+ readonly observedAt: string;
1036
+ }
1037
+ /**
1038
+ * Apply an aggregation operator to a list of instance values.
1039
+ *
1040
+ * Behaviour for empty + null-only lists:
1041
+ * - empty list → `null`
1042
+ * - all-null values → `null`
1043
+ * - `count` on either of the above → `0`
1044
+ *
1045
+ * Returns:
1046
+ * - `sum`, `mean`, `max`, `count` → `number | null`
1047
+ * - `latest` → matches the value type of the most-recent instance
1048
+ * (number / boolean / string / null)
1049
+ *
1050
+ * Throws on operators that don't apply to the value types present
1051
+ * (e.g. `sum` of booleans). Eval-policy authors are expected to pair
1052
+ * operators with appropriate field types; this is a guardrail against
1053
+ * silent garbage rather than an error-recovery mechanism.
1054
+ */
1055
+ declare function applyAggregation(op: AggregationOp, instances: ReadonlyArray<InstanceValue>): CanonicalFieldValue;
1056
+
1057
+ /**
1058
+ * Adapter manifest — the declarative descriptor every adapter ships
1059
+ * alongside its implementation. The host app's discovery mechanism
1060
+ * (SYS-2444) reads `manifest.json` from each adapter directory, validates
1061
+ * it against the JSON-schema in `schema/adapter-manifest.schema.json`,
1062
+ * and uses the manifest to construct + register the runtime adapter.
1063
+ *
1064
+ * Two adapter flavours both use this manifest shape:
1065
+ *
1066
+ * 1. Declarative JSON-only adapter (no TypeScript code) — `manifest.json`
1067
+ * includes a `fieldMap` declaring source-path → canonical-field
1068
+ * translations. The host's adapter runner applies the mapping +
1069
+ * transforms generically. Suitable for partners whose API maps
1070
+ * cleanly to canonical fields with rename + simple transforms.
1071
+ *
1072
+ * 2. TypeScript adapter — `manifest.json` includes an `entryPoint`
1073
+ * pointing at an `extract.ts` (or compiled `.js`) module that
1074
+ * exports a function implementing the `SourceAdapter#extract`
1075
+ * contract. Used when the source needs OAuth, multi-endpoint
1076
+ * merging, OCR pipelines, or partner-version negotiation.
1077
+ *
1078
+ * The JSON-schema (`schema/adapter-manifest.schema.json`) is the
1079
+ * authoritative format; this TypeScript type mirrors it. They MUST
1080
+ * stay in sync — when one changes, change the other.
1081
+ */
1082
+
1083
+ /**
1084
+ * Top-level manifest shape. Mirrors `schema/adapter-manifest.schema.json`.
1085
+ */
1086
+ interface AdapterManifest {
1087
+ /** Manifest format version. Bump this file's schema → bump this field. */
1088
+ readonly manifestVersion: 1;
1089
+ /**
1090
+ * Globally unique adapter id. Conventional pattern:
1091
+ * `<vendor>-<category-short>-v<n>` (e.g. `celcom-telco-v1`). The id is
1092
+ * what `SourceAdapter#id` returns + what provenance records pin to.
1093
+ */
1094
+ readonly id: string;
1095
+ /** Human-readable label rendered in operator UIs. */
1096
+ readonly displayName: string;
1097
+ /**
1098
+ * Generic category this adapter implements. Determines which
1099
+ * canonical fields the adapter is allowed to produce + which
1100
+ * canonical sibling table its output is persisted into.
1101
+ */
1102
+ readonly category: AdapterCategory;
1103
+ /**
1104
+ * Monotonic version per adapter. Bump on any change to payload shape,
1105
+ * field-mapping logic, or output semantics. Multiple versions of the
1106
+ * same adapter family MAY coexist in a deployment (deployment-time
1107
+ * pin).
1108
+ */
1109
+ readonly version: number;
1110
+ /**
1111
+ * Which canonical fields (subset of the category's field set) this
1112
+ * adapter promises to produce. The host validates at registration
1113
+ * time that every entry is declared by `categoryFieldsOf(category)`;
1114
+ * adapters promising out-of-category fields are refused.
1115
+ */
1116
+ readonly produces: ReadonlyArray<CanonicalFieldName>;
1117
+ /**
1118
+ * Adapter implementation flavour. Discriminator drives the rest of
1119
+ * the manifest shape.
1120
+ *
1121
+ * - `declarative` — JSON-only adapter; `fieldMap` is required, no
1122
+ * code is loaded.
1123
+ * - `typescript` — TS/JS adapter; `entryPoint` is required, the
1124
+ * host imports it dynamically.
1125
+ */
1126
+ readonly implementation: DeclarativeImplementation | TypescriptImplementation;
1127
+ /**
1128
+ * v2.7.0 — partner-specific identity fields the adapter needs from
1129
+ * the IHS row when fetch() is invoked. Strings name the keys that
1130
+ * MUST be present on the ApplicantIdentity payload — the host
1131
+ * validates per-applicant before calling fetch() and skips the
1132
+ * adapter (with a logged warning) if any required field is missing.
1133
+ *
1134
+ * Examples:
1135
+ * - Telco adapter: `["ic", "msisdn"]`
1136
+ * - Payment-network adapter: `["businessRegistrationNumber"]`
1137
+ * - Bank-statement-from-OCR adapter: `[]` (no fetch — extract
1138
+ * reads from FinXtract output staged elsewhere)
1139
+ *
1140
+ * Omit entirely (or use empty array) for adapters that don't
1141
+ * implement fetch() — the host then passes an empty raw payload
1142
+ * to extract() and the adapter is responsible for sourcing data
1143
+ * via some other path (FinXtract output, webhook ingest, etc.).
1144
+ *
1145
+ * Core identity fields (`ihsId`, `ic`, `fullName`) are always
1146
+ * present — declaring them is harmless but unnecessary.
1147
+ */
1148
+ readonly requiredIdentityFields?: ReadonlyArray<string>;
1149
+ /**
1150
+ * Optional free-form notes — useful for partner-side documentation
1151
+ * (where to find the adapter's source, who owns it, what version of
1152
+ * the partner API it expects). Not consumed by the runtime.
1153
+ */
1154
+ readonly notes?: string;
1155
+ }
1156
+ interface DeclarativeImplementation {
1157
+ readonly type: "declarative";
1158
+ /**
1159
+ * Each entry maps a JSONPath into the raw payload to a canonical
1160
+ * field name, optionally applying a transform. The host's adapter
1161
+ * runner walks this list per `extract()` invocation.
1162
+ */
1163
+ readonly fieldMap: ReadonlyArray<FieldMapEntry>;
1164
+ }
1165
+ interface TypescriptImplementation {
1166
+ readonly type: "typescript";
1167
+ /**
1168
+ * Relative path (from the adapter's directory) to the module that
1169
+ * exports the extract function. The host's discovery mechanism
1170
+ * dynamic-imports this path at adapter registration time.
1171
+ */
1172
+ readonly entryPoint: string;
1173
+ }
1174
+ interface FieldMapEntry {
1175
+ /**
1176
+ * JSONPath expression into the raw payload. E.g.
1177
+ * `$.bill.payment_24m_pct` or `$.account.tenure_months`. Single-
1178
+ * value extraction; arrays + objects MUST be flattened by the
1179
+ * partner-API client before invoking the adapter.
1180
+ */
1181
+ readonly source: string;
1182
+ /**
1183
+ * Canonical field name this maps to. MUST be declared by the
1184
+ * adapter's category. Host validates at registration.
1185
+ */
1186
+ readonly canonical: CanonicalFieldName;
1187
+ /**
1188
+ * Optional value transform applied between source extraction +
1189
+ * canonical-field write. Built-in transforms TBD as the pattern
1190
+ * settles; v1 supports:
1191
+ * - `identity` — default; pass through
1192
+ * - `pct_to_ratio01` — divide by 100, clamp to [0, 1]
1193
+ * - `to_boolean` — truthy → true, falsy → false
1194
+ * - `to_integer` — round + cast
1195
+ * Adapters needing more complex transforms should use the
1196
+ * `typescript` implementation flavour instead.
1197
+ */
1198
+ readonly transform?: "identity" | "pct_to_ratio01" | "to_boolean" | "to_integer";
1199
+ }
1200
+
1201
+ export { ALL_AGGREGATION_OPS, type AdapterCategory, AdapterError, type AdapterErrorReason, type AdapterExtraction, type AdapterManifest, type AggregationOp, type ApplicantIdentity, BASE_FIELD_SPECS, BasicFormField, type CanonicalFieldName, type CanonicalFieldSpec, type CanonicalFieldValue, type CanonicalFieldValues, type Category, type CategorySchema, type Choice, DEFAULT_VALIDATOR_DEFINITIONS, type DeclarativeImplementation, type DocExtractionResult, DocExtractionStatus, type DropdownOption, type EditorValidator, ExtractionFileType, type ExtractionJobRecord, ExtractionJobStatus, type ExtractionStatusResult, FIELD_TYPE_DEFINITIONS, type FieldData, type FieldGroup, type FieldMapEntry, type FieldReference, FieldType, type FileFieldTableData, type FileFieldTableItem, FileFieldTableType, FileFormField, FormField, FormFieldCategory, type FormFieldInputType, type FormFieldType, type FormFieldTypeDefinitions, FormFieldValidator, FormSpec, type FormValidatorDefinitions, IHS_FAILURE_STATUSES, IHS_TERMINAL_STATUSES, IHS_VALID_STATUSES, type IhsDetailCategory, type IhsFieldDetail, IhsStatus, IhsValueFormat, type InstanceValue, type PageConfig, type RHFSchemaOutput, type RHFStep, type RawPayload, type ResolvedField, Role, type SourceAdapter, type SurveyElementJSON, type SurveyJSON, type SurveyPageJSON, type TypescriptImplementation, type UnifiedFormConfig, type Validator, allCategories, applyAggregation, applyDynamicTitles, buildFileFieldTables, categoryFieldsOf, categoryForField, categorySchemaOf, evaluateExpression, extractTimePeriods, generateRHFSchema, generateSurveyJson, getBaseCategories, getBaseFieldNames, getBaseFieldSpecMap, getBaseFieldSpecs, getCategoryName, getDisplayName, getDisplayNames, getGroupDisplayNames, getPastMonthLabel, getPastYearLabel, getStepDefaultValues, getStepSchema, groupColumnsByTimePeriod, groupDetailsByCategory, groupFieldsByCategory, groupFieldsByPattern, isFailureIhsStatus, isTerminalIhsStatus, isValidIhsStatus, processIhsDetails, resolveExtractionStatus, resolvePageFields, validateFormConfig, validateFormSpec, validatePagesConfig };