@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/README.md +14 -2
- package/dist/data/adapter-categories.json +156 -0
- package/dist/index.cjs +274 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +629 -1
- package/dist/index.d.ts +629 -1
- package/dist/index.js +267 -16
- package/dist/index.js.map +1 -1
- package/dist/schema/adapter-manifest.schema.json +112 -0
- package/package.json +6 -1
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
|
-
|
|
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 };
|