@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 CHANGED
@@ -1,8 +1,20 @@
1
1
  # @finsys/core
2
2
 
3
- Convert unified form configurations into Zod schemas, React Hook Form defaults, and SurveyJS JSON.
3
+ The shared contract layer for the FinSys ecosystem. Two distinct surfaces:
4
4
 
5
- ## Features
5
+ 1. **Unified form configuration** — generate Zod schemas, React Hook Form defaults, and SurveyJS JSON from a single JSON form spec.
6
+ 2. **Source Adapter Framework** — vendor-agnostic contract for ingesting alternative-data sources (telco, payments, bank statements, etc.) into FinSys's canonical credit-signal vocabulary.
7
+
8
+ ## Building a Source Adapter?
9
+
10
+ Partner-facing integration documentation lives in [`docs/`](./docs/):
11
+
12
+ - 📖 [**Adapter Authoring Guide**](./docs/adapter-authoring.md) — start here. Walks from "what's an adapter" through "how do I package and deliver one."
13
+ - 📋 [Category Reference](./docs/category-reference.md) — canonical field set per category.
14
+ - 📋 [Manifest Reference](./docs/manifest-reference.md) — every field in `manifest.json`.
15
+ - 🔒 [Security Model](./docs/security-model.md) — sandboxing, secrets, network posture.
16
+
17
+ ## Features (form-config surface)
6
18
 
7
19
  - **Unified Form Configuration** — Single JSON format for defining form fields and multi-step page layouts
8
20
  - **React Hook Form Generator** — Generate Zod schemas, default values, and step configurations from a form config
