@mudraforge/statutory 2.4.2 → 2.5.1

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 (58) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +52 -11
  3. package/bin/cli.js +73 -7
  4. package/docs/ARCHITECTURE_DECISIONS.md +13 -9
  5. package/docs/CATALOG.md +164 -0
  6. package/docs/CORPUS_QUALITY.md +9 -6
  7. package/docs/OKF_MAINTENANCE_SOP.md +286 -0
  8. package/docs/PILOT_EVALUATION_GUIDE.md +149 -0
  9. package/okf/corporate_ma_restructuring/slump_sale_50b/circulars/cbdt_notification_68_2021_rule_11uae_fmv.md +1 -1
  10. package/okf/corporate_ma_restructuring/slump_sale_50b/circulars/cbic_notification_12_2017_going_concern_exemption.md +1 -1
  11. package/okf/corporate_ma_restructuring/slump_sale_50b/section.json +3 -3
  12. package/okf/direct_tax/tcs_goods_206c_1h/circulars/finance_act_2025_sunset_memo.md +1 -1
  13. package/okf/direct_tax/tcs_goods_206c_1h/section.json +2 -2
  14. package/okf/direct_tax/tds_commission_194h/circulars/finance_act_2024_rate_cut_memo.md +29 -0
  15. package/okf/direct_tax/tds_commission_194h/forms/form_26q_quarterly_return.json +30 -0
  16. package/okf/direct_tax/tds_commission_194h/section.json +63 -0
  17. package/okf/direct_tax/tds_contractor_194c/circulars/cbdt_circular_23_2017_gst_unbundling.md +27 -0
  18. package/okf/direct_tax/tds_contractor_194c/forms/form_26q_quarterly_return.json +31 -0
  19. package/okf/direct_tax/tds_contractor_194c/forms/transporter_194c_declaration.json +14 -0
  20. package/okf/direct_tax/tds_contractor_194c/section.json +66 -0
  21. package/okf/direct_tax/tds_goods_194q/circulars/cbdt_circular_13_2021_guidelines.md +1 -1
  22. package/okf/direct_tax/tds_goods_194q/circulars/cbdt_circular_23_2017_gst_unbundling.md +1 -1
  23. package/okf/direct_tax/tds_goods_194q/section.json +2 -2
  24. package/okf/direct_tax/tds_goods_393/section.json +1 -1
  25. package/okf/direct_tax/tds_rent_194i/circulars/cbdt_circular_23_2017_gst_unbundling.md +27 -0
  26. package/okf/direct_tax/tds_rent_194i/forms/form_26q_quarterly_return.json +31 -0
  27. package/okf/direct_tax/tds_rent_194i/section.json +63 -0
  28. package/okf/direct_tax/tds_services_194j/circulars/cbdt_circular_03_2020_fts_rate_cut.md +1 -1
  29. package/okf/direct_tax/tds_services_194j/section.json +2 -2
  30. package/okf/indirect_tax_gst/composite_mixed_supplies_sec8/circulars/cbic_circular_47_2018_composite_mixed_supplies.md +41 -0
  31. package/okf/indirect_tax_gst/composite_mixed_supplies_sec8/forms/composite_mixed_supply_assessment_matrix.json +51 -0
  32. package/okf/indirect_tax_gst/composite_mixed_supplies_sec8/section.json +58 -0
  33. package/okf/indirect_tax_gst/itc_eligibility_16_2/circulars/cbic_circular_170_2022_itc_reporting.md +53 -0
  34. package/okf/indirect_tax_gst/itc_eligibility_16_2/forms/gstr_3b_table_4_itc_reversal.json +32 -0
  35. package/okf/indirect_tax_gst/itc_eligibility_16_2/section.json +84 -0
  36. package/okf/indirect_tax_gst/rcm_liability_sec9_3/circulars/cbic_circular_102_2019_rcm_clarifications.md +33 -0
  37. package/okf/indirect_tax_gst/rcm_liability_sec9_3/forms/rcm_self_invoice_payment_voucher.json +65 -0
  38. package/okf/indirect_tax_gst/rcm_liability_sec9_3/section.json +67 -0
  39. package/okf/international_tax/cross_border_remittance_195/circulars/cbdt_circular_03_2022_form10f_electronic.md +31 -0
  40. package/okf/international_tax/cross_border_remittance_195/circulars/supreme_court_engineering_analysis_doctrine.md +41 -0
  41. package/okf/international_tax/cross_border_remittance_195/forms/form_15ca_15cb_compliance_checklist.json +73 -0
  42. package/okf/international_tax/cross_border_remittance_195/section.json +74 -0
  43. package/okf/international_tax/foreign_tax_credit_irc_901_904/section.json +1 -1
  44. package/okf/manifest.json +78 -25
  45. package/okf/msme_commercial/delayed_payments_15_16/section.json +1 -1
  46. package/okf/msme_commercial/enterprise_classification_sec7/circulars/msme_notification_2119_e_composite_criteria.md +37 -0
  47. package/okf/msme_commercial/enterprise_classification_sec7/forms/udyam_composite_calculation_worksheet.json +46 -0
  48. package/okf/msme_commercial/enterprise_classification_sec7/section.json +64 -0
  49. package/okf/msme_commercial/mca_msme_1_filing/circulars/mca_order_2019_specified_companies.md +42 -0
  50. package/okf/msme_commercial/mca_msme_1_filing/forms/form_msme_1_half_yearly_return.json +93 -0
  51. package/okf/msme_commercial/mca_msme_1_filing/section.json +56 -0
  52. package/okf/msme_commercial/statutory_disclosures_sec22/circulars/icai_guidance_note_schedule_iii_trade_payables.md +42 -0
  53. package/okf/msme_commercial/statutory_disclosures_sec22/forms/schedule_iii_msme_aging_table.json +60 -0
  54. package/okf/msme_commercial/statutory_disclosures_sec22/forms/section_23_tax_addback_schedule.json +35 -0
  55. package/okf/msme_commercial/statutory_disclosures_sec22/section.json +65 -0
  56. package/package.json +3 -2
  57. package/src/index.js +288 -37
  58. package/types/index.d.ts +21 -0
