{
  "schemaVersion": "1.4.0",
  "categories": [
    {
      "id": "telco-carrier",
      "displayName": "Telco Carrier",
      "description": "Mobile carrier bill-payment + account-history signals. Any mobile carrier implementing this category produces the same canonical field set.",
      "canonicalTable": "ihs_alt_data_telco",
      "fields": [
        {
          "name": "onTimePaymentRatio24m",
          "type": "number",
          "unit": "ratio",
          "range": [
            0,
            1
          ],
          "description": "Fraction of bills paid on time over the last 24 months. Strongest single telco predictor; \u22650.95 is the clean-history signal.",
          "legacyName": "telcoOnTimePaymentRatio24m"
        },
        {
          "name": "tenureMonths",
          "type": "number",
          "unit": "months",
          "range": [
            0,
            600
          ],
          "description": "Account age. \u226548 months is the thin-file uplift trigger.",
          "legacyName": "telcoTenureMonths"
        },
        {
          "name": "suspensionsCount24m",
          "type": "number",
          "unit": "count",
          "range": [
            0,
            100
          ],
          "description": "Non-payment-driven account suspensions in the last 24 months. \u22653 is a strong distress signal.",
          "legacyName": "telcoSuspensionsCount24m"
        },
        {
          "name": "lateDays24m",
          "type": "number",
          "unit": "days",
          "range": [
            0,
            800
          ],
          "description": "Cumulative days late across all bills in the trailing 24-month window.",
          "legacyName": "telcoLateDays24m"
        },
        {
          "name": "handsetFinancingActive",
          "type": "boolean",
          "description": "Has an active handset-EMI account currently. Proxy for financing capacity already extended.",
          "legacyName": "telcoHandsetFinancingActive"
        },
        {
          "name": "handsetFinancingDelinquent",
          "type": "boolean",
          "description": "Recent handset-EMI delinquency in the last 24 months. Distress signal even with otherwise clean bill payment.",
          "legacyName": "telcoHandsetFinancingDelinquent"
        },
        {
          "name": "arpu",
          "type": "number",
          "kind": "money",
          "description": "Average Revenue Per User (monthly). Coarse spending-capacity proxy. The Myr suffix in the field name is legacy: the denomination is carried per value, not fixed by the field. Rename deferred (breaking \u2014 field names are fact ids).",
          "legacyName": "telcoArpuMyr"
        },
        {
          "name": "paymentReliabilityTier",
          "type": "string",
          "kind": "enum",
          "description": "Coarse bill-payment reliability bucket, for partners that return a tier label instead of a continuous ratio. Parallel signal to telcoOnTimePaymentRatio24m. Labels are vendor-declared (see the adapter manifest's enumValues); ordering and scoring live in the consumer's per-value mapping.",
          "legacyName": "telcoPaymentReliabilityTier"
        },
        {
          "name": "tenureTier",
          "type": "string",
          "kind": "enum",
          "description": "Coarse account-age bucket, for partners that return a tenure tier label instead of a continuous month count. Parallel signal to telcoTenureMonths. Labels are vendor-declared (see the adapter manifest's enumValues).",
          "legacyName": "telcoTenureTier"
        },
        {
          "name": "distressTier",
          "type": "string",
          "kind": "enum",
          "description": "Coarse account-distress bucket, for partners that return a distress tier label instead of discrete counts. Parallel signal to telcoSuspensionsCount24m / telcoLateDays24m. Labels are vendor-declared (see the adapter manifest's enumValues).",
          "legacyName": "telcoDistressTier"
        },
        {
          "name": "handsetRiskTier",
          "type": "string",
          "kind": "enum",
          "description": "Coarse handset-financing risk bucket, for partners that return a tier label instead of the discrete handset flags. Parallel signal to telcoHandsetFinancingActive / telcoHandsetFinancingDelinquent. Labels are vendor-declared (see the adapter manifest's enumValues).",
          "legacyName": "telcoHandsetRiskTier"
        }
      ]
    },
    {
      "id": "payment-network",
      "displayName": "Payment Network",
      "description": "Merchant-side payment-flow signals from POS / payment-gateway networks. Captures actual transaction velocity rather than self-reported revenue.",
      "canonicalTable": "ihs_alt_data_payments",
      "fields": [
        {
          "name": "monthlyVolume3m",
          "type": "number",
          "kind": "money",
          "description": "Mean monthly inbound transaction volume over the trailing 3 months. The Myr suffix in the field name is legacy: the denomination is carried per value, not fixed by the field. Rename deferred (breaking \u2014 field names are fact ids).",
          "legacyName": "paymentsMonthlyVolumeMyrT3"
        },
        {
          "name": "monthlyVolume12m",
          "type": "number",
          "kind": "money",
          "description": "Mean monthly inbound transaction volume over the trailing 12 months. Pair with T3 for trend direction. The Myr suffix in the field name is legacy: the denomination is carried per value, not fixed by the field. Rename deferred (breaking \u2014 field names are fact ids).",
          "legacyName": "paymentsMonthlyVolumeMyrT12"
        },
        {
          "name": "arpuStability12m",
          "type": "number",
          "unit": "ratio",
          "range": [
            0,
            1
          ],
          "description": "Coefficient-of-variation inverse over monthly ARPU in the trailing 12 months. Closer to 1 = steadier; closer to 0 = volatile.",
          "legacyName": "paymentsArpuStability12m"
        },
        {
          "name": "disputeRate12m",
          "type": "number",
          "unit": "ratio",
          "range": [
            0,
            1
          ],
          "description": "Fraction of transactions disputed or refunded in the trailing 12 months.",
          "legacyName": "paymentsDisputeRate12m"
        },
        {
          "name": "customerConcentrationTop5Pct",
          "type": "number",
          "unit": "ratio",
          "range": [
            0,
            1
          ],
          "description": "Revenue share from the top-5 recurring customers. Above ~0.7 is concentration risk.",
          "legacyName": "paymentsCustomerConcentrationTop5Pct"
        },
        {
          "name": "activeTenureMonths",
          "type": "number",
          "unit": "months",
          "range": [
            0,
            600
          ],
          "description": "Months since first transaction on the payment network. Establishment / continuity proxy.",
          "legacyName": "paymentsActiveTenureMonths"
        }
      ]
    },
    {
      "id": "bank-statement",
      "displayName": "Bank Statement",
      "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).",
      "canonicalTable": "ihs_alt_data_bank_statements",
      "fields": [
        {
          "name": "statementMonth",
          "type": "string",
          "description": "Statement period in YYYY-MM format. Used as the instance_key discriminator + by the `latest` aggregation operator.",
          "legacyName": "bankStatementMonth"
        },
        {
          "name": "closingBalance",
          "type": "number",
          "kind": "money",
          "description": "Closing balance for the statement period.",
          "fact": "closingBalance",
          "legacyName": "bankClosingBalanceMyr"
        },
        {
          "name": "totalCredits",
          "type": "number",
          "kind": "money",
          "description": "Sum of credit transactions during the period.",
          "fact": "totalCredits",
          "legacyName": "bankTotalCreditsMyr"
        },
        {
          "name": "totalDebits",
          "type": "number",
          "kind": "money",
          "description": "Sum of debit transactions during the period.",
          "fact": "totalDebits",
          "legacyName": "bankTotalDebitsMyr"
        },
        {
          "name": "largestSingleCredit",
          "type": "number",
          "kind": "money",
          "description": "Largest single inbound transaction in the period. Useful for spotting one-off injections vs steady revenue.",
          "legacyName": "bankLargestSingleCreditMyr"
        },
        {
          "name": "bouncedTransactionsCount",
          "type": "number",
          "unit": "count",
          "range": [
            0,
            1000
          ],
          "description": "Bounced / returned transactions in the period. Distress signal at counts > 0.",
          "legacyName": "bankBouncedTransactionsCount"
        }
      ]
    },
    {
      "id": "social-media",
      "displayName": "Social Media Presence",
      "description": "Public business-presence signals from social / commerce platforms \u2014 establishment, reach, engagement authenticity, customer reputation, and account standing. Vendor-agnostic: any platform exposing a public business profile maps its data to this canonical field set. Useful as thin-file corroboration of a borrower's operating reality where formal financials are sparse.",
      "canonicalTable": "ihs_alt_data_social_media",
      "fields": [
        {
          "name": "accountTenureMonths",
          "type": "number",
          "unit": "months",
          "range": [
            0,
            600
          ],
          "description": "Age of the oldest verified public business presence across linked profiles. Establishment / continuity proxy, parallel to telco + payment-network tenure.",
          "legacyName": "socialAccountTenureMonths"
        },
        {
          "name": "followerCount",
          "type": "number",
          "unit": "count",
          "range": [
            0,
            100000000
          ],
          "description": "Aggregate audience size across linked public profiles. Coarse reach / scale proxy \u2014 gameable on its own, so read alongside socialEngagementRate90d.",
          "legacyName": "socialFollowerCount"
        },
        {
          "name": "engagementRate90d",
          "type": "number",
          "unit": "ratio",
          "range": [
            0,
            1
          ],
          "description": "Mean interactions per impression over the trailing 90 days. Authenticity signal: a large follower count with near-zero engagement indicates a bought or dormant audience.",
          "legacyName": "socialEngagementRate90d"
        },
        {
          "name": "postingConsistency12m",
          "type": "number",
          "unit": "ratio",
          "range": [
            0,
            1
          ],
          "description": "Fraction of weeks in the trailing 12 months with at least one public post. Ongoing-operation signal \u2014 distinguishes an active business from a stale listing.",
          "legacyName": "socialPostingConsistency12m"
        },
        {
          "name": "verifiedBusinessAccount",
          "type": "boolean",
          "description": "Has at least one platform-verified business / commerce profile. Legitimacy signal \u2014 the platform has performed its own business-identity check.",
          "legacyName": "socialVerifiedBusinessAccount"
        },
        {
          "name": "customerRatingAvg",
          "type": "number",
          "unit": "rating",
          "range": [
            0,
            5
          ],
          "description": "Mean public customer rating (normalised to a 0\u20135 scale) across review-bearing profiles. Reputation signal; especially predictive for consumer-facing SMEs.",
          "legacyName": "socialCustomerRatingAvg"
        },
        {
          "name": "negativeSentimentRatio90d",
          "type": "number",
          "unit": "ratio",
          "range": [
            0,
            1
          ],
          "description": "Fraction of public mentions / reviews classified as negative over the trailing 90 days. Reputation-risk / distress signal independent of overall rating volume.",
          "legacyName": "socialNegativeSentimentRatio90d"
        },
        {
          "name": "accountFlags24m",
          "type": "number",
          "unit": "count",
          "range": [
            0,
            100
          ],
          "description": "Policy strikes, suspensions, or content takedowns across linked profiles in the last 24 months. Distress signal, parallel to telcoSuspensionsCount24m.",
          "legacyName": "socialAccountFlags24m"
        }
      ]
    },
    {
      "id": "trade-credit",
      "displayName": "Trade Credit (Accounting)",
      "description": "Accounts-receivable / accounts-payable aging and ledger-derived working-capital signals sourced from a business's accounting / ERP system. Captures how promptly the business collects from its debtors and pays its creditors, the aging profile and concentration of its receivables, and P&L / cash-conversion efficiency. Vendor-agnostic: any accounting or ERP system that exposes an AR/AP aging report plus a P&L summary maps its data to this canonical field set. A direct, high-signal view of trade-obligation behaviour \u2014 closer to formal credit history than most alternative data \u2014 and the anchor for cross-referencing self-reported accounting figures against bank-statement reality.",
      "canonicalTable": "ihs_alt_data_trade_credit",
      "fields": [
        {
          "name": "arDaysSalesOutstanding",
          "type": "number",
          "unit": "days",
          "range": [
            0,
            400
          ],
          "description": "Days Sales Outstanding \u2014 average days to collect a receivable. The headline collection-efficiency metric; lower is healthier, and a rising DSO is an early liquidity-stress signal."
        },
        {
          "name": "apDaysPayableOutstanding",
          "type": "number",
          "unit": "days",
          "range": [
            0,
            400
          ],
          "description": "Days Payable Outstanding \u2014 average days the business takes to pay its suppliers. Context for DSO; a very high DPO can indicate the business is stretching its creditors to fund operations."
        },
        {
          "name": "arTotalOutstanding",
          "type": "number",
          "kind": "money",
          "description": "Total accounts-receivable balance outstanding. The size of the debtor book the business is carrying. The Myr suffix in the field name is legacy: the denomination is carried per value, not fixed by the field. Rename deferred (breaking \u2014 field names are fact ids).",
          "legacyName": "arTotalOutstandingMyr"
        },
        {
          "name": "arCurrentRatio",
          "type": "number",
          "unit": "ratio",
          "range": [
            0,
            1
          ],
          "description": "Fraction of the receivables book that is current (within payment terms, not yet overdue). Higher is healthier; the complement is the overdue share across all aging buckets."
        },
        {
          "name": "arOverdue90PlusRatio",
          "type": "number",
          "unit": "ratio",
          "range": [
            0,
            1
          ],
          "description": "Fraction of the receivables book aged more than 90 days past terms. The key impairment / bad-debt risk signal from the AR aging report; elevated values indicate collection difficulty."
        },
        {
          "name": "debtorConcentrationTop5Ratio",
          "type": "number",
          "unit": "ratio",
          "range": [
            0,
            1
          ],
          "description": "Share of total receivables owed by the top-5 debtors. Above ~0.6 is single-customer concentration risk \u2014 one debtor default would materially impair cash flow."
        },
        {
          "name": "tradeReferenceDefaults12m",
          "type": "number",
          "unit": "count",
          "range": [
            0,
            1000
          ],
          "description": "Count of supplier-reported defaults or dishonoured / returned payments against the business in the trailing 12 months. Direct distress signal, parallel to a bureau default record."
        },
        {
          "name": "accountingRevenue12m",
          "type": "number",
          "kind": "money",
          "description": "Trailing-12-month turnover per the general ledger. The cross-reference anchor: comparing this self-reported accounting revenue against payment-network volume and bank-statement inflows surfaces book-vs-reality inconsistencies. The Myr suffix in the field name is legacy: the denomination is carried per value, not fixed by the field. Rename deferred (breaking \u2014 field names are fact ids).",
          "legacyName": "accountingRevenue12mMyr"
        },
        {
          "name": "grossMarginPct",
          "type": "number",
          "unit": "ratio",
          "range": [
            0,
            1
          ],
          "description": "Gross margin from the P&L (gross profit / revenue). Profitability-quality signal; a thin or declining margin constrains debt-service capacity even at healthy revenue."
        },
        {
          "name": "cashConversionCycleDays",
          "type": "number",
          "unit": "days",
          "range": [
            -200,
            600
          ],
          "description": "Cash Conversion Cycle (DSO + days-inventory-outstanding \u2212 DPO). Working-capital efficiency; negative values (collect before paying suppliers) are strongest, very high values indicate cash tied up in operations."
        }
      ]
    },
    {
      "id": "geolocation",
      "displayName": "Geolocation",
      "description": "Hourly-granularity movement track plus derived mobility signals for an applicant, sourced from any location-capable provider (telco network location, mobile-SDK GPS, GIS / address-verification services). Two instance kinds share this category: point instances (instanceKey 'pt:<ISO-hour>') carry one observed position per hourly bucket, mirroring the bank-statement multi-instance pattern; a single summary instance (instanceKey 'summary') carries adapter-derived behaviour signals \u2014 most importantly work-pattern regularity (sustained weekday dwell at a stable work anchor corroborates income reliability), residential stability, vacation windows, and dwell in operator-flagged hotspot zones. Vendor-agnostic: the source classifies or the adapter infers place labels; no provider-specific semantics leak into the field set. Raw coordinates are sensitive personal data \u2014 product-plane persistence is gated on PDPA consent + CRA Act 710 s25 retention review.",
      "canonicalTable": "ihs_alt_data_geolocation",
      "fields": [
        {
          "name": "latitude",
          "type": "number",
          "unit": "deg",
          "range": [
            -90,
            90
          ],
          "description": "Observed latitude for this hourly bucket (point instances only).",
          "legacyName": "geoLatitude"
        },
        {
          "name": "longitude",
          "type": "number",
          "unit": "deg",
          "range": [
            -180,
            180
          ],
          "description": "Observed longitude for this hourly bucket (point instances only).",
          "legacyName": "geoLongitude"
        },
        {
          "name": "accuracyMeters",
          "type": "number",
          "unit": "meters",
          "range": [
            0,
            100000
          ],
          "description": "Source-reported horizontal accuracy radius of the observation. Cell-tower fixes are typically hundreds of meters; GPS fixes tens.",
          "legacyName": "geoAccuracyM"
        },
        {
          "name": "bucket",
          "type": "string",
          "description": "ISO-8601 hour bucket of the observation, e.g. '2026-06-01T08'. Redundant with the instance key (pt:<bucket>) so consumers can query without parsing keys.",
          "legacyName": "geoBucket"
        },
        {
          "name": "placeLabel",
          "type": "string",
          "description": "Classified place for the bucket: home | work | commute | leisure | travel | hotspot | other. Classified by the source or inferred by the adapter from anchor dwell.",
          "legacyName": "geoPlaceLabel"
        },
        {
          "name": "workAttendanceRatio30d",
          "type": "number",
          "unit": "ratio",
          "range": [
            0,
            1
          ],
          "description": "Fraction of the last 30 weekdays with >= 6 hours of dwell at the inferred work anchor. The income-reliability headline: regular full-time work patterns corroborate declared employment income (summary instance only).",
          "legacyName": "geoWorkAttendanceRatio30d"
        },
        {
          "name": "workDailyHoursAvg30d",
          "type": "number",
          "unit": "hours",
          "range": [
            0,
            24
          ],
          "description": "Mean daily hours of work-anchor dwell across the last 30 weekdays (summary instance only).",
          "legacyName": "geoWorkDailyHoursAvg30d"
        },
        {
          "name": "locationStabilityScore",
          "type": "number",
          "unit": "score",
          "range": [
            0,
            1
          ],
          "description": "Residential stability: share of nights spent at the primary home anchor over the observation window. 1 = every night at home (summary instance only).",
          "legacyName": "geoLocationStabilityScore"
        },
        {
          "name": "commuteRegularityRatio",
          "type": "number",
          "unit": "ratio",
          "range": [
            0,
            1
          ],
          "description": "Consistency of the weekday home->work->home rhythm: fraction of weekdays matching the dominant commute pattern within an hour's tolerance (summary instance only).",
          "legacyName": "geoCommuteRegularityRatio"
        },
        {
          "name": "vacationDays90d",
          "type": "number",
          "unit": "days",
          "range": [
            0,
            90
          ],
          "description": "Days in the last 90 spent fully away from both home and work anchors (contiguous travel windows; summary instance only).",
          "legacyName": "geoVacationDays90d"
        },
        {
          "name": "hotspotDwellRatio",
          "type": "number",
          "unit": "ratio",
          "range": [
            0,
            1
          ],
          "description": "Share of observed buckets spent inside operator-flagged hotspot zones (known fraud / illicit-activity locations supplied to the adapter). Non-zero values warrant review, not automatic decline (summary instance only).",
          "legacyName": "geoHotspotDwellRatio"
        },
        {
          "name": "primaryStateCode",
          "type": "string",
          "description": "Malaysian state / federal-territory code of the primary home anchor, e.g. PNG, KUL, JHR (summary instance only).",
          "legacyName": "geoPrimaryStateCode"
        },
        {
          "name": "addressMatchScore",
          "type": "number",
          "unit": "score",
          "range": [
            0,
            1
          ],
          "description": "Agreement between the inferred home anchor and the applicant's registered residential address. 1 = same premises, 0 = different state (summary instance only).",
          "legacyName": "geoAddressMatchScore"
        }
      ]
    },
    {
      "id": "person-identity",
      "displayName": "Identity Document (IC / Passport)",
      "description": "Identity fields extracted from a Malaysian IC (MyKad) or passport document. Produced by the host's document-extraction pipeline; one instance per identity document.",
      "canonicalTable": "ihs_alt_data_ic",
      "fields": [
        {
          "name": "personName",
          "type": "string",
          "description": "Full name as printed on the identity document.",
          "fact": "personName",
          "legacyName": "icName"
        },
        {
          "name": "personIdNumber",
          "type": "string",
          "description": "IC (MyKad) number, or passport number for passport documents.",
          "fact": "personIdNumber",
          "legacyName": "icNumber"
        },
        {
          "name": "personAddress",
          "type": "string",
          "description": "Registered address as printed on the document.",
          "fact": "personAddress",
          "legacyName": "icAddress"
        },
        {
          "name": "personGender",
          "type": "string",
          "description": "Gender as stated on the document.",
          "fact": "personGender",
          "legacyName": "icGender"
        },
        {
          "name": "personReligion",
          "type": "string",
          "description": "Religion as stated on the document (MyKad-only; absent on passports).",
          "fact": "personReligion",
          "legacyName": "icReligion"
        },
        {
          "name": "personDateOfBirth",
          "type": "string",
          "description": "Date of birth as stated on the document.",
          "fact": "personDateOfBirth",
          "legacyName": "icDateOfBirth"
        },
        {
          "name": "personPlaceOfBirth",
          "type": "string",
          "description": "Place of birth as stated on the document.",
          "fact": "personPlaceOfBirth",
          "legacyName": "icPlaceOfBirth"
        },
        {
          "name": "personNationality",
          "type": "string",
          "description": "Nationality as stated on the document.",
          "fact": "personNationality",
          "legacyName": "icNationality"
        },
        {
          "name": "personRace",
          "type": "string",
          "description": "Race as stated on the document (MyKad-only; absent on passports).",
          "fact": "personRace",
          "legacyName": "icRace"
        }
      ],
      "legacyId": "ic"
    },
    {
      "id": "finxtract-bank-statement",
      "displayName": "Bank Statement (Document Extraction)",
      "description": "Fields extracted from an uploaded bank-statement document by the host's extraction pipeline. One instance per statement document; each statement covers a single month. Distinct from the partner-API 'bank-statement' category: same real-world domain, different source and different canonical vocabulary.",
      "canonicalTable": "ihsbankstatement",
      "fields": [
        {
          "name": "issuingBankName",
          "type": "string",
          "description": "Issuing bank's name as printed on the statement.",
          "legacyName": "bankName"
        },
        {
          "name": "personName",
          "type": "string",
          "description": "Account holder's name as printed on the statement.",
          "fact": "personName",
          "legacyName": "accountHolderName"
        },
        {
          "name": "accountNumber",
          "type": "string",
          "description": "Bank account number as printed on the statement."
        },
        {
          "name": "statementDate",
          "type": "string",
          "description": "Statement date, normalized to ISO YYYY-MM-DD."
        },
        {
          "name": "openingBalance",
          "type": "number",
          "kind": "money",
          "description": "Opening balance for the statement period."
        },
        {
          "name": "closingBalance",
          "type": "number",
          "kind": "money",
          "description": "Closing balance for the statement period.",
          "fact": "closingBalance",
          "legacyName": "bankBalance"
        },
        {
          "name": "totalCredits",
          "type": "number",
          "kind": "money",
          "description": "Sum of credit transactions in the statement period.",
          "fact": "totalCredits"
        },
        {
          "name": "totalDebits",
          "type": "number",
          "kind": "money",
          "description": "Sum of debit transactions in the statement period.",
          "fact": "totalDebits"
        }
      ]
    },
    {
      "id": "epf-statement",
      "displayName": "EPF Statement (Document Extraction)",
      "description": "Fields extracted from an uploaded EPF (KWSP) statement document by the host's extraction pipeline. One instance per statement document. Field names carry the 'epf' prefix \u2014 the same prefix-namespaced vocabulary the host's flat read path and the eval engine already use, which also keeps canonical names globally unique across categories.",
      "canonicalTable": "ihsepfstatement",
      "fields": [
        {
          "name": "personName",
          "type": "string",
          "description": "Member's name as printed on the statement.",
          "fact": "personName",
          "legacyName": "epfAccountHolderName"
        },
        {
          "name": "personAddress",
          "type": "string",
          "description": "Member's address as printed on the statement.",
          "fact": "personAddress",
          "legacyName": "epfAccountHolderAddress"
        },
        {
          "name": "statementYear",
          "type": "string",
          "description": "The contribution year the statement covers.",
          "legacyName": "epfStatementYear"
        },
        {
          "name": "kwspMemberNumber",
          "type": "string",
          "description": "KWSP membership number.",
          "legacyName": "epfKwspMemberNumber"
        },
        {
          "name": "personIdNumber",
          "type": "string",
          "description": "Member's IC number as printed on the statement.",
          "fact": "personIdNumber",
          "legacyName": "epfIcNumber"
        },
        {
          "name": "employerNumber",
          "type": "string",
          "description": "Employer's KWSP reference number.",
          "legacyName": "epfEmployerNumber"
        },
        {
          "name": "dateOfStatement",
          "type": "string",
          "description": "Statement issue date, normalized to ISO YYYY-MM-DD.",
          "legacyName": "epfDateOfStatement"
        },
        {
          "name": "totalContribution",
          "type": "number",
          "kind": "money",
          "description": "Cumulative EPF balance as at the statement date.",
          "legacyName": "epfTotalContribution"
        },
        {
          "name": "totalCurrentContribution",
          "type": "number",
          "kind": "money",
          "description": "Contributions made within the statement's coverage year.",
          "legacyName": "epfTotalCurrentContribution"
        }
      ],
      "legacyId": "finxtract-epf"
    },
    {
      "id": "payslip",
      "displayName": "Payslip (Document Extraction)",
      "description": "Fields extracted from an uploaded payslip document by the host's extraction pipeline. One instance per payslip document; each payslip covers a single pay period. Field names carry the 'payslip' prefix \u2014 the same prefix-namespaced vocabulary the host's flat read path and the eval engine already use, which also keeps canonical names globally unique across categories.",
      "canonicalTable": "ihsPayslip",
      "fields": [
        {
          "name": "employerName",
          "type": "string",
          "description": "Employer's name as printed on the payslip.",
          "fact": "employerName",
          "legacyName": "payslipEmployerName"
        },
        {
          "name": "personName",
          "type": "string",
          "description": "Employee's name as printed on the payslip.",
          "fact": "personName",
          "legacyName": "payslipEmployeeName"
        },
        {
          "name": "payPeriod",
          "type": "string",
          "description": "The pay period the payslip covers, as printed.",
          "legacyName": "payslipPayPeriod"
        },
        {
          "name": "payDate",
          "type": "string",
          "description": "Payment date, as printed.",
          "legacyName": "payslipPayDate"
        },
        {
          "name": "basicPay",
          "type": "number",
          "kind": "money",
          "description": "Basic salary for the period.",
          "legacyName": "payslipBasicPay"
        },
        {
          "name": "grossPay",
          "type": "number",
          "kind": "money",
          "description": "Gross pay for the period.",
          "fact": "grossPay",
          "legacyName": "payslipGrossPay"
        },
        {
          "name": "fixedAllowances",
          "type": "number",
          "kind": "money",
          "description": "Fixed allowances for the period.",
          "legacyName": "payslipFixedAllowances"
        },
        {
          "name": "variableIncome",
          "type": "number",
          "kind": "money",
          "description": "Variable income (overtime, commission, bonus) for the period.",
          "legacyName": "payslipVariableIncome"
        },
        {
          "name": "deduction",
          "type": "number",
          "kind": "money",
          "description": "Total statutory deductions for the period.",
          "legacyName": "payslipDeduction"
        },
        {
          "name": "otherDeduction",
          "type": "number",
          "kind": "money",
          "description": "Other (non-statutory) deductions for the period.",
          "legacyName": "payslipOtherDeduction"
        },
        {
          "name": "employeeEpfContribution",
          "type": "number",
          "kind": "money",
          "description": "Employee EPF contribution for the period.",
          "legacyName": "payslipEmployeeEpfContribution"
        },
        {
          "name": "employeeSocsoContribution",
          "type": "number",
          "kind": "money",
          "description": "Employee SOCSO contribution for the period.",
          "legacyName": "payslipEmployeeSocsoContribution"
        },
        {
          "name": "employeeEisContribution",
          "type": "number",
          "kind": "money",
          "description": "Employee EIS contribution for the period.",
          "legacyName": "payslipEmployeeEisContribution"
        },
        {
          "name": "employeeTax",
          "type": "number",
          "kind": "money",
          "description": "Employee income-tax (PCB) deduction for the period.",
          "legacyName": "payslipEmployeeTax"
        },
        {
          "name": "netPay",
          "type": "number",
          "kind": "money",
          "description": "Net pay for the period.",
          "fact": "netPay",
          "legacyName": "payslipNetPay"
        }
      ],
      "legacyId": "finxtract-payslip"
    },
    {
      "id": "financial-statement",
      "displayName": "Financial Statement (Document Extraction)",
      "description": "Fields extracted from an uploaded audited financial-statement document by the host's extraction pipeline. One document = one audited financial statement carrying TWO declared periods per the period-axis contract: period1 is its current fiscal year, period2 is its prior comparative year. Metric names are bare in the flat vocabulary, like the bank-statement extraction category's.",
      "canonicalTable": "ihsfinancialstatement",
      "fields": [
        {
          "name": "localNo",
          "type": "string",
          "description": "Company registration (local) number as stated on the document."
        },
        {
          "name": "companyName",
          "type": "string",
          "fact": "companyName",
          "description": "Company name as stated on the document. Attestation of the shared companyName fact \u2014 the SSM Form 9 extraction category attests the same fact from its own document."
        },
        {
          "name": "financialYearEnd",
          "type": "string",
          "description": "Financial year end stated by the source document, normalized to ISO YYYY-MM-DD. Shared verbatim by every period the document carries \u2014 period identity comes from declared position, never from this date."
        },
        {
          "name": "consolidated",
          "type": "boolean",
          "description": "Whether the statement reports consolidated (group-level) accounts."
        },
        {
          "name": "currency",
          "type": "string",
          "description": "Reporting currency stated on the document."
        },
        {
          "name": "year",
          "type": "number",
          "description": "1-based ordinal of this document within the applicant's uploaded statement sequence; document 1 covers the most recent fiscal years."
        },
        {
          "name": "nonCurrentAssetsSubsidiaryCompanies",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: subsidiary companies."
        },
        {
          "name": "nonCurrentAssetsAssociatedCompanies",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: associated companies."
        },
        {
          "name": "nonCurrentAssetsDevelopmentPropertiesOrExpenditure",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: development properties or expenditure."
        },
        {
          "name": "nonCurrentAssetInvestmentProperties",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: investment properties."
        },
        {
          "name": "nonCurrentAssetsInvestments",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: investments."
        },
        {
          "name": "nonCurrentAssetInvestmentSecurities",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: investment securities."
        },
        {
          "name": "nonCurrentAssetDeferredAssets",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: deferred assets."
        },
        {
          "name": "nonCurrentAssetDeposits",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: deposits."
        },
        {
          "name": "nonCurrentAssetOwnGoodwill",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: own goodwill."
        },
        {
          "name": "nonCurrentAssetGoodwillOnConsolidation",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: goodwill on consolidation."
        },
        {
          "name": "nonCurrentAssetSinkingFund",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: sinking fund."
        },
        {
          "name": "nonCurrentAssetPropertyPlantEquipment",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: property plant equipment."
        },
        {
          "name": "nonCurrentAssetInventories",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: inventories."
        },
        {
          "name": "nonCurrentAssetRightOfUseAssets",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: right of use assets."
        },
        {
          "name": "nonCurrentAssetIntangibleAssets",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: intangible assets."
        },
        {
          "name": "nonCurrentAssetPrepaidLeasePayments",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: prepaid lease payments."
        },
        {
          "name": "nonCurrentAssetFinancialAssetAtFairValue",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: financial asset at fair value."
        },
        {
          "name": "nonCurrentAssetLandUseRight",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: land use right."
        },
        {
          "name": "nonCurrentAssetTradeReceivables",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: trade receivables."
        },
        {
          "name": "nonCurrentAssetOtherReceivables",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: other receivables."
        },
        {
          "name": "nonCurrentAssetTradeAndOtherReceivables",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: trade and other receivables."
        },
        {
          "name": "nonCurrentAssetOtherReceivablesDepositsAndPrepayments",
          "type": "number",
          "kind": "money",
          "description": "Non-current assets: other receivables deposits and prepayments."
        },
        {
          "name": "totalNonCurrentAssets",
          "type": "number",
          "kind": "money",
          "description": "Balance-sheet total: non-current assets."
        },
        {
          "name": "currentAssetDevelopmentPropertiesOrExpenditure",
          "type": "number",
          "kind": "money",
          "description": "Current assets: development properties or expenditure."
        },
        {
          "name": "currentAssetShortTermInvestments",
          "type": "number",
          "kind": "money",
          "description": "Current assets: short term investments."
        },
        {
          "name": "currentAssetTradeDebtors",
          "type": "number",
          "kind": "money",
          "description": "Current assets: trade debtors."
        },
        {
          "name": "currentAssetShortTermDeposits",
          "type": "number",
          "kind": "money",
          "description": "Current assets: short term deposits."
        },
        {
          "name": "currentAssetAmountDueFromHoldingCompany",
          "type": "number",
          "kind": "money",
          "description": "Current assets: amount due from holding company."
        },
        {
          "name": "currentAssetAmountDueFromSubsidiaryCompanies",
          "type": "number",
          "kind": "money",
          "description": "Current assets: amount due from subsidiary companies."
        },
        {
          "name": "currentAssetAmountDueFromAssociatedCompanies",
          "type": "number",
          "kind": "money",
          "description": "Current assets: amount due from associated companies."
        },
        {
          "name": "currentAssetAmountDueFromDirector",
          "type": "number",
          "kind": "money",
          "description": "Current assets: amount due from director."
        },
        {
          "name": "currentAssetAmountDueFromCustomer",
          "type": "number",
          "kind": "money",
          "description": "Current assets: amount due from customer."
        },
        {
          "name": "currentAssetAmountOwingByShareholders",
          "type": "number",
          "kind": "money",
          "description": "Current assets: amount owing by shareholders."
        },
        {
          "name": "currentAssetInventories",
          "type": "number",
          "kind": "money",
          "description": "Current assets: inventories."
        },
        {
          "name": "currentAssetFinancialAssetAtFairValue",
          "type": "number",
          "kind": "money",
          "description": "Current assets: financial asset at fair value."
        },
        {
          "name": "currentAssetLandUseRight",
          "type": "number",
          "kind": "money",
          "description": "Current assets: land use right."
        },
        {
          "name": "currentAssetTaxRecoverable",
          "type": "number",
          "kind": "money",
          "description": "Current assets: tax recoverable."
        },
        {
          "name": "currentAssetContractAssets",
          "type": "number",
          "kind": "money",
          "description": "Current assets: contract assets."
        },
        {
          "name": "currentAssetCurrentTaxAssets",
          "type": "number",
          "kind": "money",
          "description": "Current assets: current tax assets."
        },
        {
          "name": "currentAssetPrepayments",
          "type": "number",
          "kind": "money",
          "description": "Current assets: prepayments."
        },
        {
          "name": "currentAssetDerivative",
          "type": "number",
          "kind": "money",
          "description": "Current assets: derivative."
        },
        {
          "name": "currentAssetCashAndBankBalances",
          "type": "number",
          "kind": "money",
          "description": "Current assets: cash and bank balances."
        },
        {
          "name": "currentAssetOtherReceivables",
          "type": "number",
          "kind": "money",
          "description": "Current assets: other receivables."
        },
        {
          "name": "currentAssetRestrictedCash",
          "type": "number",
          "kind": "money",
          "description": "Current assets: restricted cash."
        },
        {
          "name": "currentAssetCashAndCashEquivalents",
          "type": "number",
          "kind": "money",
          "description": "Current assets: cash and cash equivalents."
        },
        {
          "name": "currentAssetDepositsCashAndBankBalances",
          "type": "number",
          "kind": "money",
          "description": "Current assets: deposits cash and bank balances."
        },
        {
          "name": "currentAssetCashAtBanks",
          "type": "number",
          "kind": "money",
          "description": "Current assets: cash at banks."
        },
        {
          "name": "currentAssetCashOnHand",
          "type": "number",
          "kind": "money",
          "description": "Current assets: cash on hand."
        },
        {
          "name": "currentAssetDepositsAndPrepayments",
          "type": "number",
          "kind": "money",
          "description": "Current assets: deposits and prepayments."
        },
        {
          "name": "currentAssetTradeAndOtherReceivables",
          "type": "number",
          "kind": "money",
          "description": "Current assets: trade and other receivables."
        },
        {
          "name": "currentAssetOtherReceivablesDepositsAndPrepayments",
          "type": "number",
          "kind": "money",
          "description": "Current assets: other receivables deposits and prepayments."
        },
        {
          "name": "currentAssetOtherReceivablesAndDeposits",
          "type": "number",
          "kind": "money",
          "description": "Current assets: other receivables and deposits."
        },
        {
          "name": "currentAssetOtherReceivablesAndPrepayments",
          "type": "number",
          "kind": "money",
          "description": "Current assets: other receivables and prepayments."
        },
        {
          "name": "totalCurrentAssets",
          "type": "number",
          "kind": "money",
          "description": "Balance-sheet total: current assets."
        },
        {
          "name": "totalAssets",
          "type": "number",
          "kind": "money",
          "description": "Balance-sheet total: assets."
        },
        {
          "name": "tangibleAssets",
          "type": "number",
          "kind": "money",
          "description": "Balance-sheet total: tangible assets — the statement's own total-tangible-assets line where it declares one, otherwise total assets less intangible assets. Not the complement of nonCurrentAssetIntangibleAssets by name: that field is one intangible line item, this is an assets aggregate."
        },
        {
          "name": "currentLiabilitiesTradeCreditors",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: trade creditors."
        },
        {
          "name": "currentLiabilitiesHirePurchasePayables",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: hire purchase payables."
        },
        {
          "name": "currentLiabilitiesBankOverdraft",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: bank overdraft."
        },
        {
          "name": "currentLiabilitiesBankAcceptance",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: bank acceptance."
        },
        {
          "name": "currentLiabilitiesBorrowingsOrTermLoans",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: borrowings or term loans."
        },
        {
          "name": "currentLiabilitiesBankborrowings",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: bank borrowings."
        },
        {
          "name": "currentLiabilitiesAmountsOwingToHoldingCompany",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: amounts owing to holding company."
        },
        {
          "name": "currentLiabilitiesAmountsOwingToSubsidiaryCompanies",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: amounts owing to subsidiary companies."
        },
        {
          "name": "currentLiabilitiesAmountOwingToAssociatedCompanies",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: amount owing to associated companies."
        },
        {
          "name": "currentLiabilitiesAmountsOwingToDirector",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: amounts owing to director."
        },
        {
          "name": "currentLiabilitiesProvisionForTaxation",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: provision for taxation."
        },
        {
          "name": "currentLiabilitiesLeasePayables",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: lease payables."
        },
        {
          "name": "currentLiabilitiesFinanceLeasePayables",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: finance lease payables."
        },
        {
          "name": "currentLiabilitiesOtherPayables",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: other payables."
        },
        {
          "name": "currentLiabilitiesDividendPayables",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: dividend payables."
        },
        {
          "name": "currentLiabilitiesTradeAndOtherPayables",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: trade and other payables."
        },
        {
          "name": "currentLiabilitiesDepositsFromCustomers",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: deposits from customers."
        },
        {
          "name": "currentLiabilitiesOtherPayablesAccrualsAndDeposits",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: other payables accruals and deposits."
        },
        {
          "name": "currentLiabilitiesOtherPayablesAndAccruals",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: other payables and accruals."
        },
        {
          "name": "currentLiabilitiesContractLiabilities",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: contract liabilities."
        },
        {
          "name": "currentLiabilitiesDerivative",
          "type": "number",
          "kind": "money",
          "description": "Current liabilities: derivative."
        },
        {
          "name": "totalCurrentLiabilities",
          "type": "number",
          "kind": "money",
          "description": "Balance-sheet total: current liabilities."
        },
        {
          "name": "shortTermLiabilities",
          "type": "number",
          "kind": "money",
          "description": "Balance-sheet total: short-term liabilities."
        },
        {
          "name": "nonCurrentLiabilitiesBorrowingsOrTermLoans",
          "type": "number",
          "kind": "money",
          "description": "Non-current liabilities: borrowings or term loans."
        },
        {
          "name": "nonCurrentLiabilitiesBankBorrowings",
          "type": "number",
          "kind": "money",
          "description": "Non-current liabilities: bank borrowings."
        },
        {
          "name": "nonCurrentLiabilitiesLeaseObligations",
          "type": "number",
          "kind": "money",
          "description": "Non-current liabilities: lease obligations."
        },
        {
          "name": "nonCurrentLiabilitiesFinanceLeaseObligations",
          "type": "number",
          "kind": "money",
          "description": "Non-current liabilities: finance lease obligations."
        },
        {
          "name": "nonCurrentLiabilitiesDeferredTaxation",
          "type": "number",
          "kind": "money",
          "description": "Non-current liabilities: deferred taxation."
        },
        {
          "name": "nonCurrentLiabilitiesRetirementBenefits",
          "type": "number",
          "kind": "money",
          "description": "Non-current liabilities: retirement benefits."
        },
        {
          "name": "nonCurrentLiabilitiesHirePurchasePayables",
          "type": "number",
          "kind": "money",
          "description": "Non-current liabilities: hire purchase payables."
        },
        {
          "name": "nonCurrentLiabilitiesDepositsFromCustomers",
          "type": "number",
          "kind": "money",
          "description": "Non-current liabilities: deposits from customers."
        },
        {
          "name": "nonCurrentLiabilitiesContractLiabilities",
          "type": "number",
          "kind": "money",
          "description": "Non-current liabilities: contract liabilities."
        },
        {
          "name": "totalNonCurrentLiabilities",
          "type": "number",
          "kind": "money",
          "description": "Balance-sheet total: non-current liabilities."
        },
        {
          "name": "totalLiabilities",
          "type": "number",
          "kind": "money",
          "description": "Balance-sheet total: liabilities."
        },
        {
          "name": "shareCapital",
          "type": "number",
          "kind": "money",
          "description": "Equity: share capital."
        },
        {
          "name": "preferenceShareCapital",
          "type": "number",
          "kind": "money",
          "description": "Equity: preference share capital."
        },
        {
          "name": "sharePremium",
          "type": "number",
          "kind": "money",
          "description": "Equity: share premium."
        },
        {
          "name": "reserves",
          "type": "number",
          "kind": "money",
          "description": "Equity: reserves."
        },
        {
          "name": "capitalReserves",
          "type": "number",
          "kind": "money",
          "description": "Equity: capital reserves."
        },
        {
          "name": "revaluationReserves",
          "type": "number",
          "kind": "money",
          "description": "Equity: revaluation reserves."
        },
        {
          "name": "exchangeEqualisationOrFluctuationReserves",
          "type": "number",
          "kind": "money",
          "description": "Equity: exchange equalisation or fluctuation reserves."
        },
        {
          "name": "retainedEarnings",
          "type": "number",
          "kind": "money",
          "description": "Equity: retained earnings."
        },
        {
          "name": "totalEquity",
          "type": "number",
          "kind": "money",
          "description": "Balance-sheet total: equity."
        },
        {
          "name": "revenue",
          "type": "number",
          "kind": "money",
          "description": "Income statement: revenue."
        },
        {
          "name": "costOfGoodsSold",
          "type": "number",
          "kind": "money",
          "description": "Income statement: cost of goods sold."
        },
        {
          "name": "grossProfit",
          "type": "number",
          "kind": "money",
          "description": "Income statement: gross profit."
        },
        {
          "name": "otherIncome",
          "type": "number",
          "kind": "money",
          "description": "Income statement: other income."
        },
        {
          "name": "profitFromOperations",
          "type": "number",
          "kind": "money",
          "description": "Income statement: profit from operations."
        },
        {
          "name": "financeCost",
          "type": "number",
          "kind": "money",
          "description": "Income statement: finance cost."
        },
        {
          "name": "ebitda",
          "type": "number",
          "kind": "money",
          "description": "Income statement: profit before tax (extracted from the statement's profit-before-tax line; canonical name kept verbatim from the host vocabulary)."
        },
        {
          "name": "taxes",
          "type": "number",
          "kind": "money",
          "description": "Income statement: taxes."
        },
        {
          "name": "zakat",
          "type": "number",
          "kind": "money",
          "description": "Income statement: zakat."
        },
        {
          "name": "profitAfterTax",
          "type": "number",
          "kind": "money",
          "description": "Income statement: profit after tax."
        },
        {
          "name": "netProfit",
          "type": "number",
          "kind": "money",
          "description": "Income statement: total comprehensive income for the financial year."
        },
        {
          "name": "netOperatingCashFlow",
          "type": "number",
          "kind": "money",
          "description": "Cash-flow statement: net cash flow used in / generated from operating activities."
        },
        {
          "name": "depreciation",
          "type": "number",
          "kind": "money",
          "description": "Cash-flow statement: depreciation."
        },
        {
          "name": "depreciationOfPropertyPlantEquipment",
          "type": "number",
          "kind": "money",
          "description": "Cash-flow statement: depreciation of property plant equipment."
        },
        {
          "name": "depreciationRightOfUseAssets",
          "type": "number",
          "kind": "money",
          "description": "Cash-flow statement: depreciation right of use assets."
        },
        {
          "name": "depreciationOfInvestmentProperties",
          "type": "number",
          "kind": "money",
          "description": "Cash-flow statement: depreciation of investment properties."
        },
        {
          "name": "depreciationOfInvestmentSecurities",
          "type": "number",
          "kind": "money",
          "description": "Cash-flow statement: depreciation of investment securities."
        },
        {
          "name": "endOfYearCash",
          "type": "number",
          "kind": "money",
          "description": "Cash-flow statement: cash and cash equivalents at the end of the financial year."
        }
      ],
      "legacyId": "finxtract-financial-statement"
    },
    {
      "id": "company-registration",
      "displayName": "SSM Form 9 (Document Extraction)",
      "description": "Fields extracted from an uploaded SSM Form 9 (Certificate of Incorporation of Private Company) document by the host's extraction pipeline. One instance per uploaded document. The company name and incorporation date are attestations of shared facts \u2014 the company-profile and financial-statement extraction categories attest the same real-world facts from their own documents.",
      "canonicalTable": "ihs_alt_data_form9",
      "fields": [
        {
          "name": "companyName",
          "type": "string",
          "fact": "companyName",
          "description": "Company name as printed on the certificate. Attestation of the shared companyName fact."
        },
        {
          "name": "companyRegNo",
          "type": "string",
          "description": "Company registration number as printed on the certificate.",
          "fact": "companyRegNo"
        },
        {
          "name": "companyIncorporationDate",
          "type": "string",
          "fact": "companyIncorporationDate",
          "description": "Date of incorporation as printed on the certificate, normalized to ISO YYYY-MM-DD. Attestation of the shared companyIncorporationDate fact \u2014 a company has exactly one incorporation date regardless of which document it was extracted from."
        }
      ],
      "legacyId": "finxtract-form9"
    },
    {
      "id": "company-profile",
      "displayName": "SSM Company Profile (Document Extraction)",
      "description": "Fields extracted from an uploaded SSM company-profile document (company or business registration extract) by the host's extraction pipeline. One instance per uploaded document. Covers registration identity, status, key dates, registered address, capital, and the officer/shareholder registers. The incorporation date is an attestation of a shared fact \u2014 the Form 9 extraction category attests the same real-world fact from its own document.",
      "canonicalTable": "ihs_alt_data_ssm",
      "fields": [
        {
          "name": "companyEntityType",
          "type": "string",
          "description": "Entity type stated on the profile (e.g. private limited company, enterprise).",
          "legacyName": "ssmCompanyEntityType"
        },
        {
          "name": "companyName",
          "type": "string",
          "description": "Business or company name as stated on the profile.",
          "fact": "companyName"
        },
        {
          "name": "companyRegNo",
          "type": "string",
          "description": "Business or company registration number as stated on the profile.",
          "fact": "companyRegNo"
        },
        {
          "name": "companyStatus",
          "type": "string",
          "description": "Registration status as stated on the profile (e.g. existing, dissolved)."
        },
        {
          "name": "businessOrigin",
          "type": "string",
          "description": "Business origin as stated on the profile."
        },
        {
          "name": "companyIncorporationDate",
          "type": "string",
          "fact": "companyIncorporationDate",
          "description": "Incorporation (or registration) date as stated on the profile, normalized to ISO YYYY-MM-DD. Attestation of the shared companyIncorporationDate fact \u2014 a company has exactly one incorporation date regardless of which document it was extracted from."
        },
        {
          "name": "businessCommencementDate",
          "type": "string",
          "description": "Business commencement (or start) date as stated on the profile, normalized to ISO YYYY-MM-DD."
        },
        {
          "name": "businessNature",
          "type": "string",
          "description": "Nature of business as stated on the profile."
        },
        {
          "name": "registeredAddress",
          "type": "string",
          "description": "Registered business address as stated on the profile."
        },
        {
          "name": "totalShareIssued",
          "type": "number",
          "description": "Total shares issued as stated on the profile."
        },
        {
          "name": "paidUpCapital",
          "type": "number",
          "kind": "money",
          "description": "Paid-up capital as stated on the profile (falls back to total shares issued when the profile omits an explicit paid-up figure).",
          "legacyName": "ssmPaidUpCapital"
        },
        {
          "name": "directors",
          "type": "string",
          "description": "JSON-encoded list of current directors/officers (for enterprises, the business owner normalized into the same shape)."
        },
        {
          "name": "shareholders",
          "type": "string",
          "description": "JSON-encoded list of shareholders (for enterprises, the business owner normalized into the same shape)."
        },
        {
          "name": "previousDirectors",
          "type": "string",
          "description": "JSON-encoded list of previous directors/officers."
        },
        {
          "name": "companyLastOldName",
          "type": "string",
          "description": "The company's most recent former name, when the profile records a name change."
        },
        {
          "name": "companyNameDateOfChange",
          "type": "string",
          "description": "Date of the most recent name change, normalized to ISO YYYY-MM-DD."
        }
      ],
      "legacyId": "finxtract-ssm"
    },
    {
      "id": "applicant-identity",
      "displayName": "Applicant-Provided Identity",
      "description": "Identity values the applicant typed into a lending application form. The first NON-DOCUMENT attestor in the registry: every other category is an extraction pipeline reading a document, while these are values a person entered about themselves. One instance per application.",
      "canonicalTable": "ihs_alt_data_applicant_identity",
      "fields": [
        {
          "name": "personName",
          "type": "string",
          "fact": "personName",
          "description": "Full name as the applicant entered it. Attestation of the shared personName fact \u2014 the identity-document, payslip, EPF and bank-statement categories attest the same fact from their own sources. A borrower typing their name differently from their IC is normal, not an error."
        },
        {
          "name": "personIdNumber",
          "type": "string",
          "fact": "personIdNumber",
          "description": "IC or passport number as the applicant entered it. Attestation of the shared personIdNumber fact. Forms validate 12 digits with no separators, while the identity document attests it as printed and commonly hyphenated \u2014 so comparison must normalize before concluding disagreement."
        },
        {
          "name": "personDateOfBirth",
          "type": "string",
          "fact": "personDateOfBirth",
          "description": "Date of birth as the applicant entered it, normalized to ISO YYYY-MM-DD. Attestation of the shared personDateOfBirth fact."
        },
        {
          "name": "personNationality",
          "type": "string",
          "fact": "personNationality",
          "description": "Nationality as the applicant entered it. Attestation of the shared personNationality fact; free text on both sides, so comparable after normalization."
        }
      ]
    },
    {
      "id": "applicant-demographics",
      "displayName": "Applicant-Provided Demographics",
      "canonicalTable": "ihs_alt_data_applicant_demographics",
      "description": "Demographic attributes the applicant selected on a lending application form. Split out of applicant-identity (SYS-3166) rather than merged into it, and not cosmetically: every field here is uniformly sensitive, so confidentiality is decided once for the category instead of being re-argued per field a few hundred times over. applicant-identity keeps the legally-identifying facts \u2014 name, id number, date of birth, nationality \u2014 which are the subject key under Act 710 s.28. One instance per application.",
      "fields": [
        {
          "name": "statedAge",
          "type": "string",
          "description": "Age as the applicant entered it. Deliberately NOT a shared fact with personDateOfBirth, and deliberately a string: the column is varchar(10) and forms accept free text, so a value may be a range or a non-numeric answer. An age is also true only on the day it was given \u2014 observedAt is what makes it interpretable, and it must never be reconciled against a date of birth as though the two were the same claim."
        },
        {
          "name": "genderCode",
          "type": "string",
          "description": "Gender as the applicant SELECTED IT FROM A DROPDOWN \u2014 a code, not a label. Named apart from person-identity's personGender, and carrying no shared fact, on purpose. The document attests OCR text off a MyKad while the form emits a code ('M', '01') whose meaning is per-form. Collected by 6 of the 60 live form configs, all 6 sharing one choice set today \u2014 measured per field. Declaring them one fact would put 'M' and 'LELAKI' in the disagreement surface as a permanent false positive on one of its highest-volume fields, and the registry's kind-agreement rule refuses the shortcut anyway. Comparing the two needs a per-form code-to-label resolution that does not exist yet; when it does, the comparison belongs there and not in the field name. NOT declared kind \"enum\": that promises a CLOSED label set, and the host enforces the promise by requiring every adapter producing the field to enumerate its labels in the manifest. A form-intake adapter cannot: a form config carries its OWN choices, so one can add a value tomorrow without the manifest knowing, and the declaration would go quietly false the first time someone edits a form."
        },
        {
          "name": "raceCode",
          "type": "string",
          "description": "Race as the applicant selected it from a dropdown. Same reasoning as genderCode, and the same deliberate absence of a shared fact with person-identity's personRace. Collected by 12 of the 60 live form configs, all 12 sharing one choice set today \u2014 measured per field. NOT declared kind \"enum\": that promises a CLOSED label set, and the host enforces the promise by requiring every adapter producing the field to enumerate its labels in the manifest. A form-intake adapter cannot: a form config carries its OWN choices, so one can add a value tomorrow without the manifest knowing, and the declaration would go quietly false the first time someone edits a form."
        },
        {
          "name": "maritalStatus",
          "type": "string",
          "description": "Marital status as selected. Uniquely declared, so it needs no fact. NOT declared kind \"enum\": that promises a CLOSED label set, and the host enforces the promise by requiring every adapter producing the field to enumerate its labels in the manifest."
        },
        {
          "name": "dependantsCount",
          "type": "number",
          "unit": "count",
          "description": "Number of dependants as the applicant entered it."
        },
        {
          "name": "educationLevel",
          "type": "string",
          "description": "Highest education level as selected. NOT declared kind \"enum\": that promises a CLOSED label set, and the host enforces the promise by requiring every adapter producing the field to enumerate its labels in the manifest."
        },
        {
          "name": "preferredLanguage",
          "type": "string",
          "description": "Preferred correspondence language as selected. NOT declared kind \"enum\": that promises a CLOSED label set, and the host enforces the promise by requiring every adapter producing the field to enumerate its labels in the manifest."
        },
        {
          "name": "residenceType",
          "type": "string",
          "description": "Residence type (owned, rented, family-owned, \u2026) as selected. A housing-tenure signal, which is why it sits here rather than with applicant-address: it describes the person, not a place. NOT declared kind \"enum\": that promises a CLOSED label set, and the host enforces the promise by requiring every adapter producing the field to enumerate its labels in the manifest."
        }
      ]
    },
    {
      "id": "applicant-contact",
      "displayName": "Applicant-Provided Contact Point",
      "canonicalTable": "ihs_alt_data_applicant_contact",
      "description": "One way to reach the applicant \u2014 a phone line, an email address, or an emergency contact. MULTI-instance, keyed by the kind of contact point, because the wide ihs table flattens five phone lines into five column families (mobile, office, international, emergency, and an unqualified phoneNumber) and therefore cannot hold a sixth. The instance key IS what the column suffix was encoding. Fields absent for a given kind are simply absent on that instance rather than being null columns for everyone.",
      "fields": [
        {
          "name": "contactValue",
          "type": "string",
          "description": "The number or address itself, as the applicant entered it. One field rather than one per kind: what distinguishes a mobile from an office line is the instance, not the column."
        },
        {
          "name": "contactAreaCode",
          "type": "string",
          "description": "Area or country code, where the form captures it separately from the number. Modeled because the wide table carries a matching AreaCode column for four of the five phone families; NO live form config collects any of them today, so no manifest maps it yet. The category describes the domain; a manifest describes what a form actually asks."
        },
        {
          "name": "contactExtension",
          "type": "string",
          "description": "Phone extension. Same status as contactAreaCode \u2014 one wide column exists (internationalExtensionPhoneNo), no live form collects it."
        },
        {
          "name": "contactName",
          "type": "string",
          "description": "The name of the person reached at this contact point, where that is someone other than the applicant. Populated for the emergency instance. Deliberately NOT sharing personName's fact: this is a THIRD PARTY's name, and attesting it as the subject's would be a genuine identity error, not a formatting one."
        },
        {
          "name": "contactRelationship",
          "type": "string",
          "description": "How the named person relates to the applicant (spouse, parent, employer, \u2026). Meaningful only alongside contactName. NOT declared kind \"enum\": that promises a CLOSED label set, and the host enforces the promise by requiring every adapter producing the field to enumerate its labels in the manifest."
        }
      ]
    },
    {
      "id": "applicant-address",
      "displayName": "Applicant-Provided Address",
      "canonicalTable": "ihs_alt_data_applicant_address",
      "description": "One address the applicant entered on a lending application form. MULTI-instance, keyed by which address it is (\"permanent\", \"residential\", \"office\"), because the wide ihs table spends a whole column family on each and therefore cannot hold a fourth. Declaring this single would re-encode that limit permanently, and unlike a wrong field name it is not repairable later \u2014 the information would never have been captured. Ragged coverage across the three becomes ABSENT FIELDS on an instance rather than null columns for everyone: the permanent block has no state column of its own (its state landed in the unprefixed `state`), and only the residential block carries an occupancy duration.",
      "fields": [
        {
          "name": "addressLine1",
          "type": "string",
          "description": "First line of the address, as entered."
        },
        {
          "name": "addressLine2",
          "type": "string",
          "description": "Second line of the address, as entered."
        },
        {
          "name": "addressLine3",
          "type": "string",
          "description": "Third line of the address, as entered."
        },
        {
          "name": "addressCity",
          "type": "string",
          "description": "City or town, as entered."
        },
        {
          "name": "addressPostcode",
          "type": "string",
          "description": "Postcode, as entered. A string rather than a number: Malaysian postcodes are five digits and a leading zero is significant."
        },
        {
          "name": "addressStateCode",
          "type": "string",
          "description": "State as the applicant SELECTED IT FROM A DROPDOWN \u2014 a code ('A' = PERAK, 'B' = SELANGOR), not a label. Named for what it holds, and carrying no shared fact, for the same reason as applicant-demographics' genderCode: the choice set is defined per form config, so no single adapter can enumerate it and no comparison against a free-text state is meaningful without a per-form resolution that does not exist. NOT declared kind \"enum\" \u2014 that promises a closed label set the host makes each producing adapter enumerate, which a form-intake adapter spanning 60 heterogeneous forms cannot do."
        },
        {
          "name": "addressCountry",
          "type": "string",
          "description": "Country, as entered. Modeled because the wide table carries an unprefixed `country` column; NO live form config collects it, so no manifest maps it. The category describes the domain, a manifest describes what a form actually asks."
        },
        {
          "name": "occupancyYears",
          "type": "number",
          "unit": "years",
          "description": "How long the applicant has lived at this address, whole years. Meaningful only on the residential instance \u2014 it is the one block the wide table gives a duration. Kept separate from occupancyMonths rather than folded into a single figure because the form collects two inputs and form intake has no transform slot by design; combining them is a consumer's job, and doing it here would invent a value the applicant never gave."
        },
        {
          "name": "occupancyMonths",
          "type": "number",
          "unit": "months",
          "description": "The remainder months of the residency duration above. See occupancyYears for why the pair is not collapsed."
        }
      ]
    },
    {
      "id": "applicant-employment",
      "displayName": "Applicant-Provided Employment",
      "canonicalTable": "ihs_alt_data_applicant_employment",
      "description": "One job the applicant described on a lending application form. MULTI-instance: a person can hold two jobs and the wide ihs table, which has exactly one unprefixed employment block, cannot say so. Today's form collects only the current one, so the sole adapter emits a single instance under the key \"current\" \u2014 an explicit key rather than the empty one, because \"\" reads as \"the single instance a single-cardinality category has\" and this category is not that. A second job becomes another row.",
      "fields": [
        {
          "name": "employerName",
          "type": "string",
          "fact": "employerName",
          "description": "Employer name as the applicant entered it. Attestation of the shared employerName fact \u2014 the PAYSLIP attests the same fact from the document side (SYS-3333 de-prefixed it from payslipEmployerName). This is the pairing the disagreement surface exists for: a borrower naming an employer their payslip does not, or naming it differently, is worth seeing. Free text on both sides, so comparable after normalization."
        },
        {
          "name": "employmentStatus",
          "type": "string",
          "description": "Employment status as selected (permanent, contract, probation, \u2026). NOT declared kind \"enum\". Collected by 6 of the 60 live form configs, and all 6 carry an identical choice set today \u2014 measured per field, not inferred from a sibling. The closure is still not the category's to promise: a form config carries its OWN choices, so one can add a value tomorrow without the manifest knowing, and kind \"enum\" obliges every producing adapter to enumerate the labels it emits. The declaration would go quietly false the first time someone edits a form."
        },
        {
          "name": "employmentType",
          "type": "string",
          "description": "Employment type as selected. NOT declared kind \"enum\". Collected by 6 of the 60 live form configs, and all 6 carry an identical choice set today \u2014 measured per field, not inferred from a sibling. The closure is still not the category's to promise: a form config carries its OWN choices, so one can add a value tomorrow without the manifest knowing, and kind \"enum\" obliges every producing adapter to enumerate the labels it emits. The declaration would go quietly false the first time someone edits a form."
        },
        {
          "name": "occupation",
          "type": "string",
          "description": "Occupation as selected. NOT declared kind \"enum\". Collected by 6 of the 60 live form configs, and all 6 carry an identical choice set today \u2014 measured per field, not inferred from a sibling. The closure is still not the category's to promise: a form config carries its OWN choices, so one can add a value tomorrow without the manifest knowing, and kind \"enum\" obliges every producing adapter to enumerate the labels it emits. The declaration would go quietly false the first time someone edits a form."
        },
        {
          "name": "employmentSector",
          "type": "string",
          "description": "Employment sector as selected. NOT declared kind \"enum\". Collected by 6 of the 60 live form configs, and all 6 carry an identical choice set today \u2014 measured per field, not inferred from a sibling. The closure is still not the category's to promise: a form config carries its OWN choices, so one can add a value tomorrow without the manifest knowing, and kind \"enum\" obliges every producing adapter to enumerate the labels it emits. The declaration would go quietly false the first time someone edits a form."
        },
        {
          "name": "subEmploymentSector",
          "type": "string",
          "description": "Sub-sector within employmentSector, as selected. NOT declared kind \"enum\". Collected by 6 of the 60 live form configs, and all 6 carry an identical choice set today \u2014 measured per field, not inferred from a sibling. The closure is still not the category's to promise: a form config carries its OWN choices, so one can add a value tomorrow without the manifest knowing, and kind \"enum\" obliges every producing adapter to enumerate the labels it emits. The declaration would go quietly false the first time someone edits a form."
        },
        {
          "name": "businessSector",
          "type": "string",
          "description": "Business sector of the employer. Modeled because the wide table carries the column; NO live form collects it, so no manifest maps it."
        },
        {
          "name": "dateJoined",
          "type": "string",
          "description": "Date the applicant joined this employer, as entered. A string, matching the source column's varchar(50) and the same treatment personDateOfBirth gets: forms accept a range of spellings and narrowing here would reject values the form already takes."
        },
        {
          "name": "lengthOfServiceYears",
          "type": "number",
          "unit": "years",
          "description": "Length of service, whole years. Modeled because the wide table carries the column; no live form collects it. Kept separate from lengthOfServiceMonths for the same reason applicant-address keeps occupancy years and months apart \u2014 form intake has no transform slot, so folding two figures into one would invent a value nobody gave."
        },
        {
          "name": "lengthOfServiceMonths",
          "type": "number",
          "unit": "months",
          "description": "Remainder months of the length of service. See lengthOfServiceYears."
        }
      ]
    },
    {
      "id": "applicant-income",
      "displayName": "Applicant-Provided Income",
      "canonicalTable": "ihs_alt_data_applicant_income",
      "description": "The applicant's stated income position. SINGLE-instance: unlike employment, these describe the person overall rather than one job, and the wide table's single block is not a limitation being inherited here \u2014 it matches what is being said.",
      "fields": [
        {
          "name": "grossPay",
          "type": "number",
          "kind": "money",
          "fact": "grossPay",
          "legacyName": "monthlyGrossIncome",
          "description": "Gross pay as the applicant stated it, monthly. Attestation of the shared grossPay fact, which the PAYSLIP attests from the document side. COMPARISON MUST CHECK THE PERIOD FIRST: a payslip's grossPay is the gross for ITS pay period, and the payslip category carries payPeriod for exactly that reason \u2014 a monthly statement compared against a fortnightly payslip disagrees by construction, not because anyone was wrong. The fact is shared because the two ARE the same quantity once the period is known, which is what distinguishes this from applicant-demographics' genderCode, where no resolution between the two sides exists at all."
        },
        {
          "name": "netPay",
          "type": "number",
          "kind": "money",
          "fact": "netPay",
          "legacyName": "monthlyNetIncome",
          "description": "Net pay as the applicant stated it, monthly. Attestation of the shared netPay fact, also attested by the payslip. Same period caveat as grossPay. Modeled but NOT mapped by any manifest: the wide table carries monthlyNetIncome and no live form collects it."
        },
        {
          "name": "sourceOfFund",
          "type": "string",
          "description": "Stated source of funds. NOT declared kind \"enum\". Collected by 6 of the 60 live form configs, and all 6 carry an identical choice set today \u2014 measured per field, not inferred from a sibling. The closure is still not the category's to promise: a form config carries its OWN choices, so one can add a value tomorrow without the manifest knowing, and kind \"enum\" obliges every producing adapter to enumerate the labels it emits. The declaration would go quietly false the first time someone edits a form."
        },
        {
          "name": "sourceOfWealth",
          "type": "string",
          "description": "Stated source of wealth. NOT declared kind \"enum\". Collected by 6 of the 60 live form configs, and all 6 carry an identical choice set today \u2014 measured per field, not inferred from a sibling. The closure is still not the category's to promise: a form config carries its OWN choices, so one can add a value tomorrow without the manifest knowing, and kind \"enum\" obliges every producing adapter to enumerate the labels it emits. The declaration would go quietly false the first time someone edits a form."
        },
        {
          "name": "computationMode",
          "type": "string",
          "description": "How the stated income was computed. Modeled because the wide table carries the column; no live form collects it."
        },
        {
          "name": "statementType",
          "type": "string",
          "description": "Statement type backing the stated income. Modeled because the wide table carries the column; no live form collects it."
        }
      ]
    },
    {
      "id": "subject-company",
      "displayName": "Applicant-Provided Company Profile",
      "canonicalTable": "ihs_alt_data_subject_company",
      "description": "The company an application is about, as the applicant describes it. SINGLE-instance: one application has one subject company. THREE fields, not the six wide columns the transition plan grouped here \u2014 companyBackground, noOfEmployees and companyWebsite are collected by no live form in either population (0 of 4,622 ihs rows), and a field no form can feed is a promise this category cannot keep. They stay wide-table-only until something collects them. The three that ARE collected arrive from the lead-gen SPAs, not from FinHub's form_configs; a measurement over form_configs alone reports all six as uncollected, which is exactly how this category came to be dropped once already.",
      "fields": [
        {
          "name": "entityTypeCode",
          "type": "string",
          "description": "Legal entity type of the subject company as the applicant SELECTED IT FROM A DROPDOWN \u2014 a code, not a label. Named apart from company-profile's companyEntityType, and carrying no shared fact, on purpose: that one is OCR-read off the SSM document, this one is a code whose meaning is per-form. Exactly the genderCode / personGender split, for exactly the same reason \u2014 the registry enforces it, refusing one name across two categories unless both attest the same fact. NOT declared kind \"enum\": that promises a CLOSED label set, and the only form collecting this is a lead-gen SPA whose config is compiled into a bundle shipped from a separate repo, so the choice set can change without this registry or any manifest knowing."
        },
        {
          "name": "companySizeCode",
          "type": "string",
          "description": "Stated size band of the subject company, as a per-form dropdown code. Same naming and enum reasoning as entityTypeCode. A band, not a count: noOfEmployees is the count, nothing collects it, so this can be neither derived from nor checked against one."
        },
        {
          "name": "businessNatureCode",
          "type": "string",
          "description": "Stated nature of the subject company's business, as a per-form dropdown code. Named apart from company-profile's businessNature \u2014 which is OCR-read off the SSM document \u2014 for the same reason entityTypeCode is named apart, even though the two spellings do not collide and the registry would not have objected. Relying on a spelling difference nobody chose deliberately is how a shared fact gets asserted by accident."
        }
      ]
    },
    {
      "id": "applicant-collateral",
      "displayName": "Applicant-Provided Collateral",
      "canonicalTable": "ihs_alt_data_applicant_collateral",
      "description": "The asset offered as security on a lending application, as the applicant (or the lender's own Open API caller) stated it. MULTI-instance, and not speculatively: finsys-api's lender Open API already models this block as `collateral[0]` \u2014 an ARRAY (see COLLATERAL_FIELDS in src/utils/ihsFieldDefs.ts) \u2014 so more than one collateral item is the EXISTING published contract, while the wide `ihs` table has exactly one column per attribute and can therefore hold exactly one item. The instance key is the array index the contract already uses, so a second collateral is one more row rather than eleven more columns. Every field describes a MOTOR VEHICLE, and is named for that rather than for the generic role, because the category's whole field set is vehicle-specific (a chassis number, an engine number and a laden-weight flag say nothing about a property or a machine). Naming them collateralIdentifier / collateralAttribute would buy a generality the data cannot honour; property or equipment collateral gets its own category with its own facts, and this one stays honest about what it can describe.",
      "fields": [
        {
          "name": "vehicleMake",
          "type": "string",
          "description": "Manufacturer of the vehicle offered as collateral, as stated. No live form config collects it today \u2014 measured across BOTH form-spec populations on 2026-08-15 (0 of the 60 FinHub form_configs, 0 of the four lead-gen SPA bundles) and 0 of 4,624 rows in the sim's `ihs`. It is declared anyway, on the applicant-contact precedent (contactAreaCode / contactExtension), and on a stronger basis than that precedent had: the wide column exists AND `makeOfVehicle` is an authorable field in @finsys/core's own v2 form catalog (form-field-base-specs.json, category 12 'Vehicle Information'), so a lender can put it on a form today without any change here. A category describes the domain; a manifest describes what a form actually asks.",
          "legacyName": "makeOfVehicle"
        },
        {
          "name": "vehicleModel",
          "type": "string",
          "description": "Model of the vehicle offered as collateral. COLLECTED \u2014 by the finhero-auto lead-gen SPA, whose form spec is compiled into the SPA bundle and is therefore invisible to any query over FinHub's form_configs table. That second population is why this field is mapped and vehicleMake is not: measuring form_configs alone reports `modelOfVehicle` as collected by nobody, while the sim's `ihs` carries 8 rows of it. On the finhero-auto form the field is `visible: false` \u2014 a hidden input the page fills rather than a question the applicant answers. It is still an applicant-side statement submitted through the form save, so it is attested; the hidden-ness is a UX fact, not a provenance one.",
          "legacyName": "modelOfVehicle"
        },
        {
          "name": "vehicleYearMake",
          "type": "number",
          "unit": "count",
          "range": [
            1900,
            2100
          ],
          "description": "Model year of the vehicle, as a four-digit year. Not collected by any live form (0/60 form_configs, 0/4 SPAs, 0/4,624 `ihs` rows) and declared for the same reason as vehicleMake \u2014 `yearMake` is authorable in the v2 catalog, which bounds it [2014, 2100] per-form. The range here is deliberately WIDER than any one form's: a category bound must hold for every lender, and a used-vehicle programme can legitimately accept a 1998 model. The unit is `count` rather than `years` because this is a calendar year \u2014 a position on a timeline \u2014 not a duration; `years` on this field would read as an age and be wrong by about two thousand.",
          "legacyName": "yearMake"
        },
        {
          "name": "vehicleConditionCode",
          "type": "string",
          "description": "Condition of the vehicle as the applicant SELECTED IT FROM A DROPDOWN \u2014 a code, not a label. COLLECTED: 6 of the 60 FinHub form_configs plus the finhero-auto SPA, all seven carrying the same choice set today ('N', 'U', 'UN', 'UR', 'PR'), measured 2026-08-15. Named `...Code` on the genderCode / raceCode precedent, and for the same reason: a future document attestor (a JPJ registration card, a valuation report) would attest the printed condition TEXT, and fusing a per-form code with OCR'd prose puts a permanent false positive into the disagreement surface. When the two need comparing, the comparison belongs in a per-form code-to-label resolution, not in a shared field name. NOT declared kind \"enum\": that promises a CLOSED label set and obliges every producing adapter to enumerate its labels in its manifest. A form-intake adapter cannot honour that \u2014 the choice set lives in a form config a lender can edit, so the promise goes quietly false the first time someone adds a sixth condition. That the seven live forms agree today is precisely the kind of measurement that expires.",
          "legacyName": "vehicleCondition"
        },
        {
          "name": "vehicleChassisNo",
          "type": "string",
          "description": "Chassis / VIN of the vehicle. Not collected by any live form today (0/60, 0/4, 0/4,624) and authorable in the v2 catalog. Declared rather than deferred because it is the vehicle's IDENTITY: it is the field a second attestor (a registration document extraction, a JPJ lookup) would key on, and reserving the canonical name now is what lets that attestor arrive as a manifest rather than as a registry change.",
          "legacyName": "chassisNo"
        },
        {
          "name": "vehicleEngineNo",
          "type": "string",
          "description": "Engine number of the vehicle. Same status and same reasoning as vehicleChassisNo: uncollected today, authorable in the v2 catalog, and an identity fact a document attestor would key on.",
          "legacyName": "engineNo"
        },
        {
          "name": "vehicleRegistrationNo",
          "type": "string",
          "description": "Registration (plate) number of the vehicle. Uncollected today, authorable in the v2 catalog. The third identity fact, and the one a human would actually use to tell two collateral instances apart \u2014 worth noting that it CANNOT serve as the instance key, because form intake has no transform slot and an instance key must be a literal in the manifest.",
          "legacyName": "registrationNo"
        },
        {
          "name": "vehicleRegistrationDate",
          "type": "string",
          "description": "Date the vehicle was first registered, as stated. Typed `string`, not a date primitive, and deliberately: the source column `ihs.registrationDate` is varchar(50), the v2 catalog authors it as a text input with inputType date, and neither constrains the format. Declaring it a narrower type here would assert a normalization that nothing performs \u2014 the value is whatever the form sent, and observedAt is what makes it interpretable.",
          "legacyName": "registrationDate"
        },
        {
          "name": "vehicleLadenWeightOver2500kg",
          "type": "string",
          "description": "Whether the vehicle's BDM (berat dengan muatan \u2014 laden weight) exceeds 2,500 kg, as stated. A commercial-vehicle discriminator that changes the applicable financing rules. Typed `string` and NOT `boolean`, which is the non-obvious call: the fact is boolean-shaped, but `ihs.bdmMoreThan2500kg` is varchar(10) and the v2 catalog authors it as a free TEXT input, so the values that actually arrive are whatever a form emits \u2014 'Y', 'N', 'Yes', ''. Declaring `boolean` would make the contract assert a coercion nobody performs, and the coercion's failure mode is silent: a truthiness test turns the string 'N' into true. Uncollected by any live form today.",
          "legacyName": "bdmMoreThan2500kg"
        },
        {
          "name": "collateralMarketValue",
          "type": "number",
          "kind": "money",
          "description": "Assessed market value of the collateral. Prefixed `collateral` rather than named bare `marketValue` because a bare one would be a name a company valuation, a property appraisal or a share price could all claim later \u2014 and under this registry's rules a second category claiming the name would have to attest the SAME fact, which they do not. kind \"money\": the number is meaningless without a denomination, and the denomination belongs to the observation, not the field. Uncollected by any live form today; authorable in the v2 catalog.",
          "legacyName": "marketValue"
        },
        {
          "name": "collateralPurchasePriceOnTheRoad",
          "type": "number",
          "kind": "money",
          "description": "On-the-road purchase price of the collateral \u2014 the price including registration, road tax and insurance, which is the figure Malaysian motor financing is actually written against. COLLECTED, and the most widely collected field in this category: 6 of the 60 FinHub form_configs plus the finhero-auto SPA (where it is the loan calculator's primary input), 8 of 4,624 `ihs` rows, measured 2026-08-15. Spelled out rather than left as the acronym OTR so the contract is readable by a consumer who has never seen a Malaysian motor quote; the legacy flat column keeps its `purchasePriceOTR` spelling. kind \"money\" \u2014 and note the v2 form catalog already declares this same field kind \"money\", so the two vocabularies agree on it.",
          "legacyName": "purchasePriceOTR"
        }
      ]
    },
    {
      "id": "applicant-obligations",
      "displayName": "Applicant-Declared Monthly Obligations",
      "canonicalTable": "ihs_alt_data_applicant_obligations",
      "description": "One existing monthly debt commitment the applicant declared \u2014 the input to a debt-service ratio. MULTI-instance, keyed by the kind of obligation, and this is the textbook case for the whole transition: the wide `ihs` table spends five columns on five FIXED kinds (housing loan, hire purchase, personal loan, credit card, other financing), so an applicant with a SIXTH commitment has nowhere to put it and the DSR is computed from an amount that is knowably incomplete. Five columns collapse to two fields; the kind moves from the column NAME into a value, and a sixth obligation becomes one more row. NO adapter maps this category today and that is deliberate rather than unfinished \u2014 no live form collects any of the five columns (measured 2026-08-15 across both form-spec populations: 0 of 60 FinHub form_configs, 0 of the four lead-gen SPA bundles, and 0 of 4,624 rows in the sim's `ihs`), so a form-intake manifest over them would mint an APPLICANT attestation for a value no applicant ever gave. The declaration is the useful half: it reserves the vocabulary, states the shape, and lets the manifest arrive as a declaration when a lender authors the fields (all five are already authorable in @finsys/core's v2 form catalog, category 4 'Financial Information').",
      "fields": [
        {
          "name": "obligationType",
          "type": "string",
          "description": "What kind of commitment this instance is \u2014 housing loan, hire purchase, personal loan, credit card, other financing, or something none of those five named. Carries no legacyName ON PURPOSE: there is no flat column it renames, because in the flat shape this value was not stored at all \u2014 it was encoded in WHICH of the five columns held a number. That is the specific thing this category changes. NOT declared kind \"enum\": the five wide columns look like a closed set and are exactly the set that proved too small, so promising closure here would re-create the defect in the contract that the schema change exists to remove."
        },
        {
          "name": "obligationMonthlyInstallment",
          "type": "number",
          "kind": "money",
          "description": "The monthly amount payable on this commitment. Carries no legacyName, and could not: FIVE flat columns (housingLoanMonthlyInstallment, hirePurchaseMonthlyInstallment, personalLoanMonthlyInstallment, creditCardMonthlyInstallment, otherFinancingMonthlyInstallment) all collapse into this one field, so any single legacy name would be correct for one of them and a lie about the other four. The mapping back to the flat vocabulary is (obligationType, obligationMonthlyInstallment) -> column, which is a per-instance function and not a field rename. kind \"money\"."
        }
      ]
    },
    {
      "id": "related-person",
      "displayName": "Related Person",
      "canonicalTable": "ihs_alt_data_related_person",
      "description": "A person other than the applicant who stands in a declared ROLE to the application \u2014 a company's named contact person, a director, a guarantor, a next of kin. MULTI-instance, keyed by role. Distinct from applicant-contact, which is the nearest neighbour and the one worth drawing the line against: applicant-contact models a CHANNEL (a phone line, an email address) and carries contactName / contactRelationship so an emergency contact's channel can say whose it is. It cannot carry an id number, an id type or a date of birth, and it should not \u2014 a contact POINT that carried identity facts would be two things at once. This category models the PERSON, with identity, and lets their contact details hang off them. NO adapter maps it today, and the reason is stronger here than for applicant-obligations: the eight wide `ihs` columns behind it (contactPersonName, contactPersonEmail, contactPersonPhoneNo, contactPersonPhoneNoAreaCode, contactPersonDateOfBirth, contactPersonPosition, contactPersonIdNo, contactPersonIdType) are ABSENT FROM @finsys/core's v2 form-authoring catalog entirely \u2014 verified 2026-08-15 against src/data/form-field-base-specs.json, where all 173 authorable fields were checked and none of the eight appears. So unlike applicant-collateral's unused fields, a lender CANNOT put these on a form today even if they wanted to: the columns are reachable only through the lender Open API's own payload, not through any form a borrower fills. 0 of 4,624 rows in the sim's `ihs` carry any of them. Declaring the category without a manifest states the shape and reserves the names; adding the fields to the authoring catalog is what would make a manifest meaningful, and that is a form-authoring change, not this one.",
      "fields": [
        {
          "name": "relatedPersonName",
          "type": "string",
          "description": "The related person's name, as given. Deliberately does NOT share person-identity's personName fact \u2014 the same call applicant-contact made for contactName, and for the same reason: this is a THIRD PARTY's name, and attesting it as the subject's would be a genuine identity error rather than a formatting disagreement. A shared fact means 'two sources describing one real-world thing'; these are two different people.",
          "legacyName": "contactPersonName"
        },
        {
          "name": "relatedPersonIdNumber",
          "type": "string",
          "description": "The related person's identification number (NRIC, passport, or whatever relatedPersonIdType names). Same third-party reasoning as relatedPersonName: no shared fact with personIdNumber.",
          "legacyName": "contactPersonIdNo"
        },
        {
          "name": "relatedPersonIdType",
          "type": "string",
          "description": "Which kind of identification relatedPersonIdNumber is. NOT declared kind \"enum\": that promises a CLOSED label set and obliges every producing adapter to enumerate its labels, and there is no adapter to consult \u2014 the value arrives through the lender Open API from a caller whose vocabulary is its own.",
          "legacyName": "contactPersonIdType"
        },
        {
          "name": "relatedPersonDateOfBirth",
          "type": "string",
          "description": "The related person's date of birth, as given. Typed `string` because the source column is a DATE but the value arriving through the Open API is unnormalized text, and because no shared fact ties it to personDateOfBirth \u2014 a different person's birthday is not a competing attestation of the applicant's.",
          "legacyName": "contactPersonDateOfBirth"
        },
        {
          "name": "relatedPersonRole",
          "type": "string",
          "description": "How this person relates to the application \u2014 director, authorized contact, guarantor, next of kin. This is the instance key's meaning made explicit as a value, on the same reasoning as obligationType: a role encoded only in the key is invisible to a reader holding one row. NOT declared kind \"enum\". Renames the wide column contactPersonPosition, which named the person's job title within a company; `role` is the wider and more accurate word, since a guarantor holds no position.",
          "legacyName": "contactPersonPosition"
        },
        {
          "name": "relatedPersonEmail",
          "type": "string",
          "description": "Email address for the related person.",
          "legacyName": "contactPersonEmail"
        },
        {
          "name": "relatedPersonPhone",
          "type": "string",
          "description": "Phone number for the related person, as given.",
          "legacyName": "contactPersonPhoneNo"
        },
        {
          "name": "relatedPersonPhoneAreaCode",
          "type": "string",
          "description": "Area or country code, where it is captured separately from the number. Kept as its own field rather than folded into relatedPersonPhone because the wide table captures the two separately and there is no transform slot anywhere on the intake path that could join them \u2014 joining here would invent a value nobody gave.",
          "legacyName": "contactPersonPhoneNoAreaCode"
        }
      ]
    },
    {
      "id": "document-intake",
      "displayName": "Document Intake",
      "description": "One instance per uploaded document file. This category is about the POINTER, not the document's contents — the extracted values already live in bank-statement, payslip, financial-statement and the rest, each carrying full provenance. The pointer itself had none: it is a bare string, or a JSON array of them, sitting in a wide column with no record of who put it there or when. This gives a pointer the same standing as every other fact on the canonical plane. It carries no value parsed OUT of the document, deliberately — a category mixing a file's identity with a file's contents would make it impossible to say which adapter run attested what.",
      "canonicalTable": "ihs_alt_data_document_intake",
      "fields": [
        {
          "name": "documentType",
          "type": "string",
          "description": "Which document slot this file was uploaded into — the applicant-facing kind, e.g. a payslip, a bank statement, a company registration form. Deliberately NOT declared kind: \"enum\". The closed set of slots belongs to the host, it varies by jurisdiction and by form catalog, and restating it here would create a second list that has to be kept in step with the first. A registry naming a value it does not own is how two declarations of one fact begin to disagree."
        },
        {
          "name": "pathInDms",
          "type": "string",
          "description": "Where the file lives in document storage. This is the value the legacy wide columns held directly, and the reason they could not be trusted: nothing recorded which write path set it, so a pointer overwritten by one caller was indistinguishable from a pointer set by another. As a canonical field it arrives attached to a run, which is the whole of the difference."
        },
        {
          "name": "uploadedAt",
          "type": "string",
          "description": "When the file was uploaded, as an ISO 8601 timestamp. Distinct from the adapter run's own observedAt, which records when the pointer was ATTESTED — a backfill of existing pointers observes years-old uploads, and collapsing the two would date every historical document to the day the migration ran."
        },
        {
          "name": "uploadedBy",
          "type": "string",
          "description": "Who uploaded the file — the acting principal, not the subject the document is about. The applicant, an agent acting for them, and a lender staging a document on their behalf are three different answers and only this field separates them. It is the one part of the stored file shape that had no home before: one upload route wrote it ad hoc into a single column's entries, so for every other document the answer was simply unrecoverable."
        }
      ]
    }
  ]
}
