@mudraforge/statutory 2.4.2 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +52 -11
  2. package/bin/cli.js +73 -7
  3. package/docs/ARCHITECTURE_DECISIONS.md +13 -9
  4. package/docs/CATALOG.md +164 -0
  5. package/docs/CORPUS_QUALITY.md +9 -6
  6. package/docs/PILOT_EVALUATION_GUIDE.md +149 -0
  7. package/okf/direct_tax/tds_commission_194h/circulars/finance_act_2024_rate_cut_memo.md +29 -0
  8. package/okf/direct_tax/tds_commission_194h/forms/form_26q_quarterly_return.json +30 -0
  9. package/okf/direct_tax/tds_commission_194h/section.json +63 -0
  10. package/okf/direct_tax/tds_contractor_194c/circulars/cbdt_circular_23_2017_gst_unbundling.md +27 -0
  11. package/okf/direct_tax/tds_contractor_194c/forms/form_26q_quarterly_return.json +31 -0
  12. package/okf/direct_tax/tds_contractor_194c/forms/transporter_194c_declaration.json +14 -0
  13. package/okf/direct_tax/tds_contractor_194c/section.json +66 -0
  14. package/okf/direct_tax/tds_rent_194i/circulars/cbdt_circular_23_2017_gst_unbundling.md +27 -0
  15. package/okf/direct_tax/tds_rent_194i/forms/form_26q_quarterly_return.json +31 -0
  16. package/okf/direct_tax/tds_rent_194i/section.json +63 -0
  17. package/okf/indirect_tax_gst/composite_mixed_supplies_sec8/circulars/cbic_circular_47_2018_composite_mixed_supplies.md +41 -0
  18. package/okf/indirect_tax_gst/composite_mixed_supplies_sec8/forms/composite_mixed_supply_assessment_matrix.json +51 -0
  19. package/okf/indirect_tax_gst/composite_mixed_supplies_sec8/section.json +58 -0
  20. package/okf/indirect_tax_gst/itc_eligibility_16_2/circulars/cbic_circular_170_2022_itc_reporting.md +53 -0
  21. package/okf/indirect_tax_gst/itc_eligibility_16_2/forms/gstr_3b_table_4_itc_reversal.json +32 -0
  22. package/okf/indirect_tax_gst/itc_eligibility_16_2/section.json +84 -0
  23. package/okf/indirect_tax_gst/rcm_liability_sec9_3/circulars/cbic_circular_102_2019_rcm_clarifications.md +33 -0
  24. package/okf/indirect_tax_gst/rcm_liability_sec9_3/forms/rcm_self_invoice_payment_voucher.json +65 -0
  25. package/okf/indirect_tax_gst/rcm_liability_sec9_3/section.json +67 -0
  26. package/okf/international_tax/cross_border_remittance_195/circulars/cbdt_circular_03_2022_form10f_electronic.md +31 -0
  27. package/okf/international_tax/cross_border_remittance_195/circulars/supreme_court_engineering_analysis_doctrine.md +41 -0
  28. package/okf/international_tax/cross_border_remittance_195/forms/form_15ca_15cb_compliance_checklist.json +73 -0
  29. package/okf/international_tax/cross_border_remittance_195/section.json +74 -0
  30. package/okf/manifest.json +66 -13
  31. package/okf/msme_commercial/enterprise_classification_sec7/circulars/msme_notification_2119_e_composite_criteria.md +37 -0
  32. package/okf/msme_commercial/enterprise_classification_sec7/forms/udyam_composite_calculation_worksheet.json +46 -0
  33. package/okf/msme_commercial/enterprise_classification_sec7/section.json +64 -0
  34. package/okf/msme_commercial/mca_msme_1_filing/circulars/mca_order_2019_specified_companies.md +42 -0
  35. package/okf/msme_commercial/mca_msme_1_filing/forms/form_msme_1_half_yearly_return.json +93 -0
  36. package/okf/msme_commercial/mca_msme_1_filing/section.json +56 -0
  37. package/okf/msme_commercial/statutory_disclosures_sec22/circulars/icai_guidance_note_schedule_iii_trade_payables.md +42 -0
  38. package/okf/msme_commercial/statutory_disclosures_sec22/forms/schedule_iii_msme_aging_table.json +60 -0
  39. package/okf/msme_commercial/statutory_disclosures_sec22/forms/section_23_tax_addback_schedule.json +35 -0
  40. package/okf/msme_commercial/statutory_disclosures_sec22/section.json +65 -0
  41. package/package.json +2 -2
  42. package/src/index.js +287 -37
  43. package/types/index.d.ts +21 -0
