@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.
- package/CHANGELOG.md +26 -0
- package/README.md +52 -11
- package/bin/cli.js +73 -7
- package/docs/ARCHITECTURE_DECISIONS.md +13 -9
- package/docs/CATALOG.md +164 -0
- package/docs/CORPUS_QUALITY.md +9 -6
- package/docs/OKF_MAINTENANCE_SOP.md +286 -0
- package/docs/PILOT_EVALUATION_GUIDE.md +149 -0
- package/okf/corporate_ma_restructuring/slump_sale_50b/circulars/cbdt_notification_68_2021_rule_11uae_fmv.md +1 -1
- package/okf/corporate_ma_restructuring/slump_sale_50b/circulars/cbic_notification_12_2017_going_concern_exemption.md +1 -1
- package/okf/corporate_ma_restructuring/slump_sale_50b/section.json +3 -3
- package/okf/direct_tax/tcs_goods_206c_1h/circulars/finance_act_2025_sunset_memo.md +1 -1
- package/okf/direct_tax/tcs_goods_206c_1h/section.json +2 -2
- package/okf/direct_tax/tds_commission_194h/circulars/finance_act_2024_rate_cut_memo.md +29 -0
- package/okf/direct_tax/tds_commission_194h/forms/form_26q_quarterly_return.json +30 -0
- package/okf/direct_tax/tds_commission_194h/section.json +63 -0
- package/okf/direct_tax/tds_contractor_194c/circulars/cbdt_circular_23_2017_gst_unbundling.md +27 -0
- package/okf/direct_tax/tds_contractor_194c/forms/form_26q_quarterly_return.json +31 -0
- package/okf/direct_tax/tds_contractor_194c/forms/transporter_194c_declaration.json +14 -0
- package/okf/direct_tax/tds_contractor_194c/section.json +66 -0
- package/okf/direct_tax/tds_goods_194q/circulars/cbdt_circular_13_2021_guidelines.md +1 -1
- package/okf/direct_tax/tds_goods_194q/circulars/cbdt_circular_23_2017_gst_unbundling.md +1 -1
- package/okf/direct_tax/tds_goods_194q/section.json +2 -2
- package/okf/direct_tax/tds_goods_393/section.json +1 -1
- package/okf/direct_tax/tds_rent_194i/circulars/cbdt_circular_23_2017_gst_unbundling.md +27 -0
- package/okf/direct_tax/tds_rent_194i/forms/form_26q_quarterly_return.json +31 -0
- package/okf/direct_tax/tds_rent_194i/section.json +63 -0
- package/okf/direct_tax/tds_services_194j/circulars/cbdt_circular_03_2020_fts_rate_cut.md +1 -1
- package/okf/direct_tax/tds_services_194j/section.json +2 -2
- package/okf/indirect_tax_gst/composite_mixed_supplies_sec8/circulars/cbic_circular_47_2018_composite_mixed_supplies.md +41 -0
- package/okf/indirect_tax_gst/composite_mixed_supplies_sec8/forms/composite_mixed_supply_assessment_matrix.json +51 -0
- package/okf/indirect_tax_gst/composite_mixed_supplies_sec8/section.json +58 -0
- package/okf/indirect_tax_gst/itc_eligibility_16_2/circulars/cbic_circular_170_2022_itc_reporting.md +53 -0
- package/okf/indirect_tax_gst/itc_eligibility_16_2/forms/gstr_3b_table_4_itc_reversal.json +32 -0
- package/okf/indirect_tax_gst/itc_eligibility_16_2/section.json +84 -0
- package/okf/indirect_tax_gst/rcm_liability_sec9_3/circulars/cbic_circular_102_2019_rcm_clarifications.md +33 -0
- package/okf/indirect_tax_gst/rcm_liability_sec9_3/forms/rcm_self_invoice_payment_voucher.json +65 -0
- package/okf/indirect_tax_gst/rcm_liability_sec9_3/section.json +67 -0
- package/okf/international_tax/cross_border_remittance_195/circulars/cbdt_circular_03_2022_form10f_electronic.md +31 -0
- package/okf/international_tax/cross_border_remittance_195/circulars/supreme_court_engineering_analysis_doctrine.md +41 -0
- package/okf/international_tax/cross_border_remittance_195/forms/form_15ca_15cb_compliance_checklist.json +73 -0
- package/okf/international_tax/cross_border_remittance_195/section.json +74 -0
- package/okf/international_tax/foreign_tax_credit_irc_901_904/section.json +1 -1
- package/okf/manifest.json +78 -25
- package/okf/msme_commercial/delayed_payments_15_16/section.json +1 -1
- package/okf/msme_commercial/enterprise_classification_sec7/circulars/msme_notification_2119_e_composite_criteria.md +37 -0
- package/okf/msme_commercial/enterprise_classification_sec7/forms/udyam_composite_calculation_worksheet.json +46 -0
- package/okf/msme_commercial/enterprise_classification_sec7/section.json +64 -0
- package/okf/msme_commercial/mca_msme_1_filing/circulars/mca_order_2019_specified_companies.md +42 -0
- package/okf/msme_commercial/mca_msme_1_filing/forms/form_msme_1_half_yearly_return.json +93 -0
- package/okf/msme_commercial/mca_msme_1_filing/section.json +56 -0
- package/okf/msme_commercial/statutory_disclosures_sec22/circulars/icai_guidance_note_schedule_iii_trade_payables.md +42 -0
- package/okf/msme_commercial/statutory_disclosures_sec22/forms/schedule_iii_msme_aging_table.json +60 -0
- package/okf/msme_commercial/statutory_disclosures_sec22/forms/section_23_tax_addback_schedule.json +35 -0
- package/okf/msme_commercial/statutory_disclosures_sec22/section.json +65 -0
- package/package.json +3 -2
- package/src/index.js +288 -37
- package/types/index.d.ts +21 -0
package/okf/msme_commercial/statutory_disclosures_sec22/forms/section_23_tax_addback_schedule.json
ADDED
|
@@ -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.
|
|
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.
|
|
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 = '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
(secId && secId.replace(/[()_-\s]+/g, '') === normalized)
|
|
333
|
-
)
|
|
352
|
+
secCode.replace(/[()_-\s]+/g, '') === normalized ||
|
|
353
|
+
(secId && secId.replace(/[()_-\s]+/g, '') === normalized)
|
|
334
354
|
) {
|
|
335
|
-
|
|
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
|
-
|
|
366
|
+
matchedRecord = rec;
|
|
367
|
+
break;
|
|
347
368
|
}
|
|
369
|
+
}
|
|
348
370
|
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
381
|
+
fileName.startsWith(`${rawClean}-`) || fileName.startsWith(`${normalized}-`) ||
|
|
382
|
+
tokenRegex.test(folderName) || tokenRegex.test(fileName)
|
|
356
383
|
) {
|
|
357
|
-
|
|
384
|
+
matchedRecord = rec;
|
|
385
|
+
break;
|
|
358
386
|
}
|
|
359
387
|
}
|
|
388
|
+
}
|
|
360
389
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
644
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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(
|
|
733
|
-
throw new Error(`[@mudraforge/statutory] Security violation: Destination path traversal detected in "${
|
|
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
|
+
|