@@ -0,0 +1,156 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "categories": [
4
+ {
5
+ "id": "telco-carrier",
6
+ "displayName": "Telco Carrier",
7
+ "description": "Mobile carrier bill-payment + account-history signals. Any telco vendor (Celcom, Maxis, DiGi, U-Mobile, etc.) implementing this category produces the same canonical field set.",
8
+ "canonicalTable": "ihs_alt_data_telco",
9
+ "fields": [
10
+ {
11
+ "name": "telcoOnTimePaymentRatio24m",
12
+ "type": "number",
13
+ "unit": "ratio",
14
+ "range": [0, 1],
15
+ "description": "Fraction of bills paid on time over the last 24 months. Strongest single telco predictor; ≥0.95 is the clean-history signal."
16
+ },
17
+ {
18
+ "name": "telcoTenureMonths",
19
+ "type": "number",
20
+ "unit": "months",
21
+ "range": [0, 600],
22
+ "description": "Account age. ≥48 months is the thin-file uplift trigger."
23
+ },
24
+ {
25
+ "name": "telcoSuspensionsCount24m",
26
+ "type": "number",
27
+ "unit": "count",
28
+ "range": [0, 100],
29
+ "description": "Non-payment-driven account suspensions in the last 24 months. ≥3 is a strong distress signal."
30
+ },
31
+ {
32
+ "name": "telcoLateDays24m",
33
+ "type": "number",
34
+ "unit": "days",
35
+ "range": [0, 800],
36
+ "description": "Cumulative days late across all bills in the trailing 24-month window."
37
+ },
38
+ {
39
+ "name": "telcoHandsetFinancingActive",
40
+ "type": "boolean",
41
+ "description": "Has an active handset-EMI account currently. Proxy for financing capacity already extended."
42
+ },
43
+ {
44
+ "name": "telcoHandsetFinancingDelinquent",
45
+ "type": "boolean",
46
+ "description": "Recent handset-EMI delinquency in the last 24 months. Distress signal even with otherwise clean bill payment."
47
+ },
48
+ {
49
+ "name": "telcoArpuMyr",
50
+ "type": "number",
51
+ "unit": "MYR",
52
+ "range": [0, 10000],
53
+ "description": "Average Revenue Per User (monthly) in Malaysian Ringgit. Coarse spending-capacity proxy."
54
+ }
55
+ ]
56
+ },
57
+ {
58
+ "id": "payment-network",
59
+ "displayName": "Payment Network",
60
+ "description": "Merchant-side payment-flow signals from POS / gateway networks (iPay88, GHL, etc.). Captures actual transaction velocity rather than self-reported revenue.",
61
+ "canonicalTable": "ihs_alt_data_payments",
62
+ "fields": [
63
+ {
64
+ "name": "paymentsMonthlyVolumeMyrT3",
65
+ "type": "number",
66
+ "unit": "MYR",
67
+ "range": [0, 100000000],
68
+ "description": "Mean monthly inbound transaction volume (RM) over the trailing 3 months."
69
+ },
70
+ {
71
+ "name": "paymentsMonthlyVolumeMyrT12",
72
+ "type": "number",
73
+ "unit": "MYR",
74
+ "range": [0, 100000000],
75
+ "description": "Mean monthly inbound transaction volume (RM) over the trailing 12 months. Pair with T3 for trend direction."
76
+ },
77
+ {
78
+ "name": "paymentsArpuStability12m",
79
+ "type": "number",
80
+ "unit": "ratio",
81
+ "range": [0, 1],
82
+ "description": "Coefficient-of-variation inverse over monthly ARPU in the trailing 12 months. Closer to 1 = steadier; closer to 0 = volatile."
83
+ },
84
+ {
85
+ "name": "paymentsDisputeRate12m",
86
+ "type": "number",
87
+ "unit": "ratio",
88
+ "range": [0, 1],
89
+ "description": "Fraction of transactions disputed or refunded in the trailing 12 months."
90
+ },
91
+ {
92
+ "name": "paymentsCustomerConcentrationTop5Pct",
93
+ "type": "number",
94
+ "unit": "ratio",
95
+ "range": [0, 1],
96
+ "description": "Revenue share from the top-5 recurring customers. Above ~0.7 is concentration risk."
97
+ },
98
+ {
99
+ "name": "paymentsActiveTenureMonths",
100
+ "type": "number",
101
+ "unit": "months",
102
+ "range": [0, 600],
103
+ "description": "Months since first transaction on the payment network. Establishment / continuity proxy."
104
+ }
105
+ ]
106
+ },
107
+ {
108
+ "id": "bank-statement",
109
+ "displayName": "Bank Statement",
110
+ "description": "Per-month bank-statement extractions. Naturally multi-instance: one statement per (account, month). Eval components typically aggregate across instances (sum closing balance, max debit, count of bounced transactions).",
111
+ "canonicalTable": "ihs_alt_data_bank_statements",
112
+ "fields": [
113
+ {
114
+ "name": "bankStatementMonth",
115
+ "type": "string",
116
+ "description": "Statement period in YYYY-MM format. Used as the instance_key discriminator + by the `latest` aggregation operator."
117
+ },
118
+ {
119
+ "name": "bankClosingBalanceMyr",
120
+ "type": "number",
121
+ "unit": "MYR",
122
+ "range": [-100000000, 100000000],
123
+ "description": "Closing balance for the statement period."
124
+ },
125
+ {
126
+ "name": "bankTotalCreditsMyr",
127
+ "type": "number",
128
+ "unit": "MYR",
129
+ "range": [0, 100000000],
130
+ "description": "Sum of credit transactions during the period."
131
+ },
132
+ {
133
+ "name": "bankTotalDebitsMyr",
134
+ "type": "number",
135
+ "unit": "MYR",
136
+ "range": [0, 100000000],
137
+ "description": "Sum of debit transactions during the period."
138
+ },
139
+ {
140
+ "name": "bankLargestSingleCreditMyr",
141
+ "type": "number",
142
+ "unit": "MYR",
143
+ "range": [0, 100000000],
144
+ "description": "Largest single inbound transaction in the period. Useful for spotting one-off injections vs steady revenue."
145
+ },
146
+ {
147
+ "name": "bankBouncedTransactionsCount",
148
+ "type": "number",
149
+ "unit": "count",
150
+ "range": [0, 1000],
151
+ "description": "Bounced / returned transactions in the period. Distress signal at counts > 0."
152
+ }
153
+ ]
154
+ }
155
+ ]
156
+ }
package/dist/index.cjs CHANGED
@@ -30,6 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ ALL_AGGREGATION_OPS: () => ALL_AGGREGATION_OPS,
34
+ AdapterError: () => AdapterError,
33
35
  BASE_FIELD_SPECS: () => BASE_FIELD_SPECS,
34
36
  BasicFormField: () => BasicFormField,