@@ -0,0 +1,65 @@
1
+ {
2
+ "sectionId": "MSMED2006_SEC22_DISCLOSURE",
3
+ "sectionCode": "MSMED-SEC-22",
4
+ "aliases": [
5
+ "section 22",
6
+ "sec 22",
7
+ "msme disclosures",
8
+ "schedule iii msme",
9
+ "trade payables aging",
10
+ "caro 2020 msme",
11
+ "section 23 addback",
12
+ "notes to accounts msme"
13
+ ],
14
+ "governingStatute": "Micro, Small and Medium Enterprises Development Act, 2006 read with Companies Act, 2013 Schedule III",
15
+ "statutorySection": "Section 22 of MSMED Act, 2006 read with Section 23 of MSMED Act, 2006 and Schedule III Division I & II",
16
+ "title": "Mandatory statutory disclosures in annual audited financial statements regarding dues to Micro and Small Enterprises and tax non-deductibility of penal interest",
17
+ "effectivePeriod": {
18
+ "validFrom": "2006-10-02",
19
+ "validTo": "OPEN_ENDED",
20
+ "annualReset": "Annual Financial Year-End Audited Financial Statements (March 31)"
21
+ },
22
+ "plainTextSummary": "Section 22 of the MSMED Act, 2006 mandates that any buyer required to have its accounts audited under any law must furnish five specific disclosures in its annual financial statements regarding trade payables to micro and small enterprises: (1) Principal and interest due remaining unpaid at year-end, (2) Interest paid along with delayed payments made during the year, (3) Interest due and payable for delays without added interest, (4) Interest accrued and remaining unpaid at year-end, and (5) Further interest remaining due and payable in succeeding years. In parallel, Schedule III of the Companies Act, 2013 prescribes a mandatory Trade Payables Aging Schedule segregating MSME dues (undisputed and disputed) into buckets: less than 1 year, 1-2 years, 2-3 years, and more than 3 years. Under Section 23 of the MSMED Act, compound penal interest payable or paid under Section 16 is strictly prohibited from being claimed as an income tax deduction, requiring an explicit book-to-tax add-back.",
23
+ "keyStatutoryThresholds": {
24
+ "mandatoryDisclosureCount": 5,
25
+ "scheduleIiiAgingBuckets": [
26
+ "Less than 1 year",
27
+ "1-2 years",
28
+ "2-3 years",
29
+ "More than 3 years"
30
+ ],
31
+ "disputeSegregation": "Mandatory separate aging disclosure for Undisputed MSME dues vs. Disputed MSME dues",
32
+ "taxAddbackMandate": "Section 23 strictly disallows Section 16 penal interest under Income-tax Act, 1961 (and Act 2025)",
33
+ "statutoryAuditVerification": "CARO 2020 Clause (xvi)(d) / (xix) requires statutory auditors to verify material uncertainty on settlement of MSME payables",
34
+ "applicableEntities": "All corporate and non-corporate entities subject to statutory audit having transactions with registered Micro/Small enterprises"
35
+ },
36
+ "officialCitations": [
37
+ {
38
+ "authority": "Ministry of Law and Justice",
39
+ "instrument": "Micro, Small and Medium Enterprises Development Act, 2006 (Act No. 27 of 2006)",
40
+ "subject": "Section 22: Requirement to specify unpaid amount with interest in the annual statement of accounts & Section 23: Interest not to be allowed as deduction from income",
41
+ "officialUrl": "https://msme.gov.in"
42
+ },
43
+ {
44
+ "authority": "Ministry of Corporate Affairs (MCA)",
45
+ "instrument": "Companies Act, 2013 Schedule III (Division I & II Amendment Notification G.S.R. 207(E) dated 24 March 2021)",
46
+ "subject": "Mandatory Trade Payables Aging Schedule Disclosures",
47
+ "officialUrl": "https://www.mca.gov.in"
48
+ },
49
+ {
50
+ "authority": "Institute of Chartered Accountants of India (ICAI)",
51
+ "instrument": "Guidance Note on Division I, II and III of Schedule III to the Companies Act, 2013",
52
+ "subject": "Accounting and Disclosure of Dues to Micro and Small Enterprises",
53
+ "officialUrl": "https://www.icai.org"
54
+ }
55
+ ],
56
+ "requiredDocumentChecklist": [
57
+ "Audited Balance Sheet Notes to Accounts for Trade Payables",
58
+ "Schedule III Trade Payables Aging Table (<1yr, 1-2yr, 2-3yr, >3yr)",
59
+ "General Ledger interest accrual schedule under Section 16 (compounded at 3x RBI Bank Rate)",
60
+ "Computation of Total Income showing Section 23 disallowance add-back in tax return",
61
+ "Confirmation letters from registered MSME suppliers on their status and outstanding dues"
62
+ ],
63
+ "tier": "PUBLIC_COMMONS_FREE",
64
+ "version": "2.4.0"
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mudraforge/statutory",
3
- "version": "2.4.2",
3
+ "version": "2.5.0",
4
4
  "description": "Open Knowledge Format (OKF) client & AI Agent Skill for Indian and cross-border statutory tax provisions. Zero dependencies.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "repository": {
38
38
  "type": "git",
39
- "url": "https://github.com/mondeep0123/mudraforge-statutory.git"
39
+ "url": "git+https://github.com/mondeep0123/mudraforge-statutory.git"
40
40
  },
