@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.
- 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/PILOT_EVALUATION_GUIDE.md +149 -0
- 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_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/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/manifest.json +66 -13
- 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 +2 -2
- package/src/index.js +287 -37
- 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.
|
|
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.
|
|
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 = '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
(secId && secId.replace(/[()_-\s]+/g, '') === normalized)
|
|
333
|
-
)
|
|
351
|
+
secCode.replace(/[()_-\s]+/g, '') === normalized ||
|
|
352
|
+
(secId && secId.replace(/[()_-\s]+/g, '') === normalized)
|
|
334
353
|
) {
|
|
335
|
-
|
|
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
|
-
|
|
365
|
+
matchedRecord = rec;
|
|
366
|
+
break;
|
|
347
367
|
}
|
|
368
|
+
}
|
|
348
369
|
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
380
|
+
fileName.startsWith(`${rawClean}-`) || fileName.startsWith(`${normalized}-`) ||
|
|
381
|
+
tokenRegex.test(folderName) || tokenRegex.test(fileName)
|
|
356
382
|
) {
|
|
357
|
-
|
|
383
|
+
matchedRecord = rec;
|
|
384
|
+
break;
|
|
358
385
|
}
|
|
359
386
|
}
|
|
387
|
+
}
|
|
360
388
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
644
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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(
|
|
733
|
-
throw new Error(`[@mudraforge/statutory] Security violation: Destination path traversal detected in "${
|
|
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
|
+
|