35
37
  DEFAULT_VALIDATOR_DEFINITIONS: () => DEFAULT_VALIDATOR_DEFINITIONS,
@@ -50,8 +52,13 @@ __export(index_exports, {
50
52
  IhsStatus: () => IhsStatus,
51
53
  IhsValueFormat: () => IhsValueFormat,
52
54
  Role: () => Role,
55
+ allCategories: () => allCategories,
56
+ applyAggregation: () => applyAggregation,
53
57
  applyDynamicTitles: () => applyDynamicTitles,
54
58
  buildFileFieldTables: () => buildFileFieldTables,
59
+ categoryFieldsOf: () => categoryFieldsOf,
60
+ categoryForField: () => categoryForField,
61
+ categorySchemaOf: () => categorySchemaOf,
55
62
  evaluateExpression: () => evaluateExpression,
56
63
  extractTimePeriods: () => extractTimePeriods,
57
64
  generateRHFSchema: () => generateRHFSchema,
@@ -95,7 +102,7 @@ function getPastYearLabel(yearsAgo) {
95
102
  const date = /* @__PURE__ */ new Date();
96
103
  return (date.getFullYear() - yearsAgo).toString();
97
104
  }
98
- function evaluateExpression(expression, data) {
105
+ function evaluateExpression(expression, data2) {
99
106
  if (!expression) return true;
100
107
  try {
101
108
  let jsExpression = expression.replace(/\{([^}]+)\}/g, (_, key) => {
@@ -119,7 +126,7 @@ function evaluateExpression(expression, data) {
119
126
  (_, ref, val) => `!(${ref} || []).includes('${val}')`
120
127
  );
121
128
  const func = new Function(`return ${jsExpression};`);
122
- return !!func.call(data || {});
129
+ return !!func.call(data2 || {});
123
130
  } catch (e) {
124
131
  return false;
125
132
  }
@@ -265,16 +272,16 @@ function groupFieldsByCategory(fields, categories) {
265
272
  // src/rhf-generator.ts
266
273
  var import_zod = require("zod");
267
274
  function applyVisibilityValidation(baseObject, fields) {
268
- return baseObject.superRefine((data, ctx) => {
275
+ return baseObject.superRefine((data2, ctx) => {
269
276
  fields.forEach((field) => {
270
277
  const isRequired = field.isRequired !== false && field.required !== false && field.type !== "html";
271
278
  if (!isRequired) return;
272
279
  let isVisible = field.visible !== false;
273
280
  if (isVisible && field.visibleIf) {
274
- isVisible = evaluateExpression(field.visibleIf, data);
281
+ isVisible = evaluateExpression(field.visibleIf, data2);
275
282
  }
276
283
  if (isVisible) {
277
- const value = data[field.name];
284
+ const value = data2[field.name];
278
285
  const isEmpty = value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
279
286
  if (isEmpty) {
280
287
  ctx.addIssue({
@@ -735,22 +742,22 @@ var ajv = new Ajv({ allErrors: true, strict: false });
735
742
  var addFormatsFn = import_ajv_formats.default.default || import_ajv_formats.default;
736
743
  addFormatsFn(ajv);
737
744
  ajv.addSchema(unified_form_schema_default, "unified-form.schema.json");
738
- function validateFormConfig(data) {
745
+ function validateFormConfig(data2) {
739
746
  const validate = ajv.getSchema("unified-form.schema.json");
740
747
  if (!validate) {
741
748
  return { valid: false, message: "Could not load unified form schema" };
742
749
  }
743
- const valid = validate(data);
750
+ const valid = validate(data2);
744
751
  return {
745
752
  valid: !!valid,
746
753
  errors: validate.errors || void 0
747
754
  };
748
755
  }
749
- function validateFormSpec(data) {
750
- return validateFormConfig(data);
756
+ function validateFormSpec(data2) {
757
+ return validateFormConfig(data2);
751
758
  }
752
- function validatePagesConfig(data) {
753
- return validateFormConfig(data);
759
+ function validatePagesConfig(data2) {
760
+ return validateFormConfig(data2);
754
761
  }
755
762
 
756
763
  // src/form-field-category.ts
@@ -1207,10 +1214,10 @@ var _FormSpec = class _FormSpec {
1207
1214
  if (Array.isArray(jsonData.fields)) {
1208
1215
  fieldObjects = jsonData.fields;
1209
1216
  } else if (typeof jsonData.fields === "object" && jsonData.fields !== null) {
1210
- fieldObjects = Object.entries(jsonData.fields).map(([name, data]) => {
1217
+ fieldObjects = Object.entries(jsonData.fields).map(([name, data2]) => {
1211
1218
  return {
1212
1219
  name,
1213
- ...data
1220
+ ...data2
1214
1221
  };
1215
1222
  });
1216
1223
  }
@@ -9468,18 +9475,18 @@ function buildTableForGroup(groupName, fields, ihsData) {
9468
9475
  const items = [];
9469
9476
  for (const [baseName, periodMap] of Object.entries(columnGroups)) {
9470
9477
  const numeric = isNumericField(baseName);
9471
- const data = {};
9478
+ const data2 = {};
9472
9479
  const formattedData = {};
9473
9480
  for (const period of periods) {
9474
9481
  const colName = periodMap[period];
9475
9482
  const value = colName ? ihsData[colName] ?? null : null;
9476
- data[period] = value;
9483
+ data2[period] = value;
9477
9484
  formattedData[period] = formatValue(value, numeric);
9478
9485
  }
9479
9486
  items.push({
9480
9487
  displayName: getDisplayName(baseName),
9481
9488
  timePeriods: periods,
9482
- data,
9489
+ data: data2,
9483
9490
  formattedData,
9484
9491
  type: tableType,
9485
9492
  isNumeric: numeric
@@ -9836,8 +9843,254 @@ function resolveExtractionStatus(ihsRecord, jobRecords) {
9836
9843
  );
9837
9844
  return { documents, summary };
9838
9845
  }
9846
+
9847
+ // src/adapter.ts
9848
+ var AdapterError = class extends Error {
9849
+ constructor(reason, message, cause) {
9850
+ super(message);
9851
+ this.name = "AdapterError";
9852
+ this.reason = reason;
9853
+ this.cause = cause;
9854
+ }
9855
+ };
9856
+
9857
+ // src/adapter-aggregation.ts
9858
+ var ALL_AGGREGATION_OPS = [
9859
+ "sum",
9860
+ "mean",
9861
+ "latest",
9862
+ "max",
9863
+ "count"
9864
+ ];
9865
+ function applyAggregation(op, instances) {
9866
+ if (instances.length === 0) {
9867
+ return op === "count" ? 0 : null;
9868
+ }
9869
+ if (op === "count") {
9870
+ return instances.reduce(
9871
+ (acc, i) => i.value !== null && i.value !== void 0 ? acc + 1 : acc,
9872
+ 0
9873
+ );
9874
+ }
9875
+ if (op === "latest") {
9876
+ let pick = instances[0];
9877
+ for (let i = 1; i < instances.length; i++) {
9878
+ if (instances[i].observedAt > pick.observedAt) pick = instances[i];
9879
+ }
9880
+ return pick.value;
9881
+ }
9882
+ const nums = [];
9883
+ for (const i of instances) {
9884
+ if (typeof i.value === "number" && Number.isFinite(i.value)) {
9885
+ nums.push(i.value);
9886
+ } else if (i.value !== null && typeof i.value !== "undefined") {
9887
+ throw new Error(
9888
+ `applyAggregation: '${op}' requires numeric values; got ${typeof i.value}`
9889
+ );
9890
+ }
9891
+ }
9892
+ if (nums.length === 0) return null;
9893
+ switch (op) {
9894
+ case "sum":
9895
+ return nums.reduce((a, b) => a + b, 0);
9896
+ case "mean":
9897
+ return nums.reduce((a, b) => a + b, 0) / nums.length;
9898
+ case "max":
9899
+ return nums.reduce((a, b) => a > b ? a : b);
9900
+ }
9901
+ }
9902
+
9903
+ // src/data/adapter-categories.json
9904
+ var adapter_categories_default = {
9905
+ schemaVersion: "1.0.0",
9906
+ categories: [
9907
+ {
9908
+ id: "telco-carrier",
9909
+ displayName: "Telco Carrier",
9910
+ description: "Mobile carrier bill-payment + account-history signals. Any telco vendor (Celcom, Maxis, DiGi, U-Mobile, etc.) implementing this category produces the same canonical field set.",
9911
+ canonicalTable: "ihs_alt_data_telco",
9912
+ fields: [
9913
+ {
9914
+ name: "telcoOnTimePaymentRatio24m",
9915
+ type: "number",
9916
+ unit: "ratio",
9917
+ range: [0, 1],
9918
+ description: "Fraction of bills paid on time over the last 24 months. Strongest single telco predictor; \u22650.95 is the clean-history signal."
9919
+ },
9920
+ {
9921
+ name: "telcoTenureMonths",
9922
+ type: "number",
9923
+ unit: "months",
9924
+ range: [0, 600],
9925
+ description: "Account age. \u226548 months is the thin-file uplift trigger."
9926
+ },
9927
+ {
9928
+ name: "telcoSuspensionsCount24m",
9929
+ type: "number",
9930
+ unit: "count",
9931
+ range: [0, 100],
9932
+ description: "Non-payment-driven account suspensions in the last 24 months. \u22653 is a strong distress signal."
9933
+ },
9934
+ {
9935
+ name: "telcoLateDays24m",
9936
+ type: "number",
9937
+ unit: "days",
9938
+ range: [0, 800],
9939
+ description: "Cumulative days late across all bills in the trailing 24-month window."
9940
+ },
9941
+ {
9942
+ name: "telcoHandsetFinancingActive",
9943
+ type: "boolean",
9944
+ description: "Has an active handset-EMI account currently. Proxy for financing capacity already extended."
9945
+ },
9946
+ {
9947
+ name: "telcoHandsetFinancingDelinquent",
9948
+ type: "boolean",
9949
+ description: "Recent handset-EMI delinquency in the last 24 months. Distress signal even with otherwise clean bill payment."
9950
+ },
9951
+ {
9952
+ name: "telcoArpuMyr",
9953
+ type: "number",
9954
+ unit: "MYR",
9955
+ range: [0, 1e4],
9956
+ description: "Average Revenue Per User (monthly) in Malaysian Ringgit. Coarse spending-capacity proxy."
9957
+ }
9958
+ ]
9959
+ },
9960
+ {
9961
+ id: "payment-network",
9962
+ displayName: "Payment Network",
9963
+ description: "Merchant-side payment-flow signals from POS / gateway networks (iPay88, GHL, etc.). Captures actual transaction velocity rather than self-reported revenue.",
9964
+ canonicalTable: "ihs_alt_data_payments",
9965
+ fields: [
9966
+ {
9967
+ name: "paymentsMonthlyVolumeMyrT3",
9968
+ type: "number",
9969
+ unit: "MYR",
9970
+ range: [0, 1e8],
9971
+ description: "Mean monthly inbound transaction volume (RM) over the trailing 3 months."
9972
+ },
9973
+ {
9974
+ name: "paymentsMonthlyVolumeMyrT12",
9975
+ type: "number",
9976
+ unit: "MYR",
9977
+ range: [0, 1e8],
9978
+ description: "Mean monthly inbound transaction volume (RM) over the trailing 12 months. Pair with T3 for trend direction."
9979
+ },
9980
+ {
9981
+ name: "paymentsArpuStability12m",
9982
+ type: "number",
9983
+ unit: "ratio",
9984
+ range: [0, 1],
9985
+ description: "Coefficient-of-variation inverse over monthly ARPU in the trailing 12 months. Closer to 1 = steadier; closer to 0 = volatile."
9986
+ },
9987
+ {
9988
+ name: "paymentsDisputeRate12m",
9989
+ type: "number",
9990
+ unit: "ratio",
9991
+ range: [0, 1],
9992
+ description: "Fraction of transactions disputed or refunded in the trailing 12 months."
9993
+ },
9994
+ {
9995
+ name: "paymentsCustomerConcentrationTop5Pct",
9996
+ type: "number",
9997
+ unit: "ratio",
9998
+ range: [0, 1],
9999
+ description: "Revenue share from the top-5 recurring customers. Above ~0.7 is concentration risk."
10000
+ },
10001
+ {
10002
+ name: "paymentsActiveTenureMonths",
10003
+ type: "number",
10004
+ unit: "months",
10005
+ range: [0, 600],
10006
+ description: "Months since first transaction on the payment network. Establishment / continuity proxy."
10007
+ }
10008
+ ]
10009
+ },
10010
+ {
10011
+ id: "bank-statement",
10012
+ displayName: "Bank Statement",
10013
+ description: "Per-month bank-statement extractions. Naturally multi-instance: one statement per (account, month). Eval components typically aggregate across instances (sum closing balance, max debit, count of bounced transactions).",
10014
+ canonicalTable: "ihs_alt_data_bank_statements",
10015
+ fields: [
10016
+ {
10017
+ name: "bankStatementMonth",
10018
+ type: "string",
10019
+ description: "Statement period in YYYY-MM format. Used as the instance_key discriminator + by the `latest` aggregation operator."
10020
+ },
10021
+ {
10022
+ name: "bankClosingBalanceMyr",
10023
+ type: "number",
10024
+ unit: "MYR",
10025
+ range: [-1e8, 1e8],
10026
+ description: "Closing balance for the statement period."
10027
+ },
10028
+ {
10029
+ name: "bankTotalCreditsMyr",
10030
+ type: "number",
10031
+ unit: "MYR",
10032
+ range: [0, 1e8],
10033
+ description: "Sum of credit transactions during the period."
10034
+ },
10035
+ {
10036
+ name: "bankTotalDebitsMyr",
10037
+ type: "number",
10038
+ unit: "MYR",
10039
+ range: [0, 1e8],
10040
+ description: "Sum of debit transactions during the period."
10041
+ },
10042
+ {
10043
+ name: "bankLargestSingleCreditMyr",
10044
+ type: "number",
10045
+ unit: "MYR",
10046
+ range: [0, 1e8],
10047
+ description: "Largest single inbound transaction in the period. Useful for spotting one-off injections vs steady revenue."
10048
+ },
10049
+ {
10050
+ name: "bankBouncedTransactionsCount",
10051
+ type: "number",
10052
+ unit: "count",
10053
+ range: [0, 1e3],
10054
+ description: "Bounced / returned transactions in the period. Distress signal at counts > 0."
10055
+ }
10056
+ ]
10057
+ }
10058
+ ]
10059
+ };
10060
+
10061
+ // src/adapter-categories.ts
10062
+ var data = adapter_categories_default;
10063
+ function categorySchemaOf(id) {
10064
+ const found = data.categories.find((c) => c.id === id);
10065
+ if (!found) {
10066
+ throw new Error(
10067
+ `Unknown adapter category: ${id}. Available: ${data.categories.map((c) => c.id).join(", ")}`
10068
+ );
10069
+ }
10070
+ return found;
10071
+ }
10072
+ function categoryFieldsOf(id) {
10073
+ return categorySchemaOf(id).fields.map((f) => f.name);
10074
+ }
10075
+ function allCategories() {
10076
+ return data.categories;
10077
+ }
10078
+ var fieldToCategory = (() => {
10079
+ const m = /* @__PURE__ */ new Map();
10080
+ for (const cat of data.categories) {
10081
+ for (const f of cat.fields) {
10082
+ m.set(f.name, cat.id);
10083
+ }
10084
+ }
10085
+ return m;
10086
+ })();
10087
+ function categoryForField(field) {
10088
+ return fieldToCategory.get(field) ?? null;
10089
+ }
9839
10090
  // Annotate the CommonJS export names for ESM import in node:
9840
10091
  0 && (module.exports = {
10092
+ ALL_AGGREGATION_OPS,
10093
+ AdapterError,
9841
10094
  BASE_FIELD_SPECS,
9842
10095
  BasicFormField,
9843
10096
  DEFAULT_VALIDATOR_DEFINITIONS,
@@ -9858,8 +10111,13 @@ function resolveExtractionStatus(ihsRecord, jobRecords) {
9858
10111
  IhsStatus,
9859
10112
  IhsValueFormat,
9860
10113
  Role,
10114
+ allCategories,
10115
+ applyAggregation,
9861
10116
  applyDynamicTitles,
9862
10117
  buildFileFieldTables,
10118
+ categoryFieldsOf,
10119
+ categoryForField,
10120
+ categorySchemaOf,
9863
10121
  evaluateExpression,
9864
10122
  extractTimePeriods,
9865
10123
  generateRHFSchema,