@@ -0,0 +1,35 @@
1
+ {
2
+ "formCode": "SECTION_23_TAX_ADDBACK",
3
+ "title": "Section 23 MSMED Interest Non-Deductibility Tax Computation Schedule",
4
+ "statutoryBasis": "Section 23 of MSMED Act, 2006 read with Income-tax Act, 1961 / Act 2025",
5
+ "schema": {
6
+ "assessmentYear": {
7
+ "type": "string",
8
+ "pattern": "^20[0-9]{2}-[0-9]{2}$",
9
+ "required": true
10
+ },
11
+ "reportingCompanyPan": {
12
+ "type": "string",
13
+ "pattern": "^[A-Z]{5}[0-9]{4}[A-Z]{1}$",
14
+ "required": true
15
+ },
16
+ "financialProfitBeforeTaxInr": {
17
+ "type": "number",
18
+ "required": true
19
+ },
20
+ "penalInterestUnderSection16DebitedToPnlInr": {
21
+ "type": "number",
22
+ "required": true,
23
+ "description": "Total compound penal interest debited to Finance Costs in P&L"
24
+ },
25
+ "statutoryAddbackAmountInr": {
26
+ "type": "number",
27
+ "required": true,
28
+ "description": "Strict add-back amount under Section 23 of MSMED Act"
29
+ },
30
+ "adjustedIncomeFromBusinessInr": {
31
+ "type": "number",
32
+ "required": true
33
+ }
34
+ }
35
+ }
@@ -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.1",
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",
@@ -27,6 +27,7 @@
27
27
  "scripts": {
28
28
  "test": "node test.js",
29
29
  "validate": "node scripts/validate_okf.js",
30
+ "citations": "node scripts/verify_citation_links.js",
30
31
  "conformance": "node conformance/test_conformance.js",
31
32
  "quality": "node scripts/corpus_quality_report.js",
32
33
  "example:trace": "node examples/simulate_end_to_end_trace.js",
@@ -36,7 +37,7 @@
36
37
  },
37
38
  "repository": {
38
39
  "type": "git",
39
- "url": "https://github.com/mondeep0123/mudraforge-statutory.git"
40
+ "url": "git+https://github.com/mondeep0123/mudraforge-statutory.git"
40
41
  },
41
42
  "homepage": "https://mudraforge.in/docs",
