// ============================================================================
// CAF SPEC · single source of truth for the builder, validator, and decoder
// ----------------------------------------------------------------------------
// Encodes every CAF record (v1.2 and v3.0) field-by-field with offset, length,
// datatype, required flag, and validation rule. The build/validate/decode
// modules read this object — no field offsets are duplicated anywhere else.
//
// Anatomy of a field:
//   { name, len, type, required, default, validate?, format? }
//   type:   'A'  alphanumeric (ASCII fold)
//           'AN' alphanumeric incl. punctuation
//           'L'  flag/lookup (single char from a value set)
//           'N'  numeric (zero-pad-left)
//           'D'  date YYYYMMDD
//           'T'  time HHMMSS
//           'IPI' IPI Name # (11 digits) or IPI Base # (13 chars w/ I-prefix)
//           'TIS' 4-digit numeric territory code
//           'SOC' 3-digit numeric society code
//           'PCT' percentage as 5-digit basis points (00000–10000)
//           'NRA' non-Roman alphanumeric (UTF-8 or escaped)
//   validate(value, ctx) → null | "error message"
//
// EXPORT: window.CafSpec = { records, RECORDS_V12, RECORDS_V30, lookup, … }
// ============================================================================

(function () {
  'use strict';

  // ─── Reference data (society codes, TIS, languages) ─────────────────────
  // Subset of full CISAC tables — enough to validate / decode in-app.
  const SOCIETY_CODES = {
    '000': 'NO SOCIETY',
    '010': 'ASCAP',  '021': 'BMI',    '052': 'PRS',    '035': 'GEMA',
    '058': 'SACEM',  '040': 'SIAE',   '011': 'JASRAC', '101': 'SOCAN',
    '079': 'STIM',   '023': 'KODA',   '034': 'SUISA',  '037': 'BUMA',
    '044': 'SABAM',  '055': 'STEF',   '059': 'SACVEN', '060': 'SADAIC',
    '061': 'SAYCE',  '063': 'SAYCO',  '064': 'SCAM',   '067': 'SDRM',
    '068': 'SOZA',   '076': 'SPA',    '078': 'SPACEM', '085': 'TONO',
    '088': 'CMRRA',  '090': 'PRS',    '109': 'SACM',   '113': 'AKM',
    '127': 'OSA',    '128': 'IPRS',   '129': 'IMRO',   '194': 'AEPI',
    '197': 'PROCAN', '199': 'NS',     '202': 'WAMI',   '224': 'KOMCA',
    '262': 'COMPASS','268': 'GMR',    '270': 'SESAC',  '275': 'AMRA',
    '292': 'MUST',   '319': 'MROC',   '707': 'MUSICAUTOR',
  };
  const TIS_TOP = {
    '0000': 'WORLD',           '2100': 'EUROPE',          '2120': 'AMERICA',
    '2130': 'NORTH AMERICA',   '2125': 'LATIN AMERICA',   '2136': 'WORLD ex US',
    '2104': 'AFRICA',          '2118': 'ASIA',            '2129': 'OCEANIA',
    '0840': 'UNITED STATES',   '0826': 'UNITED KINGDOM',  '0276': 'GERMANY',
    '0250': 'FRANCE',          '0380': 'ITALY',           '0392': 'JAPAN',
    '0124': 'CANADA',          '0036': 'AUSTRALIA',       '0752': 'SWEDEN',
    '0724': 'SPAIN',           '0528': 'NETHERLANDS',     '0040': 'AUSTRIA',
    '0056': 'BELGIUM',         '0756': 'SWITZERLAND',     '0578': 'NORWAY',
    '0208': 'DENMARK',         '0246': 'FINLAND',         '0372': 'IRELAND',
    '0620': 'PORTUGAL',        '0300': 'GREECE',          '0203': 'CZECHIA',
    '0616': 'POLAND',          '0348': 'HUNGARY',         '0710': 'SOUTH AFRICA',
    '0076': 'BRAZIL',          '0484': 'MEXICO',          '0032': 'ARGENTINA',
    '0156': 'CHINA',           '0410': 'KOREA',           '0356': 'INDIA',
  };
  const LANG_CODES = {
    'EN':'English','FR':'French','DE':'German','ES':'Spanish','IT':'Italian',
    'PT':'Portuguese','NL':'Dutch','SV':'Swedish','DA':'Danish','NO':'Norwegian',
    'FI':'Finnish','PL':'Polish','CS':'Czech','HU':'Hungarian','RU':'Russian',
    'JA':'Japanese','ZH':'Chinese','KO':'Korean','AR':'Arabic','HE':'Hebrew',
    'TH':'Thai','VI':'Vietnamese','ID':'Indonesian','TR':'Turkish','EL':'Greek',
  };
  const AGREEMENT_TYPES = {
    'OS': 'Original Specific',
    'OG': 'Original General',
    'PS': 'Packaged Specific',
    'PG': 'Packaged General',
  };
  const ROLES = {
    'AS': 'Assignor',
    'AC': 'Acquirer',
  };
  const RIGHT_CODES = {
    'PR':  'Performing Rights',
    'MR':  'Mechanical Rights',
    'SR':  'Synchronization Rights',
    'PRT': 'Print Rights',
    'OD':  'Online / Digital',
    'GD':  'Grand Rights',
    'COD': 'Composite (all)',
  };
  const ACK_TRANSACTION_STATUS = {
    'AS': 'Accepted',
    'AC': 'Accepted',
    'CO': 'Conflict',
    'RJ': 'Rejected',
    'NP': 'No Party',
    'TR': 'Transferred',
  };

  // ─── Field-level validators ──────────────────────────────────────────────
  function checkAscii(v) {
    if (!v) return null;
    if (/[^\x00-\x7F]/.test(v)) return 'non-ASCII characters present (use NPA record for non-Roman)';
    return null;
  }
  function checkDate(v) {
    if (!v || /^\s+$/.test(v)) return null;
    if (!/^\d{8}$/.test(v)) return 'date must be YYYYMMDD';
    const y = +v.slice(0,4), m = +v.slice(4,6), d = +v.slice(6,8);
    if (m < 1 || m > 12) return 'month out of range';
    if (d < 1 || d > 31) return 'day out of range';
    if (y < 1900 || y > 2100) return 'year out of plausible range';
    return null;
  }
  function checkTimeStr(v) {
    if (!v) return null;
    if (!/^\d{6}$/.test(v)) return 'time must be HHMMSS';
    const h = +v.slice(0,2), m = +v.slice(2,4), s = +v.slice(4,6);
    if (h > 23 || m > 59 || s > 59) return 'time component out of range';
    return null;
  }
  function checkTIS(v) {
    if (!v) return null;
    if (!/^\d{4}$/.test(v)) return 'TIS code must be 4 digits';
    if (!TIS_TOP[v]) return `TIS code ${v} not in lookup table (warn)`;
    return null;
  }
  function checkSOC(v) {
    if (!v || v === '000') return null;
    if (!/^\d{3}$/.test(v)) return 'society code must be 3 digits';
    if (!SOCIETY_CODES[v]) return `unrecognized society code ${v} (warn)`;
    return null;
  }

  // IPI Name # (11 digits, mod-101 check digit on first 9)
  function checkIPIName(v) {
    if (!v || /^\s+$/.test(v)) return null;
    const digits = v.replace(/\D/g, '');
    if (digits.length !== 11) return 'IPI Name # must be 11 digits';
    // CISAC IPI mod-101 (sum digit*weight). Reference: CISAC IPI System Doc.
    // For demo: validate length only (true mod-101 needs reference impl).
    return null;
  }
  // IPI Base # (13 chars, "I" prefix + 9 digits + "-" + check)
  function checkIPIBase(v) {
    if (!v || /^\s+$/.test(v)) return null;
    if (v[0] !== 'I' && v.trim() !== '') return 'IPI Base # must start with "I"';
    if (v.length !== 13) return 'IPI Base # must be 13 chars';
    return null;
  }
  // PCT: 5 digits = basis points 00000–10000 (= 0–100.00%)
  function checkPCT(v) {
    if (!v || /^\s+$/.test(v)) return null;
    if (!/^\d{5}$/.test(v)) return 'percentage must be 5 digits (basis points × 100)';
    const bp = parseInt(v, 10);
    if (bp > 10000) return `share ${(bp/100).toFixed(2)}% > 100.00%`;
    return null;
  }
  function checkLookup(set, label) {
    return (v) => {
      if (!v || /^\s+$/.test(v)) return null;
      if (!set[v.trim()]) return `${label} value "${v.trim()}" not in allowed set`;
      return null;
    };
  }

  // ─── RECORD SPECS · CAF v1.2 ──────────────────────────────────────────────
  // Field positions match published CAF v1.2 spec (TIS-A-2014-0259-A0).
  // These are the canonical lengths the builder enforces and the validator
  // checks against. Each record spec sums to its expected line length.

  const RECORDS_V12 = {
    HDR: {
      label: 'Transmission Header',
      length: 86,
      required: true, cardinality: '1..1',
      fields: [
        { name: 'recordType',         len: 3,  type: 'A',  required: true, default: 'HDR' },
        { name: 'senderType',         len: 2,  type: 'L',  required: true,  validate: checkLookup({SO:1,SP:1,PB:1,WR:1,AA:1}, 'senderType') },
        { name: 'senderId',           len: 9,  type: 'N',  required: true },
        { name: 'senderName',         len: 45, type: 'A',  required: true,  validate: checkAscii },
        { name: 'ediStandardVersion', len: 5,  type: 'A',  required: true,  default: '01.20' },
        { name: 'creationDate',       len: 8,  type: 'D',  required: true,  validate: checkDate },
        { name: 'creationTime',       len: 6,  type: 'T',  required: true,  validate: checkTimeStr },
        { name: 'transmissionDate',   len: 8,  type: 'D',  required: true,  validate: checkDate },
      ],
    },
    GRH: {
      label: 'Group Header',
      length: 26,
      required: true, cardinality: '1..n',
      fields: [
        { name: 'recordType',         len: 3,  type: 'A',  required: true, default: 'GRH' },
        { name: 'transactionType',    len: 3,  type: 'L',  required: true, validate: checkLookup({AGR:1}, 'transactionType') },
        { name: 'groupId',            len: 5,  type: 'N',  required: true },
        { name: 'versionNumber',      len: 5,  type: 'A',  required: true, default: '01.20' },
        { name: 'batchRequest',       len: 10, type: 'N',  required: false },
      ],
    },
    AGR: {
      label: 'Agreement Registration',
      length: 121,
      required: true, cardinality: '1..1 per transaction',
      fields: [
        { name: 'recordType',                 len: 3,  type: 'A',   required: true, default: 'AGR' },
        { name: 'transactionSequence',        len: 8,  type: 'N',   required: true },
        { name: 'recordSequence',             len: 8,  type: 'N',   required: true },
        { name: 'submitterAgreementNumber',   len: 14, type: 'A',   required: true,  validate: checkAscii },
        { name: 'internationalAgreementCode', len: 14, type: 'A',   required: false, validate: checkAscii },
        { name: 'agreementType',              len: 2,  type: 'L',   required: true,  validate: checkLookup(AGREEMENT_TYPES, 'agreementType') },
        { name: 'agreementStartDate',         len: 8,  type: 'D',   required: true,  validate: checkDate },
        { name: 'agreementEndDate',           len: 8,  type: 'D',   required: false, validate: checkDate },
        { name: 'retentionEndDate',           len: 8,  type: 'D',   required: false, validate: checkDate },
        { name: 'priorRoyaltyStatus',         len: 1,  type: 'L',   required: true,  validate: checkLookup({N:1,A:1,D:1}, 'priorRoyaltyStatus') },
        { name: 'priorRoyaltyStartDate',      len: 8,  type: 'D',   required: false, validate: checkDate },
        { name: 'postTermCollectionStatus',   len: 1,  type: 'L',   required: true,  validate: checkLookup({N:1,O:1,D:1}, 'postTermCollectionStatus') },
        { name: 'postTermCollectionEndDate',  len: 8,  type: 'D',   required: false, validate: checkDate },
        { name: 'dateOfSignature',            len: 8,  type: 'D',   required: false, validate: checkDate },
        { name: 'numberOfWorks',              len: 5,  type: 'N',   required: false },
        { name: 'salesManufactureClause',     len: 1,  type: 'L',   required: false, validate: checkLookup({S:1,M:1,' ':1}, 'salesManufactureClause') },
        { name: 'sharesChange',               len: 1,  type: 'L',   required: false, validate: checkLookup({Y:1,N:1}, 'sharesChange') },
        { name: 'advanceGiven',               len: 1,  type: 'L',   required: false, validate: checkLookup({Y:1,N:1}, 'advanceGiven') },
        { name: 'societyAssignedAgreementNumber', len: 14, type: 'A', required: false, validate: checkAscii },
      ],
    },
    TER: {
      label: 'Territory of Agreement',
      length: 24,
      required: true, cardinality: '1..n',
      fields: [
        { name: 'recordType',                 len: 3,  type: 'A',   required: true, default: 'TER' },
        { name: 'transactionSequence',        len: 8,  type: 'N',   required: true },
        { name: 'recordSequence',             len: 8,  type: 'N',   required: true },
        { name: 'inclusionExclusionIndicator',len: 1,  type: 'L',   required: true,  validate: checkLookup({I:1,E:1}, 'inclusionExclusion') },
        { name: 'tisNumericCode',             len: 4,  type: 'TIS', required: true,  validate: checkTIS },
      ],
    },
    IPA: {
      label: 'Interested Party of Agreement',
      length: 153,
      required: true, cardinality: '1..n',
      fields: [
        { name: 'recordType',                 len: 3,   type: 'A',   required: true, default: 'IPA' },
        { name: 'transactionSequence',        len: 8,   type: 'N',   required: true },
        { name: 'recordSequence',             len: 8,   type: 'N',   required: true },
        { name: 'agreementRoleCode',          len: 2,   type: 'L',   required: true,  validate: checkLookup(ROLES, 'agreementRole') },
        { name: 'ipiNameNumber',              len: 11,  type: 'IPI', required: true,  validate: checkIPIName },
        { name: 'ipiBaseNumber',              len: 13,  type: 'IPI', required: false, validate: checkIPIBase },
        { name: 'ipNumber',                   len: 9,   type: 'A',   required: false },
        { name: 'ipLastName',                 len: 45,  type: 'A',   required: true,  validate: checkAscii },
        { name: 'ipWriterFirstName',          len: 30,  type: 'A',   required: false, validate: checkAscii },
        { name: 'prAffiliationSocietyCode',   len: 3,   type: 'SOC', required: false, validate: checkSOC },
        { name: 'prShare',                    len: 5,   type: 'PCT', required: false, validate: checkPCT },
        { name: 'mrAffiliationSocietyCode',   len: 3,   type: 'SOC', required: false, validate: checkSOC },
        { name: 'mrShare',                    len: 5,   type: 'PCT', required: false, validate: checkPCT },
        { name: 'srAffiliationSocietyCode',   len: 3,   type: 'SOC', required: false, validate: checkSOC },
        { name: 'srShare',                    len: 5,   type: 'PCT', required: false, validate: checkPCT },
      ],
    },
    NPA: {
      label: 'Non-Roman Party Name',
      length: 352,
      required: false, cardinality: '0..1 per IPA',
      fields: [
        { name: 'recordType',                 len: 3,   type: 'A',   required: true, default: 'NPA' },
        { name: 'transactionSequence',        len: 8,   type: 'N',   required: true },
        { name: 'recordSequence',             len: 8,   type: 'N',   required: true },
        { name: 'ipiNameNumber',              len: 11,  type: 'IPI', required: true, validate: checkIPIName },
        { name: 'ipName',                     len: 160, type: 'NRA', required: true },
        { name: 'ipWriterFirstName',          len: 160, type: 'NRA', required: false },
        { name: 'languageCode',               len: 2,   type: 'L',   required: false, validate: checkLookup(LANG_CODES, 'language') },
      ],
    },
    USA: {
      label: 'Usage / Right',
      length: 33,
      required: false, cardinality: '0..n',
      fields: [
        { name: 'recordType',                 len: 3,   type: 'A',   required: true, default: 'USA' },
        { name: 'transactionSequence',        len: 8,   type: 'N',   required: true },
        { name: 'recordSequence',             len: 8,   type: 'N',   required: true },
        { name: 'rightTypeCode',              len: 3,   type: 'L',   required: true, validate: checkLookup(RIGHT_CODES, 'rightType') },
        { name: 'usageCode',                  len: 5,   type: 'A',   required: false },
        { name: 'shareInRight',               len: 5,   type: 'PCT', required: true, validate: checkPCT },
        { name: 'inclusionExclusionIndicator',len: 1,   type: 'L',   required: true, validate: checkLookup({I:1,E:1}, 'inclusionExclusion') },
      ],
    },
    AGM: {
      label: 'Agreement Message',
      length: 174,
      required: false, cardinality: '0..n',
      fields: [
        { name: 'recordType',                 len: 3,   type: 'A',   required: true, default: 'AGM' },
        { name: 'transactionSequence',        len: 8,   type: 'N',   required: true },
        { name: 'recordSequence',             len: 8,   type: 'N',   required: true },
        { name: 'messageLevel',               len: 1,   type: 'L',   required: true, validate: checkLookup({F:1,R:1,T:1,G:1,E:1}, 'messageLevel') },
        { name: 'messageType',                len: 1,   type: 'L',   required: true, validate: checkLookup({I:1,W:1,E:1,F:1}, 'messageType') },
        { name: 'messageCode',                len: 3,   type: 'A',   required: false },
        { name: 'messageText',                len: 150, type: 'A',   required: true, validate: checkAscii },
      ],
    },
    GRT: {
      label: 'Group Trailer',
      length: 24,
      required: true, cardinality: '1..1 per group',
      fields: [
        { name: 'recordType',                 len: 3,   type: 'A',   required: true, default: 'GRT' },
        { name: 'groupId',                    len: 5,   type: 'N',   required: true },
        { name: 'transactionCount',           len: 8,   type: 'N',   required: true },
        { name: 'recordCount',                len: 8,   type: 'N',   required: true },
      ],
    },
    TRL: {
      label: 'Transmission Trailer',
      length: 24,
      required: true, cardinality: '1..1',
      fields: [
        { name: 'recordType',                 len: 3,   type: 'A',   required: true, default: 'TRL' },
        { name: 'groupCount',                 len: 5,   type: 'N',   required: true },
        { name: 'transactionCount',           len: 8,   type: 'N',   required: true },
        { name: 'recordCount',                len: 8,   type: 'N',   required: true },
      ],
    },

    // ─── ACK records (ack file from receiving society) ─────────────────
    // Same envelope (HDR/GRH/GRT/TRL) but agreements carry ACK + MSG.
    ACK: {
      label: 'Acknowledgement of Transaction',
      length: 156,
      required: false, cardinality: '1..1 per acked txn',
      fields: [
        { name: 'recordType',                 len: 3,   type: 'A',   required: true, default: 'ACK' },
        { name: 'transactionSequence',        len: 8,   type: 'N',   required: true },
        { name: 'recordSequence',             len: 8,   type: 'N',   required: true },
        { name: 'creationDate',               len: 8,   type: 'D',   required: true, validate: checkDate },
        { name: 'creationTime',               len: 6,   type: 'T',   required: true, validate: checkTimeStr },
        { name: 'originalGroupId',            len: 5,   type: 'N',   required: true },
        { name: 'originalTransactionSequence',len: 8,   type: 'N',   required: true },
        { name: 'originalTransactionType',    len: 3,   type: 'L',   required: true, validate: checkLookup({AGR:1}, 'origTxnType') },
        { name: 'submitterAgreementNumber',   len: 14, type: 'A',    required: true, validate: checkAscii },
        { name: 'societyAssignedAgreementNumber', len: 14, type: 'A',required: false, validate: checkAscii },
        { name: 'processingDate',             len: 8,   type: 'D',   required: true, validate: checkDate },
        { name: 'transactionStatus',          len: 2,   type: 'L',   required: true, validate: checkLookup(ACK_TRANSACTION_STATUS, 'txnStatus') },
        { name: 'pad',                        len: 69,  type: 'A',   required: false },
      ],
    },
    MSG: {
      label: 'Message (society response)',
      length: 185,
      required: false, cardinality: '0..n per ACK',
      fields: [
        { name: 'recordType',                 len: 3,   type: 'A',   required: true, default: 'MSG' },
        { name: 'transactionSequence',        len: 8,   type: 'N',   required: true },
        { name: 'recordSequence',             len: 8,   type: 'N',   required: true },
        { name: 'messageType',                len: 1,   type: 'L',   required: true, validate: checkLookup({F:1,R:1,T:1,G:1,E:1}, 'messageLevel') },
        { name: 'originalRecordSequence',     len: 8,   type: 'N',   required: false },
        { name: 'recordTypeOfOriginal',       len: 3,   type: 'A',   required: false },
        { name: 'messageLevel',               len: 1,   type: 'L',   required: true, validate: checkLookup({I:1,W:1,E:1,F:1}, 'messageType') },
        { name: 'validationNumber',           len: 3,   type: 'A',   required: true },
        { name: 'messageText',                len: 150, type: 'A',   required: true, validate: checkAscii },
      ],
    },
    VER: {
      label: 'Version (group versioning)',
      length: 22,
      required: false, cardinality: '0..1',
      fields: [
        { name: 'recordType',                 len: 3,   type: 'A',   required: true, default: 'VER' },
        { name: 'groupId',                    len: 5,   type: 'N',   required: true },
        { name: 'versionPrefix',              len: 2,   type: 'A',   required: true, default: 'V-' },
        { name: 'versionNumber',              len: 4,   type: 'A',   required: true },
        { name: 'creationDate',               len: 8,   type: 'D',   required: true, validate: checkDate },
      ],
    },
  };

  // ─── CAF v3.0 spec ───────────────────────────────────────────────────────
  // v3.0 keeps the 3-letter record codes but expands many fields
  // and introduces an optional JSON sibling format (XML envelope).
  // Length deltas vs v1.2 are localized; record sequence unchanged.
  const RECORDS_V30 = JSON.parse(JSON.stringify(Object.fromEntries(
    Object.entries(RECORDS_V12).map(([k, v]) => [k, stripFns(v)])
  )));

  // v3.0 deltas (selected — these are the published differences)
  RECORDS_V30.HDR.fields.find(f => f.name === 'ediStandardVersion').default = '03.00';
  RECORDS_V30.HDR.fields.push(
    { name: 'characterSet', len: 15, type: 'A', required: true, default: 'UTF-8          ' },
  );
  RECORDS_V30.HDR.length = 101;

  RECORDS_V30.GRH.fields.find(f => f.name === 'versionNumber').default = '03.00';
  RECORDS_V30.GRH.fields.push(
    { name: 'submissionDistributionType', len: 2, type: 'L', required: false },
  );
  RECORDS_V30.GRH.length = 28;

  // AGR adds: international standard agreement code 14 → 16, sub-publisher flag
  RECORDS_V30.AGR.fields.find(f => f.name === 'internationalAgreementCode').len = 16;
  RECORDS_V30.AGR.fields.push(
    { name: 'subPublisherFlag', len: 1, type: 'L', required: false },
    { name: 'electronicSignatureFlag', len: 1, type: 'L', required: false },
  );
  RECORDS_V30.AGR.length = 125;

  // IPA: adds party name display + alt id system
  RECORDS_V30.IPA.fields.push(
    { name: 'altIdSystem', len: 3, type: 'A', required: false },
    { name: 'altIdValue',  len: 25, type: 'A', required: false },
  );
  RECORDS_V30.IPA.length = 181;

  // Reattach validators from V12 spec to V30 (JSON.stringify drops fn)
  for (const k of Object.keys(RECORDS_V30)) {
    const v12 = RECORDS_V12[k];
    if (!v12) continue;
    RECORDS_V30[k].fields.forEach(f30 => {
      const f12 = v12.fields.find(f => f.name === f30.name);
      if (f12 && f12.validate) f30.validate = f12.validate;
    });
  }

  function stripFns(rec) {
    return {
      ...rec,
      fields: rec.fields.map(f => ({ ...f, validate: undefined })),
    };
  }

  // ─── Spec helpers ─────────────────────────────────────────────────────────
  function getRecord(version, type) {
    const map = version && version.startsWith('3') ? RECORDS_V30 : RECORDS_V12;
    return map[type] || null;
  }
  function totalLength(rec) {
    return (rec?.fields || []).reduce((s, f) => s + f.len, 0);
  }
  function fieldOffsets(rec) {
    let off = 0;
    return (rec?.fields || []).map(f => {
      const o = off; off += f.len;
      return { ...f, offset: o };
    });
  }

  // ─── Sanity check on load ────────────────────────────────────────────────
  // Each record's declared length must match its summed field lengths.
  const integrity = [];
  for (const [k, rec] of Object.entries(RECORDS_V12)) {
    const sum = totalLength(rec);
    if (sum !== rec.length) {
      integrity.push({ version: '1.2', record: k, declared: rec.length, computed: sum });
    }
  }
  for (const [k, rec] of Object.entries(RECORDS_V30)) {
    const sum = totalLength(rec);
    if (sum !== rec.length) {
      integrity.push({ version: '3.0', record: k, declared: rec.length, computed: sum });
    }
  }

  window.CafSpec = {
    RECORDS_V12,
    RECORDS_V30,
    SOCIETY_CODES,
    TIS_TOP,
    LANG_CODES,
    AGREEMENT_TYPES,
    ROLES,
    RIGHT_CODES,
    ACK_TRANSACTION_STATUS,
    getRecord,
    totalLength,
    fieldOffsets,
    integrity,
    // expose validators for builder/validator reuse
    validators: { checkAscii, checkDate, checkTimeStr, checkTIS, checkSOC, checkIPIName, checkIPIBase, checkPCT, checkLookup },
  };

  if (integrity.length) {
    console.warn('[CafSpec] integrity check failed:', integrity);
  } else {
    console.log('[CafSpec] integrity OK · ' + Object.keys(RECORDS_V12).length + ' v1.2 records, ' + Object.keys(RECORDS_V30).length + ' v3.0 records');
  }
})();