41
41
  "homepage": "https://mudraforge.in/docs",
42
42
  "bugs": {
package/src/index.js CHANGED
@@ -18,13 +18,18 @@ const __filename = fileURLToPath(import.meta.url);
18
18
  const __dirname = path.dirname(__filename);
19
19
  export const DEFAULT_LOCAL_OKF_DIR = path.resolve(__dirname, '..', 'okf');
20
20
  export const DEFAULT_BASE_URL = 'https://api.mudraforge.in';
21
- export const STATUTORY_CLIENT_VERSION = '2.4.2';
21
+ export const STATUTORY_CLIENT_VERSION = '2.5.0';
22
22
 
23
23
  /**
24
24
  * Pinned official release Merkle root digests.
25
25
  * Prevents unauthorized bundle substitution attacks even if an attacker controls the remote endpoint.
26
26
  */
27
27
  export const PINNED_RELEASE_ROOTS = new Set([
28
+ 'sha256-c32be389bfb82a52ffc40670b74103216c45f3b284c66d2b0020097eb36a25f9', // v2.5.0 Merkle Manifest Root (18 Sections across 5 Domains)
29
+ 'sha256-f3b230a0774eb55d607403b5a656bbe2950a32d07145a46f0eb083232ec12986', // v2.6.0 Edge Sync Bundle Root (12 Sections across 5 Domains)
30
+ 'sha256-4e9d9c064e55748dc6389eae1b5cb2fec1ad7f1c811097afd57e4fb0a6f6454e', // v2.6.0 Local Static Manifest Root (12 Sections across 5 Domains)
31
+ 'sha256-b421910e35f4f16321f6a72382377db66a05ead521ff09d18a91ec9d534d0069', // v2.6.0 Edge Sync Bundle Root (11 Sections)
32
+ 'sha256-dbbd3bc618b7807c4e76861329bd8c6903ef7f9cb28efc9a8693abc06915c9c1', // v2.6.0 Local Static Manifest Root (11 Sections)
28
33
  'sha256-7fe98cf432db5d31ee6b65bbb9d77575ce8c9192eec6f318e3ad3382af0ba900', // v2.5.0 Edge Sync Bundle Root
29
34
  'sha256-96089fe2835d0b9493293a1e5205e0b6b46f16771c9380d1f571969b453ba80d', // v2.4.0 Local Static Manifest Root
30
35
  'sha256-d9612074241f7c2cc1d05fa5eee254996f21db56ad1ab71ac36a084ffd4169e1' // Baseline Release Root
@@ -34,14 +39,16 @@ export const PINNED_RELEASE_ROOTS = new Set([
34
39
  * Canonical MudraForge Statutory Root Ed25519 Public Key (SPKI DER Base64).
35
40
  * Used for verifying digital signatures on remote statutory update bundles.
36
41
  */
37
- export const MUDRAFORGE_STATUTORY_ROOT_PUBLIC_KEY = 'MCowBQYDK2VwAyEAw/XsE6QGtQzdq3EUP3xFpPFHu1x87VSNreXmXr6W17s=';
42
+ export const MUDRAFORGE_STATUTORY_ROOT_PUBLIC_KEY = 'MCowBQYDK2VwAyEAYN4ZtkUBOwybN68SiVUI9YMDlEBNiCa1CAgUNx6rXUo=';
43
+ export const LEGACY_STATUTORY_ROOT_PUBLIC_KEY = 'MCowBQYDK2VwAyEAw/XsE6QGtQzdq3EUP3xFpPFHu1x87VSNreXmXr6W17s=';
38
44
 
39
45
  /**
40
46
  * Authoritative set of trusted MudraForge Root Public Keys.
41
47
  * Public keys supplied through options that are not present in this set are rejected.
42
48
  */
43
49
  export const TRUSTED_STATUTORY_PUBLIC_KEYS = new Set([
44
- MUDRAFORGE_STATUTORY_ROOT_PUBLIC_KEY
50
+ MUDRAFORGE_STATUTORY_ROOT_PUBLIC_KEY,
51
+ LEGACY_STATUTORY_ROOT_PUBLIC_KEY
45
52
  ]);
46
53
 
47
54
  /**
@@ -51,7 +58,15 @@ export const TRUSTED_STATUTORY_KEYS_METADATA = {
51
58
  [MUDRAFORGE_STATUTORY_ROOT_PUBLIC_KEY]: {
52
59
  keyId: 'mudraforge-root-2026-v1',
53
60
  algorithm: 'Ed25519',
54
- purpose: 'MudraForge Open Knowledge Format Statutory Root Authority',
61
+ purpose: 'MudraForge Open Knowledge Format Statutory Root Authority (Active)',
62
+ validFrom: '2026-01-01T00:00:00Z',
63
+ validTo: '2027-12-31T23:59:59Z',
64
+ status: 'ACTIVE'
65
+ },
66
+ [LEGACY_STATUTORY_ROOT_PUBLIC_KEY]: {
67
+ keyId: 'mudraforge-root-2026-v0',
68
+ algorithm: 'Ed25519',
69
+ purpose: 'MudraForge Statutory Genesis Anchor (Legacy)',
55
70
  validFrom: '2026-01-01T00:00:00Z',
56
71
  validTo: '2027-12-31T23:59:59Z',
57
72
  status: 'ACTIVE'
@@ -305,6 +320,7 @@ export function lookupLocalStatute(section, okfDir = DEFAULT_LOCAL_OKF_DIR) {
305
320
  }
306
321
 
307
322
  const files = findJsonFilesRecursively(okfDir);
323
+ const records = [];
308
324
  for (const filePath of files) {
309
325
  let data;
310
326
  try {
@@ -312,55 +328,68 @@ export function lookupLocalStatute(section, okfDir = DEFAULT_LOCAL_OKF_DIR) {
312
328
  } catch (_) {
313
329
  continue;
314
330
  }
315
-
316
- // Must be a valid statutory record with sectionCode
317
331
  if (!data || !data.sectionCode || typeof data.sectionCode !== 'string') continue;
332
+ records.push({ filePath, data });
333
+ }
334
+
335
+ let matchedRecord = null;
318
336
 
319
- let isMatch = false;
337
+ // Pass 1: Authoritative exact match on sectionCode, sectionId, or aliases
338
+ for (const rec of records) {
339
+ const { data } = rec;
320
340
  const secCode = data.sectionCode.toLowerCase();
321
341
  const secId = (data.sectionId && typeof data.sectionId === 'string') ? data.sectionId.toLowerCase() : '';
322
342
 
323
343
  // 1. Direct sectionCode / sectionId match
324
344
  if (secCode === rawClean || (secId && secId === rawClean)) {
325
- isMatch = true;
345
+ matchedRecord = rec;
346
+ break;
326
347
  }
327
348
 
328
349
  // 2. Normalized alphanumeric match (handles "17_5", "17(5)", "17-5", "msmed_15_16")
329
350
  if (
330
- !isMatch && (
331
- secCode.replace(/[()_-\s]+/g, '') === normalized ||
332
- (secId && secId.replace(/[()_-\s]+/g, '') === normalized)
333
- )
351
+ secCode.replace(/[()_-\s]+/g, '') === normalized ||
352
+ (secId && secId.replace(/[()_-\s]+/g, '') === normalized)
334
353
  ) {
335
- isMatch = true;
354
+ matchedRecord = rec;
355
+ break;
336
356
  }
337
357
 
338
358
  // 3. Aliases match
339
359
  if (
340
- !isMatch &&
341
360
  data.aliases &&
342
361
  data.aliases.some(
343
362
  a => a.toLowerCase() === rawClean || a.toLowerCase().replace(/[()_-\s]+/g, '') === normalized
344
363
  )
345
364
  ) {
346
- isMatch = true;
365
+ matchedRecord = rec;
366
+ break;
347
367
  }
368
+ }
348
369
 
349
- // 4. Folder or filename match (handles "tds_goods_194q", "194Q-tds-on-purchase...")
350
- if (!isMatch) {
370
+ // Pass 2: Heuristic folder or filename match (only if no authoritative code match found)
371
+ if (!matchedRecord) {
372
+ const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
373
+ const tokenRegex = new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`, 'i');
374
+ for (const rec of records) {
375
+ const { filePath } = rec;
351
376
  const fileName = path.basename(filePath).toLowerCase().replace(/\.json$/, '');
352
377
  const folderName = path.basename(path.dirname(filePath)).toLowerCase();
378
+
353
379
  if (
354
- fileName.startsWith(rawClean) || fileName.startsWith(normalized) ||
355
- folderName.includes(rawClean) || folderName.includes(normalized)
380
+ fileName.startsWith(`${rawClean}-`) || fileName.startsWith(`${normalized}-`) ||
381
+ tokenRegex.test(folderName) || tokenRegex.test(fileName)
356
382
  ) {
357
- isMatch = true;
383
+ matchedRecord = rec;
384
+ break;
358
385
  }
359
386
  }
387
+ }
360
388
 
361
- if (isMatch) {
362
- // Attach supporting circulars and compliance forms if present in domain hierarchy
363
- const parentDir = path.dirname(filePath);
389
+ if (matchedRecord) {
390
+ const { filePath, data } = matchedRecord;
391
+ // Attach supporting circulars and compliance forms if present in domain hierarchy
392
+ const parentDir = path.dirname(filePath);
364
393
  const circularsDir = path.join(parentDir, 'circulars');
365
394
  const formsDir = path.join(parentDir, 'forms');
366
395
 
@@ -370,7 +399,6 @@ export function lookupLocalStatute(section, okfDir = DEFAULT_LOCAL_OKF_DIR) {
370
399
  };
371
400
 
372
401
  return data;
373
- }
374
402
  }
375
403
  return null;
376
404
  }
@@ -539,7 +567,6 @@ export async function syncOkf(options = {}) {
539
567
  assertSecureEndpoint(baseUrl);
540
568
 
541
569
  const okfDir = options.okfDir || DEFAULT_LOCAL_OKF_DIR;
542
- const sectionsDir = path.join(okfDir, 'sections');
543
570
  const localManifest = getLocalManifest(okfDir);
544
571
 
545
572
  const bundleUrl = new URL('/api/v1/statutory/bundle.json', baseUrl);
@@ -627,6 +654,7 @@ export async function syncOkf(options = {}) {
627
654
  const fileHashes = {};
628
655
  const syncedSectionCodes = [];
629
656
  const seenCodes = new Set();
657
+ const stagedRelPaths = [];
630
658
 
631
659
  for (const statute of bundle.statutes) {
632
660
  // Verify core required statutory fields
@@ -640,20 +668,30 @@ export async function syncOkf(options = {}) {
640
668
  }
641
669
  seenCodes.add(codeKey);
642
670
 
643
- const fileName = resolveSafeStatuteFilename(statute);
644
- const targetPath = path.resolve(stagingDir, fileName);
671
+ // Determine relative deployment target:
672
+ // If statute provides relPath, preserve domain hierarchy (e.g. "direct_tax/tds_goods_194q/section.json")
673
+ // Otherwise, fall back to flat "sections/<filename>.json" for legacy compatibility
674
+ const relTarget = statute.relPath
675
+ ? statute.relPath.replace(/\\/g, '/')
676
+ : path.join('sections', resolveSafeStatuteFilename(statute));
677
+
678
+ const targetPath = path.resolve(stagingDir, relTarget);
645
679
 
646
680
  // Path Traversal Containment Assertion
647
681
  if (!targetPath.startsWith(path.resolve(stagingDir) + path.sep)) {
648
- throw new Error(`[@mudraforge/statutory] Security violation: Path traversal detected in filename "${fileName}".`);
682
+ throw new Error(`[@mudraforge/statutory] Security violation: Path traversal detected in target "${relTarget}".`);
649
683
  }
650
684
 
685
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
686
+
651
687
  const jsonStr = JSON.stringify(statute, null, 2);
652
688
  fs.writeFileSync(targetPath, jsonStr, 'utf8');
653
689
 
690
+ const fileName = resolveSafeStatuteFilename(statute);
654
691
  const fileHash = crypto.createHash('sha256').update(jsonStr).digest('hex');
655
692
  fileHashes[fileName] = fileHash;
656
693
  syncedSectionCodes.push(statute.sectionCode);
694
+ stagedRelPaths.push(relTarget);
657
695
  }
658
696
 
659
697
  // 2.5 Cryptographic Root Merkle Digest Verification
@@ -694,7 +732,14 @@ export async function syncOkf(options = {}) {
694
732
  const isPinned = PINNED_RELEASE_ROOTS.has(bundle.rootHash);
695
733
 
696
734
  if (!isSignatureValid && !isPinned) {
697
- throw new Error(`[@mudraforge/statutory] Security violation: Untrusted bundle rootHash "${bundle.rootHash}". Bundle is neither cryptographically signed by MudraForge Root Authority nor pinned in authorized release roots. Set options.allowUnverifiedRoot=true for local non-production fixtures.`);
735
+ const isEdgeNewer = bundle.okfVersion && compareSemver(bundle.okfVersion, STATUTORY_CLIENT_VERSION) >= 0;
736
+ const remediation = isEdgeNewer
737
+ ? `The remote legal bundle (OKF v${bundle.okfVersion}) has a newer cryptographic release root than recognized by your installed package (v${STATUTORY_CLIENT_VERSION}).\n` +
738
+ ` To receive the updated release roots and trust anchors, update your client:\n` +
739
+ ` npm install @mudraforge/statutory@latest\n` +
740
+ ` npx @mudraforge/statutory@latest sync`
741
+ : `Bundle is neither cryptographically signed by MudraForge Root Authority nor pinned in authorized release roots. Set options.allowUnverifiedRoot=true for local non-production fixtures.`;
742
+ throw new Error(`[@mudraforge/statutory] Security violation: Untrusted bundle rootHash "${bundle.rootHash}". ${remediation}`);
698
743
  }
699
744
  }
700
745
 
@@ -720,19 +765,18 @@ export async function syncOkf(options = {}) {
720
765
  }
721
766
 
722
767
  // 3. Atomically deploy from staging to destination
723
- fs.mkdirSync(sectionsDir, { recursive: true });
724
-
725
- const stagedFiles = fs.readdirSync(stagingDir);
726
- for (const stagedFile of stagedFiles) {
727
- const srcPath = path.join(stagingDir, stagedFile);
728
- const destPath = path.join(sectionsDir, stagedFile);
768
+ for (const relTarget of stagedRelPaths) {
769
+ const srcPath = path.join(stagingDir, relTarget);
770
+ const destPath = path.join(okfDir, relTarget);
729
771
 
730
772
  // Safety assertion on destination path containment
731
773
  const resolvedDest = path.resolve(destPath);
732
- if (!resolvedDest.startsWith(path.resolve(sectionsDir) + path.sep)) {
733
- throw new Error(`[@mudraforge/statutory] Security violation: Destination path traversal detected in "${stagedFile}".`);
774
+ if (!resolvedDest.startsWith(path.resolve(okfDir) + path.sep)) {
775
+ throw new Error(`[@mudraforge/statutory] Security violation: Destination path traversal detected in "${relTarget}".`);
734
776
  }
735
777
 
778
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
779
+
736
780
  try {
737
781
  fs.renameSync(srcPath, destPath);
738
782
  } catch {
@@ -742,12 +786,20 @@ export async function syncOkf(options = {}) {
742
786
  }
743
787
 
744
788
  // 4. Write verified manifest
789
+ const manifestDomains = {};
790
+ for (const statute of bundle.statutes) {
791
+ const domain = statute.domain || (statute.relPath ? statute.relPath.split('/')[0] : 'sections');
792
+ if (!manifestDomains[domain]) manifestDomains[domain] = [];
793
+ if (statute.sectionCode) manifestDomains[domain].push(statute.sectionCode);
794
+ }
795
+
745
796
  const manifest = {
746
797
  okfVersion: bundle.okfVersion,
747
798
  publishedAt: bundle.publishedAt || new Date().toISOString(),
748
799
  rootHash: bundle.rootHash,
749
800
  totalSections: bundle.statutes.length,
750
801
  changelog: bundle.changelog || 'Synchronized from Cloudflare Edge API',
802
+ domains: manifestDomains,
751
803
  sections: syncedSectionCodes
752
804
  };
753
805
  fs.writeFileSync(path.join(okfDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
@@ -1193,3 +1245,201 @@ export class StatutoryClient {
1193
1245
  return await syncOkf({ baseUrl: this.baseUrl, okfDir: this.okfDir });
1194
1246
  }
1195
1247
  }
1248
+
1249
+ /**
1250
+ * Standard Indian State & Union Territory Codes (Census 2011 / GSTN 2-digit prefix).
1251
+ */
1252
+ export const INDIAN_STATE_CODES = {
1253
+ '01': 'Jammu & Kashmir',
1254
+ '02': 'Himachal Pradesh',
1255
+ '03': 'Punjab',
1256
+ '04': 'Chandigarh',
1257
+ '05': 'Uttarakhand',
1258
+ '06': 'Haryana',
1259
+ '07': 'Delhi',
1260
+ '08': 'Rajasthan',
1261
+ '09': 'Uttar Pradesh',
1262
+ '10': 'Bihar',
1263
+ '11': 'Sikkim',
1264
+ '12': 'Arunachal Pradesh',
1265
+ '13': 'Nagaland',
1266
+ '14': 'Manipur',
1267
+ '15': 'Mizoram',
1268
+ '16': 'Tripura',
1269
+ '17': 'Meghalaya',
1270
+ '18': 'Assam',
1271
+ '19': 'West Bengal',
1272
+ '20': 'Jharkhand',
1273
+ '21': 'Odisha',
1274
+ '22': 'Chhattisgarh',
1275
+ '23': 'Madhya Pradesh',
1276
+ '24': 'Gujarat',
1277
+ '26': 'Dadra & Nagar Haveli and Daman & Diu',
1278
+ '27': 'Maharashtra',
1279
+ '29': 'Karnataka',
1280
+ '30': 'Goa',
1281
+ '31': 'Lakshadweep',
1282
+ '32': 'Kerala',
1283
+ '33': 'Tamil Nadu',
1284
+ '34': 'Puducherry',
1285
+ '35': 'Andaman & Nicobar Islands',
1286
+ '36': 'Telangana',
1287
+ '37': 'Andhra Pradesh',
1288
+ '38': 'Ladakh',
1289
+ '97': 'Other Territory',
1290
+ '99': 'Centre Jurisdiction'
1291
+ };
1292
+
1293
+ /**
1294
+ * Statutory 4th character legal status classification for Indian Permanent Account Numbers (PAN).
1295
+ * Enacted under Section 139A of the Income-tax Act, 1961 read with Rule 114 of Income Tax Rules.
1296
+ */
1297
+ export const PAN_ENTITY_TYPES = {
1298
+ 'P': { code: 'P', name: 'Individual', isIndividualOrHuf: true, standard194cRatePercent: 1.0 },
1299
+ 'H': { code: 'H', name: 'Hindu Undivided Family (HUF)', isIndividualOrHuf: true, standard194cRatePercent: 1.0 },
1300
+ 'C': { code: 'C', name: 'Company', isIndividualOrHuf: false, standard194cRatePercent: 2.0 },
1301
+ 'F': { code: 'F', name: 'Partnership Firm / LLP', isIndividualOrHuf: false, standard194cRatePercent: 2.0 },
1302
+ 'T': { code: 'T', name: 'Trust', isIndividualOrHuf: false, standard194cRatePercent: 2.0 },
1303
+ 'A': { code: 'A', name: 'Association of Persons (AOP)', isIndividualOrHuf: false, standard194cRatePercent: 2.0 },
1304
+ 'B': { code: 'B', name: 'Body of Individuals (BOI)', isIndividualOrHuf: false, standard194cRatePercent: 2.0 },
1305
+ 'L': { code: 'L', name: 'Local Authority', isIndividualOrHuf: false, standard194cRatePercent: 2.0 },
1306
+ 'J': { code: 'J', name: 'Artificial Juridical Person', isIndividualOrHuf: false, standard194cRatePercent: 2.0 },
1307
+ 'G': { code: 'G', name: 'Government Agency', isIndividualOrHuf: false, standard194cRatePercent: 2.0 }
1308
+ };
1309
+
1310
+ const BASE36_ALPHANUMERIC = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
1311
+
1312
+ /**
1313
+ * Validates the check digit of a 15-character Goods and Services Tax Identification Number (GSTIN)
1314
+ * using the official ISO/IEC 7064 Mod 36, 36 check digit algorithm.
1315
+ *
1316
+ * Offline, zero-dependency, and deterministic. Requires zero network calls.
1317
+ *
1318
+ * @param {string} gstin 15-character alphanumeric GSTIN string
1319
+ * @returns {boolean} True if the 15th character exactly matches the calculated check character
1320
+ */
1321
+ export function isValidGstinChecksum(gstin) {
1322
+ if (!gstin || typeof gstin !== 'string') return false;
1323
+ const clean = gstin.trim().toUpperCase();
1324
+
1325
+ // Structural syntax: State (2 digits) + PAN (10 chars) + Entity (1 char) + Z (1 char) + Check (1 char)
1326
+ const pattern = /^\d{2}[A-Z]{5}\d{4}[A-Z][A-Z\d]Z[A-Z\d]$/;
1327
+ if (!pattern.test(clean)) return false;
1328
+
1329
+ let total = 0;
1330
+ for (let i = 0; i < 14; i++) {
1331
+ const val = BASE36_ALPHANUMERIC.indexOf(clean[i]);
1332
+ if (val === -1) return false;
1333
+ const factor = (i % 2 === 0) ? 1 : 2;
1334
+ const product = val * factor;
1335
+ total += Math.floor(product / 36) + (product % 36);
1336
+ }
1337
+
1338
+ const expectedIndex = (36 - (total % 36)) % 36;
1339
+ const expectedChar = BASE36_ALPHANUMERIC[expectedIndex];
1340
+ return clean[14] === expectedChar;
1341
+ }
1342
+
1343
+ /**
1344
+ * Deterministically decodes an Indian Tax Identifier (PAN or GSTIN).
1345
+ *
1346
+ * Capabilities:
1347
+ * - Structural regular expression pattern validation
1348
+ * - Offline ISO/IEC 7064 Mod 36, 36 checksum calculation for GSTIN
1349
+ * - State code and name extraction from GSTIN prefix
1350
+ * - Embedded 10-character PAN extraction from GSTIN positions 3..12
1351
+ * - Statutory legal entity classification via 4th character of PAN (Individual, HUF, Company, Firm, etc.)
1352
+ * - Statutory Section 194C withholding rate derivation (1% for Individual/HUF vs 2% for Other Entities)
1353
+ *
1354
+ * Zero dependencies, 0-neuron, zero network calls.
1355
+ *
1356
+ * @param {string} rawId PAN (10 characters) or GSTIN (15 characters)
1357
+ * @returns {Object} Decoded tax entity metadata
1358
+ */
1359
+ export function decodeTaxIdentifier(rawId) {
1360
+ if (!rawId || typeof rawId !== 'string') {
1361
+ return {
1362
+ isValid: false,
1363
+ error: 'Tax identifier must be a non-empty string.'
1364
+ };
1365
+ }
1366
+
1367
+ const clean = rawId.trim().toUpperCase();
1368
+
1369
+ // 1. Process 15-character GSTIN
1370
+ if (clean.length === 15) {
1371
+ const pattern = /^\d{2}[A-Z]{5}\d{4}[A-Z][A-Z\d]Z[A-Z\d]$/;
1372
+ if (!pattern.test(clean)) {
1373
+ return {
1374
+ isValid: false,
1375
+ type: 'GSTIN',
1376
+ gstin: clean,
1377
+ error: 'Malformed GSTIN format: expected 2 digits state + 10 chars PAN + 1 entity char + Z + 1 check char.'
1378
+ };
1379
+ }
1380
+
1381
+ const isChecksumValid = isValidGstinChecksum(clean);
1382
+ const stateCode = clean.slice(0, 2);
1383
+ const pan = clean.slice(2, 12);
1384
+ const entityCode = pan[3];
1385
+ const entityMeta = PAN_ENTITY_TYPES[entityCode] || {
1386
+ code: entityCode,
1387
+ name: 'Other Entity / Non-Individual',
1388
+ isIndividualOrHuf: false,
1389
+ standard194cRatePercent: 2.0
1390
+ };
1391
+
1392
+ return {
1393
+ isValid: isChecksumValid,
1394
+ checksumValid: isChecksumValid,
1395
+ type: 'GSTIN',
1396
+ gstin: clean,
1397
+ pan,
1398
+ stateCode,
1399
+ stateName: INDIAN_STATE_CODES[stateCode] || 'Unknown Territory',
1400
+ entityCode,
1401
+ entityType: entityMeta.name,
1402
+ isIndividualOrHuf: entityMeta.isIndividualOrHuf,
1403
+ standard194cRatePercent: entityMeta.standard194cRatePercent,
1404
+ error: isChecksumValid ? null : 'GSTIN checksum mismatch under ISO/IEC 7064 Mod 36, 36.'
1405
+ };
1406
+ }
1407
+
1408
+ // 2. Process 10-character PAN
1409
+ if (clean.length === 10) {
1410
+ const panPattern = /^[A-Z]{5}\d{4}[A-Z]$/;
1411
+ if (!panPattern.test(clean)) {
1412
+ return {
1413
+ isValid: false,
1414
+ type: 'PAN',
1415
+ pan: clean,
1416
+ error: 'Malformed PAN format: expected 5 letters + 4 digits + 1 letter.'
1417
+ };
1418
+ }
1419
+
1420
+ const entityCode = clean[3];
1421
+ const entityMeta = PAN_ENTITY_TYPES[entityCode] || {
1422
+ code: entityCode,
1423
+ name: 'Other Entity / Non-Individual',
1424
+ isIndividualOrHuf: false,
1425
+ standard194cRatePercent: 2.0
1426
+ };
1427
+
1428
+ return {
1429
+ isValid: true,
1430
+ type: 'PAN',
1431
+ pan: clean,
1432
+ entityCode,
1433
+ entityType: entityMeta.name,
1434
+ isIndividualOrHuf: entityMeta.isIndividualOrHuf,
1435
+ standard194cRatePercent: entityMeta.standard194cRatePercent,
1436
+ error: null
1437
+ };
1438
+ }
1439
+
1440
+ return {
1441
+ isValid: false,
1442
+ error: `Invalid identifier length (${clean.length}): Expected 10 characters for PAN or 15 characters for GSTIN.`
1443
+ };
1444
+ }
1445
+
package/types/index.d.ts CHANGED
@@ -190,3 +190,24 @@ export declare class StatutoryClient {
190
190
  checkUpdates(): Promise<UpdateInfo>;
191
191
  sync(): Promise<SyncResult>;
192
192
  }
193
+
194
+ export interface TaxIdentifierResult {
195
+ isValid: boolean;
196
+ checksumValid?: boolean;
197
+ type?: 'GSTIN' | 'PAN';
198
+ gstin?: string;
199
+ pan?: string;
200
+ stateCode?: string;
201
+ stateName?: string;
202
+ entityCode?: string;
203
+ entityType?: string;
204
+ isIndividualOrHuf?: boolean;
205
+ standard194cRatePercent?: number;
206
+ error?: string | null;
207
+ }
208
+
209
+ export declare const INDIAN_STATE_CODES: Record<string, string>;
210
+ export declare const PAN_ENTITY_TYPES: Record<string, { code: string; name: string; isIndividualOrHuf: boolean; standard194cRatePercent: number }>;
211
+ export declare function isValidGstinChecksum(gstin: string): boolean;
212
+ export declare function decodeTaxIdentifier(rawId: string): TaxIdentifierResult;
213
+