42
43
  "bugs": {
package/src/index.js CHANGED
@@ -18,13 +18,19 @@ 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.1';
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-3f25fc3941d29de5b8befc0ddb24d1bb2632067f00b200680cf784961616424c', // v2.5.0 Verified Citation Merkle Root (18 Sections across 5 Domains)
29
+ 'sha256-c32be389bfb82a52ffc40670b74103216c45f3b284c66d2b0020097eb36a25f9', // v2.5.0 Merkle Manifest Root (18 Sections across 5 Domains)
30
+ 'sha256-f3b230a0774eb55d607403b5a656bbe2950a32d07145a46f0eb083232ec12986', // v2.6.0 Edge Sync Bundle Root (12 Sections across 5 Domains)
31
+ 'sha256-4e9d9c064e55748dc6389eae1b5cb2fec1ad7f1c811097afd57e4fb0a6f6454e', // v2.6.0 Local Static Manifest Root (12 Sections across 5 Domains)
32
+ 'sha256-b421910e35f4f16321f6a72382377db66a05ead521ff09d18a91ec9d534d0069', // v2.6.0 Edge Sync Bundle Root (11 Sections)
33
+ 'sha256-dbbd3bc618b7807c4e76861329bd8c6903ef7f9cb28efc9a8693abc06915c9c1', // v2.6.0 Local Static Manifest Root (11 Sections)
28
34
  'sha256-7fe98cf432db5d31ee6b65bbb9d77575ce8c9192eec6f318e3ad3382af0ba900', // v2.5.0 Edge Sync Bundle Root
29
35
  'sha256-96089fe2835d0b9493293a1e5205e0b6b46f16771c9380d1f571969b453ba80d', // v2.4.0 Local Static Manifest Root
30
36
  'sha256-d9612074241f7c2cc1d05fa5eee254996f21db56ad1ab71ac36a084ffd4169e1' // Baseline Release Root
@@ -34,14 +40,16 @@ export const PINNED_RELEASE_ROOTS = new Set([
34
40
  * Canonical MudraForge Statutory Root Ed25519 Public Key (SPKI DER Base64).
35
41
  * Used for verifying digital signatures on remote statutory update bundles.
36
42
  */
37
- export const MUDRAFORGE_STATUTORY_ROOT_PUBLIC_KEY = 'MCowBQYDK2VwAyEAw/XsE6QGtQzdq3EUP3xFpPFHu1x87VSNreXmXr6W17s=';
43
+ export const MUDRAFORGE_STATUTORY_ROOT_PUBLIC_KEY = 'MCowBQYDK2VwAyEAYN4ZtkUBOwybN68SiVUI9YMDlEBNiCa1CAgUNx6rXUo=';
44
+ export const LEGACY_STATUTORY_ROOT_PUBLIC_KEY = 'MCowBQYDK2VwAyEAw/XsE6QGtQzdq3EUP3xFpPFHu1x87VSNreXmXr6W17s=';
38
45
 
39
46
  /**
40
47
  * Authoritative set of trusted MudraForge Root Public Keys.
41
48
  * Public keys supplied through options that are not present in this set are rejected.
42
49
  */
43
50
  export const TRUSTED_STATUTORY_PUBLIC_KEYS = new Set([
44
- MUDRAFORGE_STATUTORY_ROOT_PUBLIC_KEY
51
+ MUDRAFORGE_STATUTORY_ROOT_PUBLIC_KEY,
52
+ LEGACY_STATUTORY_ROOT_PUBLIC_KEY
45
53
  ]);
46
54
 
47
55
  /**
@@ -51,7 +59,15 @@ export const TRUSTED_STATUTORY_KEYS_METADATA = {
51
59
  [MUDRAFORGE_STATUTORY_ROOT_PUBLIC_KEY]: {
52
60
  keyId: 'mudraforge-root-2026-v1',
53
61
  algorithm: 'Ed25519',
54
- purpose: 'MudraForge Open Knowledge Format Statutory Root Authority',
62
+ purpose: 'MudraForge Open Knowledge Format Statutory Root Authority (Active)',
63
+ validFrom: '2026-01-01T00:00:00Z',
64
+ validTo: '2027-12-31T23:59:59Z',
65
+ status: 'ACTIVE'
66
+ },
67
+ [LEGACY_STATUTORY_ROOT_PUBLIC_KEY]: {
68
+ keyId: 'mudraforge-root-2026-v0',
69
+ algorithm: 'Ed25519',
70
+ purpose: 'MudraForge Statutory Genesis Anchor (Legacy)',
55
71
  validFrom: '2026-01-01T00:00:00Z',
56
72
  validTo: '2027-12-31T23:59:59Z',
57
73
  status: 'ACTIVE'
@@ -305,6 +321,7 @@ export function lookupLocalStatute(section, okfDir = DEFAULT_LOCAL_OKF_DIR) {
305
321
  }
306
322
 
307
323
  const files = findJsonFilesRecursively(okfDir);
324
+ const records = [];
308
325
  for (const filePath of files) {
309
326
  let data;
310
327
  try {
@@ -312,55 +329,68 @@ export function lookupLocalStatute(section, okfDir = DEFAULT_LOCAL_OKF_DIR) {
312
329
  } catch (_) {
313
330
  continue;
314
331
  }
315
-
316
- // Must be a valid statutory record with sectionCode
317
332
  if (!data || !data.sectionCode || typeof data.sectionCode !== 'string') continue;
333
+ records.push({ filePath, data });
334
+ }
335
+
336
+ let matchedRecord = null;
318
337
 
319
- let isMatch = false;
338
+ // Pass 1: Authoritative exact match on sectionCode, sectionId, or aliases
339
+ for (const rec of records) {
340
+ const { data } = rec;
320
341
  const secCode = data.sectionCode.toLowerCase();
321
342
  const secId = (data.sectionId && typeof data.sectionId === 'string') ? data.sectionId.toLowerCase() : '';
322
343
 
323
344
  // 1. Direct sectionCode / sectionId match
324
345
  if (secCode === rawClean || (secId && secId === rawClean)) {
325
- isMatch = true;
346
+ matchedRecord = rec;
347
+ break;
326
348
  }
327
349
 
328
350
  // 2. Normalized alphanumeric match (handles "17_5", "17(5)", "17-5", "msmed_15_16")
329
351
  if (
330
- !isMatch && (
331
- secCode.replace(/[()_-\s]+/g, '') === normalized ||
332
- (secId && secId.replace(/[()_-\s]+/g, '') === normalized)
333
- )
352
+ secCode.replace(/[()_-\s]+/g, '') === normalized ||
353
+ (secId && secId.replace(/[()_-\s]+/g, '') === normalized)
334
354
  ) {
335
- isMatch = true;
355
+ matchedRecord = rec;
356
+ break;
336
357
  }
337
358
 
338
359
  // 3. Aliases match
339
360
  if (
340
- !isMatch &&
341
361
  data.aliases &&
342
362
  data.aliases.some(
343
363
  a => a.toLowerCase() === rawClean || a.toLowerCase().replace(/[()_-\s]+/g, '') === normalized
344
364
  )
345
365
  ) {
346
- isMatch = true;
366
+ matchedRecord = rec;
367
+ break;
347
368
  }
369
+ }
348
370
 
349
- // 4. Folder or filename match (handles "tds_goods_194q", "194Q-tds-on-purchase...")
350
- if (!isMatch) {
371
+ // Pass 2: Heuristic folder or filename match (only if no authoritative code match found)
372
+ if (!matchedRecord) {
373
+ const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
374
+ const tokenRegex = new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`, 'i');
375
+ for (const rec of records) {
376
+ const { filePath } = rec;
351
377
  const fileName = path.basename(filePath).toLowerCase().replace(/\.json$/, '');
352
378
  const folderName = path.basename(path.dirname(filePath)).toLowerCase();
379
+
353
380
  if (
354
- fileName.startsWith(rawClean) || fileName.startsWith(normalized) ||
355
- folderName.includes(rawClean) || folderName.includes(normalized)
381
+ fileName.startsWith(`${rawClean}-`) || fileName.startsWith(`${normalized}-`) ||
382
+ tokenRegex.test(folderName) || tokenRegex.test(fileName)
356
383
  ) {
357
- isMatch = true;
384
+ matchedRecord = rec;
385
+ break;
358
386
  }
359
387
  }
388
+ }
360
389
 
361
- if (isMatch) {
362
- // Attach supporting circulars and compliance forms if present in domain hierarchy
363
- const parentDir = path.dirname(filePath);
390
+ if (matchedRecord) {
391
+ const { filePath, data } = matchedRecord;
392
+ // Attach supporting circulars and compliance forms if present in domain hierarchy
393
+ const parentDir = path.dirname(filePath);
364
394
  const circularsDir = path.join(parentDir, 'circulars');
365
395
  const formsDir = path.join(parentDir, 'forms');
366
396
 
@@ -370,7 +400,6 @@ export function lookupLocalStatute(section, okfDir = DEFAULT_LOCAL_OKF_DIR) {
370
400
  };
371
401
 
372
402
  return data;
373
- }
374
403
  }
375
404
  return null;
376
405
  }
@@ -539,7 +568,6 @@ export async function syncOkf(options = {}) {
539
568
  assertSecureEndpoint(baseUrl);
540
569
 
541
570
  const okfDir = options.okfDir || DEFAULT_LOCAL_OKF_DIR;
542
- const sectionsDir = path.join(okfDir, 'sections');
543
571
  const localManifest = getLocalManifest(okfDir);
544
572
 
545
573
  const bundleUrl = new URL('/api/v1/statutory/bundle.json', baseUrl);
@@ -627,6 +655,7 @@ export async function syncOkf(options = {}) {
627
655
  const fileHashes = {};
628
656
  const syncedSectionCodes = [];
629
657
  const seenCodes = new Set();
658
+ const stagedRelPaths = [];
630
659
 
631
660
  for (const statute of bundle.statutes) {
632
661
  // Verify core required statutory fields
@@ -640,20 +669,30 @@ export async function syncOkf(options = {}) {
640
669
  }
641
670
  seenCodes.add(codeKey);
642
671
 
643
- const fileName = resolveSafeStatuteFilename(statute);
644
- const targetPath = path.resolve(stagingDir, fileName);
672
+ // Determine relative deployment target:
673
+ // If statute provides relPath, preserve domain hierarchy (e.g. "direct_tax/tds_goods_194q/section.json")
674
+ // Otherwise, fall back to flat "sections/<filename>.json" for legacy compatibility
675
+ const relTarget = statute.relPath
676
+ ? statute.relPath.replace(/\\/g, '/')
677
+ : path.join('sections', resolveSafeStatuteFilename(statute));
678
+
679
+ const targetPath = path.resolve(stagingDir, relTarget);
645
680
 
646
681
  // Path Traversal Containment Assertion
647
682
  if (!targetPath.startsWith(path.resolve(stagingDir) + path.sep)) {
648
- throw new Error(`[@mudraforge/statutory] Security violation: Path traversal detected in filename "${fileName}".`);
683
+ throw new Error(`[@mudraforge/statutory] Security violation: Path traversal detected in target "${relTarget}".`);
649
684
  }
650
685
 
686
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
687
+
651
688
  const jsonStr = JSON.stringify(statute, null, 2);
652
689
  fs.writeFileSync(targetPath, jsonStr, 'utf8');
653
690
 
691
+ const fileName = resolveSafeStatuteFilename(statute);
654
692
  const fileHash = crypto.createHash('sha256').update(jsonStr).digest('hex');
655
693
  fileHashes[fileName] = fileHash;
656
694
  syncedSectionCodes.push(statute.sectionCode);
695
+ stagedRelPaths.push(relTarget);
657
696
  }
658
697
 
659
698
  // 2.5 Cryptographic Root Merkle Digest Verification
@@ -694,7 +733,14 @@ export async function syncOkf(options = {}) {
694
733
  const isPinned = PINNED_RELEASE_ROOTS.has(bundle.rootHash);
695
734
 
696
735
  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.`);
736
+ const isEdgeNewer = bundle.okfVersion && compareSemver(bundle.okfVersion, STATUTORY_CLIENT_VERSION) >= 0;
737
+ const remediation = isEdgeNewer
738
+ ? `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` +
739
+ ` To receive the updated release roots and trust anchors, update your client:\n` +
740
+ ` npm install @mudraforge/statutory@latest\n` +
741
+ ` npx @mudraforge/statutory@latest sync`
742
+ : `Bundle is neither cryptographically signed by MudraForge Root Authority nor pinned in authorized release roots. Set options.allowUnverifiedRoot=true for local non-production fixtures.`;
743
+ throw new Error(`[@mudraforge/statutory] Security violation: Untrusted bundle rootHash "${bundle.rootHash}". ${remediation}`);
698
744
  }
699
745
  }
700
746
 
@@ -720,19 +766,18 @@ export async function syncOkf(options = {}) {
720
766
  }
721
767
 
722
768
  // 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);
769
+ for (const relTarget of stagedRelPaths) {
770
+ const srcPath = path.join(stagingDir, relTarget);
771
+ const destPath = path.join(okfDir, relTarget);
729
772
 
730
773
  // Safety assertion on destination path containment
731
774
  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}".`);
775
+ if (!resolvedDest.startsWith(path.resolve(okfDir) + path.sep)) {
776
+ throw new Error(`[@mudraforge/statutory] Security violation: Destination path traversal detected in "${relTarget}".`);
734
777
  }
735
778
 
779
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
780
+
736
781
  try {
737
782
  fs.renameSync(srcPath, destPath);
738
783
  } catch {
@@ -742,12 +787,20 @@ export async function syncOkf(options = {}) {
742
787
  }
743
788
 
744
789
  // 4. Write verified manifest
790
+ const manifestDomains = {};
791
+ for (const statute of bundle.statutes) {
792
+ const domain = statute.domain || (statute.relPath ? statute.relPath.split('/')[0] : 'sections');
793
+ if (!manifestDomains[domain]) manifestDomains[domain] = [];
794
+ if (statute.sectionCode) manifestDomains[domain].push(statute.sectionCode);
795
+ }
796
+
745
797
  const manifest = {
746
798
  okfVersion: bundle.okfVersion,
747
799
  publishedAt: bundle.publishedAt || new Date().toISOString(),
748
800
  rootHash: bundle.rootHash,
749
801
  totalSections: bundle.statutes.length,
750
802
  changelog: bundle.changelog || 'Synchronized from Cloudflare Edge API',
803
+ domains: manifestDomains,
751
804
  sections: syncedSectionCodes
752
805
  };
753
806
  fs.writeFileSync(path.join(okfDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
@@ -1193,3 +1246,201 @@ export class StatutoryClient {
1193
1246
  return await syncOkf({ baseUrl: this.baseUrl, okfDir: this.okfDir });
1194
1247
  }
1195
1248
  }
1249
+
1250
+ /**
1251
+ * Standard Indian State & Union Territory Codes (Census 2011 / GSTN 2-digit prefix).
1252
+ */
1253
+ export const INDIAN_STATE_CODES = {
1254
+ '01': 'Jammu & Kashmir',
1255
+ '02': 'Himachal Pradesh',
1256
+ '03': 'Punjab',
1257
+ '04': 'Chandigarh',
1258
+ '05': 'Uttarakhand',
1259
+ '06': 'Haryana',
1260
+ '07': 'Delhi',
1261
+ '08': 'Rajasthan',
1262
+ '09': 'Uttar Pradesh',
1263
+ '10': 'Bihar',
1264
+ '11': 'Sikkim',
1265
+ '12': 'Arunachal Pradesh',
1266
+ '13': 'Nagaland',
1267
+ '14': 'Manipur',
1268
+ '15': 'Mizoram',
1269
+ '16': 'Tripura',
1270
+ '17': 'Meghalaya',
1271
+ '18': 'Assam',
1272
+ '19': 'West Bengal',
1273
+ '20': 'Jharkhand',
1274
+ '21': 'Odisha',
1275
+ '22': 'Chhattisgarh',
1276
+ '23': 'Madhya Pradesh',
1277
+ '24': 'Gujarat',
1278
+ '26': 'Dadra & Nagar Haveli and Daman & Diu',
1279
+ '27': 'Maharashtra',
1280
+ '29': 'Karnataka',
1281
+ '30': 'Goa',
1282
+ '31': 'Lakshadweep',
1283
+ '32': 'Kerala',
1284
+ '33': 'Tamil Nadu',
1285
+ '34': 'Puducherry',
1286
+ '35': 'Andaman & Nicobar Islands',
1287
+ '36': 'Telangana',
1288
+ '37': 'Andhra Pradesh',
1289
+ '38': 'Ladakh',
1290
+ '97': 'Other Territory',
1291
+ '99': 'Centre Jurisdiction'
1292
+ };
1293
+
1294
+ /**
1295
+ * Statutory 4th character legal status classification for Indian Permanent Account Numbers (PAN).
1296
+ * Enacted under Section 139A of the Income-tax Act, 1961 read with Rule 114 of Income Tax Rules.
1297
+ */
1298
+ export const PAN_ENTITY_TYPES = {
1299
+ 'P': { code: 'P', name: 'Individual', isIndividualOrHuf: true, standard194cRatePercent: 1.0 },
1300
+ 'H': { code: 'H', name: 'Hindu Undivided Family (HUF)', isIndividualOrHuf: true, standard194cRatePercent: 1.0 },
1301
+ 'C': { code: 'C', name: 'Company', isIndividualOrHuf: false, standard194cRatePercent: 2.0 },
1302
+ 'F': { code: 'F', name: 'Partnership Firm / LLP', isIndividualOrHuf: false, standard194cRatePercent: 2.0 },
1303
+ 'T': { code: 'T', name: 'Trust', isIndividualOrHuf: false, standard194cRatePercent: 2.0 },
1304
+ 'A': { code: 'A', name: 'Association of Persons (AOP)', isIndividualOrHuf: false, standard194cRatePercent: 2.0 },
1305
+ 'B': { code: 'B', name: 'Body of Individuals (BOI)', isIndividualOrHuf: false, standard194cRatePercent: 2.0 },
1306
+ 'L': { code: 'L', name: 'Local Authority', isIndividualOrHuf: false, standard194cRatePercent: 2.0 },
1307
+ 'J': { code: 'J', name: 'Artificial Juridical Person', isIndividualOrHuf: false, standard194cRatePercent: 2.0 },
1308
+ 'G': { code: 'G', name: 'Government Agency', isIndividualOrHuf: false, standard194cRatePercent: 2.0 }
1309
+ };
1310
+
1311
+ const BASE36_ALPHANUMERIC = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
1312
+
1313
+ /**
1314
+ * Validates the check digit of a 15-character Goods and Services Tax Identification Number (GSTIN)
1315
+ * using the official ISO/IEC 7064 Mod 36, 36 check digit algorithm.
1316
+ *
1317
+ * Offline, zero-dependency, and deterministic. Requires zero network calls.
1318
+ *
1319
+ * @param {string} gstin 15-character alphanumeric GSTIN string
1320
+ * @returns {boolean} True if the 15th character exactly matches the calculated check character
1321
+ */
1322
+ export function isValidGstinChecksum(gstin) {
1323
+ if (!gstin || typeof gstin !== 'string') return false;
1324
+ const clean = gstin.trim().toUpperCase();
1325
+
1326
+ // Structural syntax: State (2 digits) + PAN (10 chars) + Entity (1 char) + Z (1 char) + Check (1 char)
1327
+ const pattern = /^\d{2}[A-Z]{5}\d{4}[A-Z][A-Z\d]Z[A-Z\d]$/;
1328
+ if (!pattern.test(clean)) return false;
1329
+
1330
+ let total = 0;
1331
+ for (let i = 0; i < 14; i++) {
1332
+ const val = BASE36_ALPHANUMERIC.indexOf(clean[i]);
1333
+ if (val === -1) return false;
1334
+ const factor = (i % 2 === 0) ? 1 : 2;
1335
+ const product = val * factor;
1336
+ total += Math.floor(product / 36) + (product % 36);
1337
+ }
1338
+
1339
+ const expectedIndex = (36 - (total % 36)) % 36;
1340
+ const expectedChar = BASE36_ALPHANUMERIC[expectedIndex];
1341
+ return clean[14] === expectedChar;
1342
+ }
1343
+
1344
+ /**
1345
+ * Deterministically decodes an Indian Tax Identifier (PAN or GSTIN).
1346
+ *
1347
+ * Capabilities:
1348
+ * - Structural regular expression pattern validation
1349
+ * - Offline ISO/IEC 7064 Mod 36, 36 checksum calculation for GSTIN
1350
+ * - State code and name extraction from GSTIN prefix
1351
+ * - Embedded 10-character PAN extraction from GSTIN positions 3..12
1352
+ * - Statutory legal entity classification via 4th character of PAN (Individual, HUF, Company, Firm, etc.)
1353
+ * - Statutory Section 194C withholding rate derivation (1% for Individual/HUF vs 2% for Other Entities)
1354
+ *
1355
+ * Zero dependencies, 0-neuron, zero network calls.
1356
+ *
1357
+ * @param {string} rawId PAN (10 characters) or GSTIN (15 characters)
1358
+ * @returns {Object} Decoded tax entity metadata
1359
+ */
1360
+ export function decodeTaxIdentifier(rawId) {
1361
+ if (!rawId || typeof rawId !== 'string') {
1362
+ return {
1363
+ isValid: false,
1364
+ error: 'Tax identifier must be a non-empty string.'
1365
+ };
1366
+ }
1367
+
1368
+ const clean = rawId.trim().toUpperCase();
1369
+
1370
+ // 1. Process 15-character GSTIN
1371
+ if (clean.length === 15) {
1372
+ const pattern = /^\d{2}[A-Z]{5}\d{4}[A-Z][A-Z\d]Z[A-Z\d]$/;
1373
+ if (!pattern.test(clean)) {
1374
+ return {
1375
+ isValid: false,
1376
+ type: 'GSTIN',
1377
+ gstin: clean,
1378
+ error: 'Malformed GSTIN format: expected 2 digits state + 10 chars PAN + 1 entity char + Z + 1 check char.'
1379
+ };
1380
+ }
1381
+
1382
+ const isChecksumValid = isValidGstinChecksum(clean);
1383
+ const stateCode = clean.slice(0, 2);
1384
+ const pan = clean.slice(2, 12);
1385
+ const entityCode = pan[3];
1386
+ const entityMeta = PAN_ENTITY_TYPES[entityCode] || {
1387
+ code: entityCode,
1388
+ name: 'Other Entity / Non-Individual',
1389
+ isIndividualOrHuf: false,
1390
+ standard194cRatePercent: 2.0
1391
+ };
1392
+
1393
+ return {
1394
+ isValid: isChecksumValid,
1395
+ checksumValid: isChecksumValid,
1396
+ type: 'GSTIN',
1397
+ gstin: clean,
1398
+ pan,
1399
+ stateCode,
1400
+ stateName: INDIAN_STATE_CODES[stateCode] || 'Unknown Territory',
1401
+ entityCode,
1402
+ entityType: entityMeta.name,
1403
+ isIndividualOrHuf: entityMeta.isIndividualOrHuf,
1404
+ standard194cRatePercent: entityMeta.standard194cRatePercent,
1405
+ error: isChecksumValid ? null : 'GSTIN checksum mismatch under ISO/IEC 7064 Mod 36, 36.'
1406
+ };
1407
+ }
1408
+
1409
+ // 2. Process 10-character PAN
1410
+ if (clean.length === 10) {
1411
+ const panPattern = /^[A-Z]{5}\d{4}[A-Z]$/;
1412
+ if (!panPattern.test(clean)) {
1413
+ return {
1414
+ isValid: false,
1415
+ type: 'PAN',
1416
+ pan: clean,
1417
+ error: 'Malformed PAN format: expected 5 letters + 4 digits + 1 letter.'
1418
+ };
1419
+ }
1420
+
1421
+ const entityCode = clean[3];
1422
+ const entityMeta = PAN_ENTITY_TYPES[entityCode] || {
1423
+ code: entityCode,
1424
+ name: 'Other Entity / Non-Individual',
1425
+ isIndividualOrHuf: false,
1426
+ standard194cRatePercent: 2.0
1427
+ };
1428
+
1429
+ return {
1430
+ isValid: true,
1431
+ type: 'PAN',
1432
+ pan: clean,
1433
+ entityCode,
1434
+ entityType: entityMeta.name,
1435
+ isIndividualOrHuf: entityMeta.isIndividualOrHuf,
1436
+ standard194cRatePercent: entityMeta.standard194cRatePercent,
1437
+ error: null
1438
+ };
1439
+ }
1440
+
1441
+ return {
1442
+ isValid: false,
1443
+ error: `Invalid identifier length (${clean.length}): Expected 10 characters for PAN or 15 characters for GSTIN.`
1444
+ };
1445
+ }
1446
+
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
+