@mudraforge/statutory 2.9.0 → 2.10.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/CHANGELOG.md +20 -0
- package/README.md +105 -124
- package/SKILL.md +7 -2
- package/bin/cli.js +215 -8
- package/docs/ARCHITECTURE.md +8 -8
- package/docs/ARCHITECTURE_DECISIONS.md +4 -3
- package/docs/CATALOG.md +1 -1
- package/docs/CORPUS_QUALITY.md +5 -5
- package/docs/DOMAIN_BREADTH_AND_DEPTH_COMPARISON.md +1 -1
- package/docs/ENTERPRISE_GOVERNANCE.md +81 -22
- package/docs/PILOT_EVALUATION_GUIDE.md +155 -90
- package/docs/PROVENANCE.md +4 -4
- package/docs/VERIFICATION.md +7 -7
- package/okf/manifest.json +1 -1
- package/package.json +18 -1
- package/src/constants.js +10 -0
- package/src/index.js +92 -13
- package/src/outcomes.js +483 -0
- package/types/index.d.ts +55 -0
package/src/index.js
CHANGED
|
@@ -23,12 +23,27 @@ import {
|
|
|
23
23
|
evaluateSection79LossContinuity,
|
|
24
24
|
revalueForeignExchangeIndAs109
|
|
25
25
|
} from './calculators.js';
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
import {
|
|
27
|
+
getStatutoryOutcomes,
|
|
28
|
+
explainStatutoryDisposition,
|
|
29
|
+
STATUTORY_OUTCOMES_DICTIONARY,
|
|
30
|
+
enrichWithOutcomeExplanation
|
|
31
|
+
} from './outcomes.js';
|
|
32
|
+
|
|
33
|
+
import { STATUTORY_CLIENT_VERSION, DEFAULT_BASE_URL } from './constants.js';
|
|
34
|
+
|
|
35
|
+
let defaultOkfDir = null;
|
|
36
|
+
try {
|
|
37
|
+
if (typeof import.meta?.url === 'string' && import.meta.url.startsWith('file:')) {
|
|
38
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
39
|
+
const __dirname = path.dirname(__filename);
|
|
40
|
+
defaultOkfDir = path.resolve(__dirname, '..', 'okf');
|
|
41
|
+
}
|
|
42
|
+
} catch (_) {
|
|
43
|
+
defaultOkfDir = null;
|
|
44
|
+
}
|
|
45
|
+
export const DEFAULT_LOCAL_OKF_DIR = defaultOkfDir;
|
|
46
|
+
export { STATUTORY_CLIENT_VERSION, DEFAULT_BASE_URL };
|
|
32
47
|
|
|
33
48
|
/**
|
|
34
49
|
* Pinned official release Merkle root digests.
|
|
@@ -886,7 +901,11 @@ export async function checkUpdates(options = {}) {
|
|
|
886
901
|
* @returns {Promise<{ updateAvailable: boolean, currentVersion: string, latestVersion: string } | null>}
|
|
887
902
|
*/
|
|
888
903
|
export async function checkForPackageUpdate(options = {}) {
|
|
889
|
-
if (!options.force && (
|
|
904
|
+
if (!options.force && (
|
|
905
|
+
process.env.NO_UPDATE_NOTIFIER === '1' ||
|
|
906
|
+
process.env.MUDRA_NO_UPDATE_NOTIFIER === '1' ||
|
|
907
|
+
(process.env.CI === 'true' && !options.registryUrl)
|
|
908
|
+
)) {
|
|
890
909
|
return null;
|
|
891
910
|
}
|
|
892
911
|
|
|
@@ -922,7 +941,9 @@ export async function checkForPackageUpdate(options = {}) {
|
|
|
922
941
|
latestVersion = json.version;
|
|
923
942
|
if (cacheTtlMs > 0) {
|
|
924
943
|
try {
|
|
944
|
+
const prev = fs.existsSync(cacheFile) ? JSON.parse(fs.readFileSync(cacheFile, 'utf8')) : {};
|
|
925
945
|
fs.writeFileSync(cacheFile, JSON.stringify({
|
|
946
|
+
...prev,
|
|
926
947
|
lastChecked: Date.now(),
|
|
927
948
|
latestVersion
|
|
928
949
|
}), 'utf8');
|
|
@@ -939,13 +960,44 @@ export async function checkForPackageUpdate(options = {}) {
|
|
|
939
960
|
if (!latestVersion) return null;
|
|
940
961
|
|
|
941
962
|
const cmp = compareSemver(latestVersion, currentVersion);
|
|
963
|
+
const isIgnored = Boolean(cached?.ignoredVersion && cached.ignoredVersion === latestVersion);
|
|
964
|
+
|
|
942
965
|
return {
|
|
943
|
-
updateAvailable: cmp > 0,
|
|
966
|
+
updateAvailable: cmp > 0 && (!isIgnored || Boolean(options.force)),
|
|
967
|
+
isIgnored,
|
|
944
968
|
currentVersion,
|
|
945
969
|
latestVersion
|
|
946
970
|
};
|
|
947
971
|
}
|
|
948
972
|
|
|
973
|
+
/**
|
|
974
|
+
* Configure the CLI to ignore update notifications for a specific version.
|
|
975
|
+
* @param {string} versionToIgnore Package version to suppress notifications for
|
|
976
|
+
* @returns {{ status: 'SUCCESS' | 'ERROR', ignoredVersion: string, error?: string }}
|
|
977
|
+
*/
|
|
978
|
+
export function ignorePackageUpdate(versionToIgnore) {
|
|
979
|
+
if (!versionToIgnore || typeof versionToIgnore !== 'string') {
|
|
980
|
+
throw new Error('[@mudraforge/statutory] A valid version string is required to ignore update notifications.');
|
|
981
|
+
}
|
|
982
|
+
const cacheFile = path.join(os.tmpdir(), '.mudraforge_statutory_npm_update.json');
|
|
983
|
+
let current = {};
|
|
984
|
+
try {
|
|
985
|
+
if (fs.existsSync(cacheFile)) {
|
|
986
|
+
current = JSON.parse(fs.readFileSync(cacheFile, 'utf8')) || {};
|
|
987
|
+
}
|
|
988
|
+
} catch (_) {}
|
|
989
|
+
|
|
990
|
+
current.ignoredVersion = versionToIgnore.trim();
|
|
991
|
+
current.ignoredAt = Date.now();
|
|
992
|
+
|
|
993
|
+
try {
|
|
994
|
+
fs.writeFileSync(cacheFile, JSON.stringify(current, null, 2), 'utf8');
|
|
995
|
+
return { status: 'SUCCESS', ignoredVersion: versionToIgnore.trim() };
|
|
996
|
+
} catch (err) {
|
|
997
|
+
return { status: 'ERROR', ignoredVersion: versionToIgnore.trim(), error: err.message };
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
949
1001
|
/**
|
|
950
1002
|
* Download and synchronize the latest OKF knowledge bundle to disk.
|
|
951
1003
|
* Hardened with:
|
|
@@ -1574,9 +1626,10 @@ export class StatutoryClient {
|
|
|
1574
1626
|
async calculate(payload = {}, options = {}) {
|
|
1575
1627
|
if (!this.apiKey) {
|
|
1576
1628
|
const err = new Error(
|
|
1577
|
-
'[@mudraforge/statutory]
|
|
1578
|
-
'
|
|
1579
|
-
'
|
|
1629
|
+
'[@mudraforge/statutory] The hosted cloud calculation endpoint (\'/api/v1/statutory/calculate\') requires an Enterprise API key.\n' +
|
|
1630
|
+
'To calculate deterministically offline without an API key or remote server, import pure calculation functions directly:\n' +
|
|
1631
|
+
' import { calculateCorporateTaxMatVsConcessional, evaluatePlaceOfSupplyAndLut, calculateTransferPricingAlp, evaluateSection79LossContinuity, revalueForeignExchangeIndAs109 } from \'@mudraforge/statutory\';\n' +
|
|
1632
|
+
'For hosted cloud edge calculation access or custom enterprise models, contact hello@mudraforge.in or support@mudraforge.in.'
|
|
1580
1633
|
);
|
|
1581
1634
|
err.code = 'API_KEY_REQUIRED';
|
|
1582
1635
|
err.contact = 'hello@mudraforge.in';
|
|
@@ -1656,7 +1709,8 @@ export class StatutoryClient {
|
|
|
1656
1709
|
throw err;
|
|
1657
1710
|
}
|
|
1658
1711
|
|
|
1659
|
-
|
|
1712
|
+
const parsedResult = await safeJsonParse(res, 'StatutoryClient.calculate');
|
|
1713
|
+
return enrichWithOutcomeExplanation(parsedResult, { ...options, verbose: options.verbose || payload.verbose });
|
|
1660
1714
|
}
|
|
1661
1715
|
|
|
1662
1716
|
/**
|
|
@@ -1750,6 +1804,27 @@ export class StatutoryClient {
|
|
|
1750
1804
|
revalueForeignExchangeIndAs109(payload) {
|
|
1751
1805
|
return revalueForeignExchangeIndAs109(payload);
|
|
1752
1806
|
}
|
|
1807
|
+
|
|
1808
|
+
/**
|
|
1809
|
+
* Retrieve statutory outcomes and disposition codes.
|
|
1810
|
+
* @param {string} [provision] Optional section code or provision alias
|
|
1811
|
+
* @param {Object} [options]
|
|
1812
|
+
*/
|
|
1813
|
+
async getOutcomes(provision = null, options = {}) {
|
|
1814
|
+
return await getStatutoryOutcomes(provision, {
|
|
1815
|
+
...options,
|
|
1816
|
+
baseUrl: this.baseUrl
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
/**
|
|
1821
|
+
* Retrieve in-depth explanation and remediation steps for a disposition code.
|
|
1822
|
+
* @param {string} codeOrQuery Disposition code (e.g. 'BUYER_TURNOVER_UNVERIFIED', 'INDETERMINATE')
|
|
1823
|
+
* @param {Object} [options]
|
|
1824
|
+
*/
|
|
1825
|
+
explain(codeOrQuery, options = {}) {
|
|
1826
|
+
return explainStatutoryDisposition(codeOrQuery, options);
|
|
1827
|
+
}
|
|
1753
1828
|
}
|
|
1754
1829
|
|
|
1755
1830
|
/**
|
|
@@ -2752,7 +2827,11 @@ export {
|
|
|
2752
2827
|
evaluatePlaceOfSupplyAndLut,
|
|
2753
2828
|
calculateTransferPricingAlp,
|
|
2754
2829
|
evaluateSection79LossContinuity,
|
|
2755
|
-
revalueForeignExchangeIndAs109
|
|
2830
|
+
revalueForeignExchangeIndAs109,
|
|
2831
|
+
getStatutoryOutcomes,
|
|
2832
|
+
explainStatutoryDisposition,
|
|
2833
|
+
STATUTORY_OUTCOMES_DICTIONARY,
|
|
2834
|
+
enrichWithOutcomeExplanation
|
|
2756
2835
|
};
|
|
2757
2836
|
|
|
2758
2837
|
|
package/src/outcomes.js
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Canonical Statutory Outcomes & Semantic Disposition Dictionary.
|
|
3
|
+
* Educates developers and autonomous AI agents on all possible non-numerical
|
|
4
|
+
* states, legal preconditions, missing facts, and required downstream actions.
|
|
5
|
+
*
|
|
6
|
+
* Implements hybrid offline-first resolution with graceful edge synchronization.
|
|
7
|
+
*
|
|
8
|
+
* @license MIT
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { STATUTORY_CLIENT_VERSION } from './constants.js';
|
|
12
|
+
|
|
13
|
+
export const STATUTORY_OUTCOMES_DICTIONARY = [
|
|
14
|
+
{
|
|
15
|
+
code: 'DETERMINED_APPLICABLE',
|
|
16
|
+
aliases: ['APPLICABLE', 'STATUTORY_WITHHOLDING_APPLICABLE', 'APPLICABLE_STANDARD_RATE'],
|
|
17
|
+
category: 'STATUTORY_WITHHOLDING',
|
|
18
|
+
title: 'Statutory Obligation Established',
|
|
19
|
+
description: 'Statutory deduction or tax withholding obligation is actively established on the transaction.',
|
|
20
|
+
triggerCondition: 'All statutory criteria met: cumulative FY threshold crossed, buyer/seller eligibility verified, and statute actively in force.',
|
|
21
|
+
recommendedAction: 'Deduct statutory tax at rate in force from basic invoice amount; credit vendor payable net of TDS; credit TDS payable.',
|
|
22
|
+
remedy: 'Proceed with standard invoice booking. Formulate double-entry voucher: Debit Expense, Debit Input GST, Credit Vendor Payable (Net), Credit TDS Payable (Section rate).',
|
|
23
|
+
consequenceIfUnresolved: 'Failure to deduct leads to interest under Section 201(1A) at 1% per month and 30% expenditure disallowance under Section 40(a)(ia).',
|
|
24
|
+
applicableProvisions: ['194Q', '194C', '194J', '194I', '194H', '195', '51', '393'],
|
|
25
|
+
governingAuthority: 'Direct Tax / Central Board of Direct Taxes (CBDT)'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
code: 'DETERMINED_THRESHOLD_NOT_MET',
|
|
29
|
+
aliases: ['EXEMPT', 'BELOW_THRESHOLD', 'EXEMPT_THRESHOLD_NOT_MET', 'THRESHOLD_NOT_CROSSED'],
|
|
30
|
+
category: 'STATUTORY_SAFE_HARBOR',
|
|
31
|
+
title: 'Statutory Spend Below Monetary Threshold',
|
|
32
|
+
description: 'Transaction falls below the legal monetary threshold; no tax withholding is legally required.',
|
|
33
|
+
triggerCondition: 'Cumulative FY purchases under ₹50 Lakhs (Section 194Q), single invoice under ₹30,000 / aggregate under ₹1,00,000 (Section 194C), or aggregate fees under ₹30,000 (Section 194J).',
|
|
34
|
+
recommendedAction: 'Disburse invoice in full; record transaction amount toward cumulative financial year spend tracker for subsequent invoice gates.',
|
|
35
|
+
remedy: 'Disburse payment without tax deduction. Ensure the cumulative spend accumulator for this vendor is incremented in ERP to catch future threshold crossings.',
|
|
36
|
+
consequenceIfUnresolved: 'Over-withholding harms vendor commercial relations, triggers vendor dispute tickets, and requires cumbersome TDS refund processing.',
|
|
37
|
+
applicableProvisions: ['194Q', '194C', '194J', '194H', '393'],
|
|
38
|
+
governingAuthority: 'Direct Tax / Central Board of Direct Taxes (CBDT)'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
code: 'INDETERMINATE_BUYER_TURNOVER_UNVERIFIED',
|
|
42
|
+
aliases: ['BUYER_TURNOVER_UNVERIFIED', 'TURNOVER_UNVERIFIED', 'EXEMPT_BELOW_BUYER_TURNOVER_THRESHOLD'],
|
|
43
|
+
category: 'EVIDENTIARY_AMBIGUITY',
|
|
44
|
+
title: 'Buyer Turnover Precondition Unverified',
|
|
45
|
+
description: 'Preceding financial year buyer turnover is unverified; statutory applicability cannot be determined.',
|
|
46
|
+
triggerCondition: 'Section 194Q requires buyer turnover exceeding ₹10 Crore in the financial year immediately preceding the transaction year (Section 194Q(1) Explanation).',
|
|
47
|
+
recommendedAction: 'Fail closed; hold automated withholding determination until finance or vendor management records verified audited turnover.',
|
|
48
|
+
remedy: 'Supply buyer turnover in Crores INR (buyerTurnoverCr) verified against the company audited financial statements or income tax return for the preceding FY.',
|
|
49
|
+
missingFacts: ['buyerTurnoverCr', 'buyerTurnoverPrecedingFy'],
|
|
50
|
+
missingFactsExplanation: {
|
|
51
|
+
buyerTurnoverCr: {
|
|
52
|
+
type: 'number',
|
|
53
|
+
description: 'Gross turnover / sales / gross receipts of the buyer entity in the financial year preceding the purchase, expressed in Crores INR.',
|
|
54
|
+
example: 15.5,
|
|
55
|
+
source: 'Audited Profit & Loss Statement / Income Tax Return Form ITR-6'
|
|
56
|
+
},
|
|
57
|
+
buyerTurnoverPrecedingFy: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'The financial year identifier for the preceding turnover evaluation.',
|
|
60
|
+
example: '2024-25',
|
|
61
|
+
source: 'Corporate Tax Master'
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
consequenceIfUnresolved: 'Autonomous payment execution halts fail-closed. If turnover actually exceeds ₹10 Cr and TDS is missed, buyer is treated as assessee-in-default.',
|
|
65
|
+
applicableProvisions: ['194Q', '393'],
|
|
66
|
+
governingAuthority: 'Income-tax Act, 1961 Section 194Q(1) Explanation'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
code: 'INDETERMINATE_PENDING_TRANSACTION_DATE',
|
|
70
|
+
aliases: ['TRANSACTION_DATE_REQUIRED_FOR_REGIME_DETERMINATION', 'PENDING_TRANSACTION_DATE', 'MISSING_TRANSACTION_DATE', 'REGIME_DETERMINATION_PENDING'],
|
|
71
|
+
category: 'TEMPORAL_AMBIGUITY',
|
|
72
|
+
title: 'Transaction Timestamp Required for Regime Resolution',
|
|
73
|
+
description: 'Transaction date, credit date, and payment date are missing; statutory governing Act cannot be determined.',
|
|
74
|
+
triggerCondition: 'Dual-act boundary transition: Income-tax Act, 1961 applies to events crystallized on or before 31 March 2026; Income-tax Act, 2025 applies thereafter.',
|
|
75
|
+
recommendedAction: 'Prompt upstream ERP or invoice capture system for invoice booking/credit timestamp before evaluating legal rules.',
|
|
76
|
+
remedy: 'Provide creditDate (date invoice is booked in accounting ledger) or paymentDate (date funds are disbursed). The earlier event crystallizes the governing Act.',
|
|
77
|
+
missingFacts: ['creditDate', 'paymentDate', 'transactionDate'],
|
|
78
|
+
missingFactsExplanation: {
|
|
79
|
+
creditDate: {
|
|
80
|
+
type: 'string (ISO 8601 YYYY-MM-DD)',
|
|
81
|
+
description: 'Date when amount is credited to the account of the payee or suspense account in corporate books.',
|
|
82
|
+
example: '2026-03-25',
|
|
83
|
+
source: 'ERP General Ledger Booking Timestamp'
|
|
84
|
+
},
|
|
85
|
+
paymentDate: {
|
|
86
|
+
type: 'string (ISO 8601 YYYY-MM-DD)',
|
|
87
|
+
description: 'Date of actual cash disbursement, cheque issuance, or bank wire dispatch.',
|
|
88
|
+
example: '2026-04-10',
|
|
89
|
+
source: 'Bank Rail Payment Instruction'
|
|
90
|
+
},
|
|
91
|
+
transactionDate: {
|
|
92
|
+
type: 'string (ISO 8601 YYYY-MM-DD)',
|
|
93
|
+
description: 'Supplier invoice date on the face of the bill.',
|
|
94
|
+
example: '2026-03-20',
|
|
95
|
+
source: 'Vendor Invoice Document Header'
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
consequenceIfUnresolved: 'Statutory determination cannot establish whether return forms belong to Form 26Q (1961 Act) or successor forms under Income-tax Act, 2025.',
|
|
99
|
+
applicableProvisions: ['194Q', '194C', '194J', '194I', '194H', '393'],
|
|
100
|
+
governingAuthority: 'Income-tax Act, 2025 Section 536 read with Section 6 General Clauses Act, 1897'
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
code: 'SUNSET_OMITTED',
|
|
104
|
+
aliases: ['OMITTED', 'SUNSET', 'REPEALED', 'STATUTE_SUNSET_REPEALED'],
|
|
105
|
+
category: 'LEGISLATIVE_SUNSET',
|
|
106
|
+
title: 'Statute Omitted / Repealed by Finance Act',
|
|
107
|
+
description: 'Statutory provision has been omitted or repealed by Parliament; tax deduction or collection under this section is prohibited.',
|
|
108
|
+
triggerCondition: 'Transaction date falls on or after statutory sunset effective date (e.g. Section 206C(1H) omitted w.e.f. 1 April 2025; Section 206AB omitted w.e.f. 1 April 2025).',
|
|
109
|
+
recommendedAction: 'Halt tax withholding or collection under this section; update ERP tax engine to prevent obsolete tax collection.',
|
|
110
|
+
remedy: 'Update vendor master and ERP billing tax codes. For post-1 April 2025 goods transactions, evaluate Section 194Q buyer withholding instead of seller TCS.',
|
|
111
|
+
consequenceIfUnresolved: 'Charging repealed taxes creates illegal collections under Article 265 of the Constitution of India and damages customer commercial trust.',
|
|
112
|
+
applicableProvisions: ['206C(1H)', '206AB'],
|
|
113
|
+
governingAuthority: 'Finance Act, 2024 / Finance Act, 2025'
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
code: 'PENALTY_RATE_MANDATED',
|
|
117
|
+
aliases: ['PENALTY_RATE', 'INOPERATIVE_PAN_PENALTY', 'PAN_PENALTY_FLOOR', 'SECTION_206AA_PENALTY'],
|
|
118
|
+
category: 'STATUTORY_PENALTY_RATE',
|
|
119
|
+
title: 'Mandatory Non-Furnishing Penalty Rate',
|
|
120
|
+
description: 'Higher penalty rate mandatory due to non-furnishing or invalid/inoperative PAN under Section 206AA.',
|
|
121
|
+
triggerCondition: 'Payee PAN missing, invalid ISO Mod-36 checksum, or designated inoperative due to Aadhaar non-linking.',
|
|
122
|
+
recommendedAction: 'Withhold at statutory penalty floor rate (5% for Section 194Q; 20% for Section 194C, 194J, 194I); flag vendor master for PAN rectification.',
|
|
123
|
+
remedy: 'Apply mandatory penalty rate immediately. Concurrently issue automated notification to vendor requesting valid, operative PAN with Aadhaar linkage proof.',
|
|
124
|
+
missingFactsExplanation: {
|
|
125
|
+
pan: {
|
|
126
|
+
type: 'string (10-char alphanumeric)',
|
|
127
|
+
description: 'Permanent Account Number of the vendor with verified ISO Mod-36 syntax and active/operative filing status.',
|
|
128
|
+
example: 'AAACR5055K',
|
|
129
|
+
source: 'Income Tax Department PAN Verification API / Form 60'
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
consequenceIfUnresolved: 'Deducting standard rates on inoperative PANs makes the deductor liable for short-deduction demand plus penal interest under Section 201(1A).',
|
|
133
|
+
applicableProvisions: ['194Q', '194C', '194J', '194I', '194H', '206AA', '393'],
|
|
134
|
+
governingAuthority: 'Income-tax Act, 1961 Section 206AA'
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
code: 'DISQUALIFIED_FROM_ZERO_RATING',
|
|
138
|
+
aliases: ['DISQUALIFIED_ZERO_RATING', 'ZERO_RATING_FAILED', 'EXPORT_ZERO_RATING_DISQUALIFIED'],
|
|
139
|
+
category: 'INDIRECT_TAX_DISQUALIFICATION',
|
|
140
|
+
title: 'Export Zero-Rating Disqualified',
|
|
141
|
+
description: 'Supply disqualified from 0% zero-rated export treatment under Letter of Undertaking (LUT).',
|
|
142
|
+
triggerCondition: 'Non-receipt of convertible foreign exchange or authorized RBI INR within statutory timeline (IGST Sec 2(6)(iv)) or classification as Intermediary (IGST Sec 2(13)).',
|
|
143
|
+
recommendedAction: 'Levy standard 18% Integrated GST (IGST); do not issue zero-rated LUT export invoice.',
|
|
144
|
+
remedy: 'Obtain FIRC (Foreign Inward Remittance Certificate) / BRC or ensure supplier acts as principal service provider rather than intermediary broker.',
|
|
145
|
+
missingFactsExplanation: {
|
|
146
|
+
isConvertibleForexReceivedOrRbiInr: {
|
|
147
|
+
type: 'boolean',
|
|
148
|
+
description: 'Affirmation that payment has been realized in convertible foreign currency or Indian Rupees permitted by RBI.',
|
|
149
|
+
example: true,
|
|
150
|
+
source: 'Bank Swift Inward Advice / EDPMS Inward Remittance'
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
consequenceIfUnresolved: 'Claiming zero-rating without satisfying export conditions results in IGST demand with 18% interest and 100% penalty under Section 122 CGST Act.',
|
|
154
|
+
applicableProvisions: ['IGST-SEC-16-LUT', '8'],
|
|
155
|
+
governingAuthority: 'Integrated Goods and Services Tax Act, 2017 Section 2(6) & Section 16'
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
code: 'REQUIRES_HUMAN_REVIEW',
|
|
159
|
+
aliases: ['HUMAN_REVIEW_REQUIRED', 'COMPOSITE_AMBIGUITY_REVIEW', 'MANUAL_APPROVAL_REQUIRED'],
|
|
160
|
+
category: 'COMPOSITE_AMBIGUITY',
|
|
161
|
+
title: 'Composite Contract Evidentiary Ambiguity',
|
|
162
|
+
description: 'Contractual ambiguity or composite scope of work prevents autonomous deterministic classification.',
|
|
163
|
+
triggerCondition: 'Mixed supplies (Section 8 CGST), composite contracts spanning both goods and on-site engineering labour, or unitemized invoices.',
|
|
164
|
+
recommendedAction: 'Route invoice to corporate tax controller with highlighted ambiguity and evidence requirements; do not auto-disburse.',
|
|
165
|
+
remedy: 'Corporate tax counsel must review the underlying Statement of Work (SOW) to classify the principal supply (e.g. pure works contract vs supply of equipment).',
|
|
166
|
+
consequenceIfUnresolved: 'Misclassifying composite services as goods leads to wrong tax rates, disallowed Input Tax Credits, and cross-statute withholding disputes.',
|
|
167
|
+
applicableProvisions: ['8', '194C', '194J', '17(5)'],
|
|
168
|
+
governingAuthority: 'Central Goods and Services Tax Act, 2017 Section 8'
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
code: 'MISSING_EVIDENCE_HOLD',
|
|
172
|
+
aliases: ['CERTIFICATE_MISSING', 'EVIDENCE_HOLD', 'MISSING_STATUTORY_FILING'],
|
|
173
|
+
category: 'DOCUMENTARY_PRECONDITION',
|
|
174
|
+
title: 'Statutory Exemption Certificate Missing',
|
|
175
|
+
description: 'Statutory prerequisite filing or exemption certificate not on file; automated claim blocked fail-closed.',
|
|
176
|
+
triggerCondition: 'Missing Section 197 lower-withholding certificate, missing Form 10F / TRC for cross-border remittance, or missing Schedule K-3.',
|
|
177
|
+
recommendedAction: 'Place payment authorization lease on hold; issue automated vendor request for required statutory certificate.',
|
|
178
|
+
remedy: 'Upload verified statutory exemption document to tenant vault. For lower withholding, verify certificate validity on Traces portal.',
|
|
179
|
+
missingFacts: ['lowerWithholdingCertificate', 'taxResidencyCertificate', 'form10F', 'scheduleK3'],
|
|
180
|
+
missingFactsExplanation: {
|
|
181
|
+
lowerWithholdingCertificate: {
|
|
182
|
+
type: 'string (Certificate Number)',
|
|
183
|
+
description: 'Assessing Officer certificate issued under Section 197 specifying lower/nil withholding percentage and valid financial year.',
|
|
184
|
+
example: 'CERT197-2025-00982',
|
|
185
|
+
source: 'TRACES Portal Verification API'
|
|
186
|
+
},
|
|
187
|
+
taxResidencyCertificate: {
|
|
188
|
+
type: 'string / file',
|
|
189
|
+
description: 'Tax Residency Certificate (TRC) issued by government of non-resident vendor home country under Section 90(4).',
|
|
190
|
+
example: 'TRC-IRS-FORM-6166',
|
|
191
|
+
source: 'Vendor Tax Department'
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
consequenceIfUnresolved: 'Withholding at concession rates without certificate makes the deductor liable for the full statutory rate difference plus mandatory interest.',
|
|
195
|
+
applicableProvisions: ['195', '194C', 'IRC-901-904'],
|
|
196
|
+
governingAuthority: 'Income-tax Act, 1961 Section 195 & Section 197'
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
code: 'INDETERMINATE_DISPUTE_UNRESOLVED',
|
|
200
|
+
aliases: ['UNRESOLVED_WRITTEN_OBJECTION', 'DISPUTE_CLOCK_PAUSED', 'WRITTEN_OBJECTION_FILED'],
|
|
201
|
+
category: 'COMMERCIAL_DISPUTE_AMBIGUITY',
|
|
202
|
+
title: 'Written Commercial Dispute Objection Pending',
|
|
203
|
+
description: 'Statutory 15-day or 45-day payment clock is paused due to formal written objection filed within 15 days under MSMED Act Section 2(h)(ii).',
|
|
204
|
+
triggerCondition: 'Buyer filed formal written defect notice or commercial objection within 15 days of invoice receipt.',
|
|
205
|
+
recommendedAction: 'Halt statutory interest clock; resolve vendor dispute or obtain defect rectification confirmation.',
|
|
206
|
+
remedy: 'Resolve buyer objection or deliver defect removal notice. Once formal objection is removed or settled, supply objectionRemovalDate to resume statutory clock.',
|
|
207
|
+
missingFacts: ['objectionRemovalDate'],
|
|
208
|
+
missingFactsExplanation: {
|
|
209
|
+
objectionRemovalDate: {
|
|
210
|
+
type: 'string (ISO 8601 YYYY-MM-DD)',
|
|
211
|
+
description: 'Date when the supplier rectifies the defect or buyer formally withdraws written objection.',
|
|
212
|
+
example: '2025-05-10',
|
|
213
|
+
source: 'Dispute Settlement Agreement / Defect Rectification Note'
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
consequenceIfUnresolved: 'Delay in resolving dispute risks retroactive compounding penal interest at 3x RBI rate if objection is ultimately dismissed or deemed invalid.',
|
|
217
|
+
applicableProvisions: ['MSMED-15-16', 'MCA-MSME-1'],
|
|
218
|
+
governingAuthority: 'Micro, Small and Medium Enterprises Development Act, 2006 Section 2(h)(ii)'
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
code: 'INDETERMINATE_RECIPIENT_COUNTRY_REQUIRED',
|
|
222
|
+
aliases: ['MISSING_RECIPIENT_COUNTRY_FOR_DTAA', 'MISSING_COUNTRY', 'DTAA_COUNTRY_REQUIRED'],
|
|
223
|
+
category: 'CROSS_BORDER_TREATY_AMBIGUITY',
|
|
224
|
+
title: 'Recipient Tax Jurisdiction Country Required for DTAA',
|
|
225
|
+
description: 'Cross-border remittance withholding cannot determine DTAA treaty relief without recipient tax residency country.',
|
|
226
|
+
triggerCondition: 'Section 195 remittance payment without recipient country ISO code.',
|
|
227
|
+
recommendedAction: 'Hold remittance authorization; obtain payee tax residency jurisdiction.',
|
|
228
|
+
remedy: 'Provide ISO 3166-1 alpha-2 country code for the non-resident recipient (e.g. US, SG, DE, GB) and verify Tax Residency Certificate (TRC).',
|
|
229
|
+
missingFacts: ['country'],
|
|
230
|
+
missingFactsExplanation: {
|
|
231
|
+
country: {
|
|
232
|
+
type: 'string (ISO 3166-1 2-letter)',
|
|
233
|
+
description: 'Tax residency country of the foreign payee.',
|
|
234
|
+
example: 'US',
|
|
235
|
+
source: 'Tax Residency Certificate (TRC) Form 10F'
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
consequenceIfUnresolved: 'Without country verification, statutory withholding must default to higher domestic rate (typically 20% or 40% plus applicable surcharge).',
|
|
239
|
+
applicableProvisions: ['195'],
|
|
240
|
+
governingAuthority: 'Income-tax Act, 1961 Section 195 read with Section 90'
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
code: 'MSME_DELAYED_PAYMENT_INTEREST_DUE',
|
|
244
|
+
aliases: ['MSMED_INTEREST_DUE', 'PENAL_INTEREST_DUE', 'SECTION_16_INTEREST'],
|
|
245
|
+
category: 'COMMERCIAL_STATUTORY_PENALTY',
|
|
246
|
+
title: 'MSME Statutory Delayed Payment Interest Accrued',
|
|
247
|
+
description: 'Payment settlement exceeds statutory 15-day or 45-day window under Section 15 of MSMED Act, 2006; mandatory interest attached.',
|
|
248
|
+
triggerCondition: 'Payment to verified Micro or Small enterprise delayed past statutory clock (15 days oral / max 45 days written contract).',
|
|
249
|
+
recommendedAction: 'Compute compound monthly interest at 3x RBI repo rate under Section 16; flag Section 43B(h) tax disallowance risk at fiscal year-end.',
|
|
250
|
+
remedy: 'Disburse principal payment immediately to cap interest accrual. Calculate compound penal interest using MSMED Section 16 calculator and book to expense.',
|
|
251
|
+
consequenceIfUnresolved: 'Section 43B(h) permanently disallows income tax expense deduction if unpaid at year-end; MSMED interest is inadmissible as a business deduction under Section 23.',
|
|
252
|
+
applicableProvisions: ['MSMED-15-16', 'MSMED-SEC-22', 'MCA-MSME-1'],
|
|
253
|
+
governingAuthority: 'Micro, Small and Medium Enterprises Development Act, 2006 Sections 15 & 16'
|
|
254
|
+
}
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Retrieve statutory outcomes and disposition codes for developers and AI agents.
|
|
259
|
+
* Evaluates local OKF dictionary first, with optional edge synchronization when online.
|
|
260
|
+
*
|
|
261
|
+
* @param {string} [provision] Optional section code or provision alias (e.g. '194Q', '194C', 'MSMED-15-16')
|
|
262
|
+
* @param {Object} [options]
|
|
263
|
+
* @param {boolean} [options.offline=false] Force offline local evaluation
|
|
264
|
+
* @param {boolean} [options.verbose=false] Include full remediation steps and parameter schemas
|
|
265
|
+
* @param {string} [options.baseUrl] Cloudflare edge base URL (default: https://api.mudraforge.in)
|
|
266
|
+
* @param {number} [options.timeoutMs=2500] Edge request timeout in milliseconds
|
|
267
|
+
* @returns {Promise<{
|
|
268
|
+
* status: 'SUCCESS',
|
|
269
|
+
* source: 'LIVE_EDGE' | 'LOCAL_OKF_SUBSTRATE',
|
|
270
|
+
* provision: string | null,
|
|
271
|
+
* totalOutcomes: number,
|
|
272
|
+
* outcomes: typeof STATUTORY_OUTCOMES_DICTIONARY,
|
|
273
|
+
* version: string,
|
|
274
|
+
* offlineFallback?: boolean
|
|
275
|
+
* }>}
|
|
276
|
+
*/
|
|
277
|
+
export async function getStatutoryOutcomes(provision = null, options = {}) {
|
|
278
|
+
const normProvision = provision ? String(provision).trim().toUpperCase() : null;
|
|
279
|
+
const isOffline = options.offline === true ||
|
|
280
|
+
(typeof process !== 'undefined' && process.env?.MUDRA_OFFLINE === '1');
|
|
281
|
+
|
|
282
|
+
// Local filter function
|
|
283
|
+
const filterLocal = (prov) => {
|
|
284
|
+
if (!prov) return STATUTORY_OUTCOMES_DICTIONARY;
|
|
285
|
+
return STATUTORY_OUTCOMES_DICTIONARY.filter(item => {
|
|
286
|
+
if (!item.applicableProvisions || item.applicableProvisions.length === 0) return true;
|
|
287
|
+
return item.applicableProvisions.some(p => p.toUpperCase() === prov || prov.includes(p.toUpperCase()));
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
if (isOffline) {
|
|
292
|
+
const outcomes = filterLocal(normProvision);
|
|
293
|
+
return {
|
|
294
|
+
status: 'SUCCESS',
|
|
295
|
+
source: 'LOCAL_OKF_SUBSTRATE',
|
|
296
|
+
provision: normProvision,
|
|
297
|
+
totalOutcomes: outcomes.length,
|
|
298
|
+
outcomes,
|
|
299
|
+
version: STATUTORY_CLIENT_VERSION,
|
|
300
|
+
offlineFallback: false
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Attempt edge synchronization
|
|
305
|
+
const baseUrl = options.baseUrl || 'https://api.mudraforge.in';
|
|
306
|
+
const edgeUrl = new URL('/api/v1/statutory/outcomes', baseUrl);
|
|
307
|
+
if (normProvision) {
|
|
308
|
+
edgeUrl.searchParams.set('provision', normProvision);
|
|
309
|
+
}
|
|
310
|
+
if (options.verbose) {
|
|
311
|
+
edgeUrl.searchParams.set('verbose', 'true');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const timeoutMs = options.timeoutMs || 2500;
|
|
316
|
+
const res = await fetch(edgeUrl.toString(), {
|
|
317
|
+
method: 'GET',
|
|
318
|
+
headers: {
|
|
319
|
+
'Accept': 'application/json',
|
|
320
|
+
'User-Agent': `@mudraforge/statutory/${STATUTORY_CLIENT_VERSION} (CLI/SDK Outcomes)`
|
|
321
|
+
},
|
|
322
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if (res.ok) {
|
|
326
|
+
const data = await res.json();
|
|
327
|
+
if (data && Array.isArray(data.outcomes)) {
|
|
328
|
+
return {
|
|
329
|
+
status: 'SUCCESS',
|
|
330
|
+
source: 'LIVE_EDGE',
|
|
331
|
+
provision: normProvision,
|
|
332
|
+
totalOutcomes: data.outcomes.length,
|
|
333
|
+
outcomes: data.outcomes,
|
|
334
|
+
version: data.version || STATUTORY_CLIENT_VERSION,
|
|
335
|
+
offlineFallback: false
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} catch (_) {
|
|
340
|
+
// Fall through cleanly to local dictionary on network error, 404, or timeout
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Graceful fallback to local substrate
|
|
344
|
+
const localOutcomes = filterLocal(normProvision);
|
|
345
|
+
return {
|
|
346
|
+
status: 'SUCCESS',
|
|
347
|
+
source: 'LOCAL_OKF_SUBSTRATE',
|
|
348
|
+
provision: normProvision,
|
|
349
|
+
totalOutcomes: localOutcomes.length,
|
|
350
|
+
outcomes: localOutcomes,
|
|
351
|
+
version: STATUTORY_CLIENT_VERSION,
|
|
352
|
+
offlineFallback: true
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Look up in-depth explanation, legal rationale, and remediation steps for a specific disposition or indeterminate code.
|
|
358
|
+
*
|
|
359
|
+
* @param {string} codeOrQuery Disposition code (e.g. 'BUYER_TURNOVER_UNVERIFIED', 'INDETERMINATE', 'PENALTY_RATE_MANDATED')
|
|
360
|
+
* @param {Object} [options]
|
|
361
|
+
* @returns {Object} Explanation result with matched outcome(s) and remediation instructions
|
|
362
|
+
*/
|
|
363
|
+
export function explainStatutoryDisposition(codeOrQuery, options = {}) {
|
|
364
|
+
if (!codeOrQuery || typeof codeOrQuery !== 'string' || !codeOrQuery.trim()) {
|
|
365
|
+
throw new Error('[@mudraforge/statutory] A valid disposition code or query string is required, e.g. "BUYER_TURNOVER_UNVERIFIED" or "INDETERMINATE".');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const query = codeOrQuery.trim().toUpperCase();
|
|
369
|
+
|
|
370
|
+
// Special case: "INDETERMINATE" returns all indeterminate outcomes
|
|
371
|
+
if (query === 'INDETERMINATE' || query === 'INDETERMINACY') {
|
|
372
|
+
const matched = STATUTORY_OUTCOMES_DICTIONARY.filter(item =>
|
|
373
|
+
item.code.startsWith('INDETERMINATE_') ||
|
|
374
|
+
item.category.includes('AMBIGUITY') ||
|
|
375
|
+
item.code === 'MISSING_EVIDENCE_HOLD' ||
|
|
376
|
+
item.code === 'REQUIRES_HUMAN_REVIEW'
|
|
377
|
+
);
|
|
378
|
+
return {
|
|
379
|
+
status: 'SUCCESS',
|
|
380
|
+
query,
|
|
381
|
+
matchType: 'CATEGORY_GROUP',
|
|
382
|
+
totalMatches: matched.length,
|
|
383
|
+
matches: matched,
|
|
384
|
+
summary: 'All system dispositions where automated tax determination halts fail-closed pending human review or missing evidentiary facts.'
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Search by exact code or registered alias
|
|
389
|
+
let directMatch = STATUTORY_OUTCOMES_DICTIONARY.find(item =>
|
|
390
|
+
item.code === query ||
|
|
391
|
+
(item.aliases && item.aliases.some(a => a.toUpperCase() === query))
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
if (!directMatch) {
|
|
395
|
+
// Search by partial code, alias substring, or missingFact match
|
|
396
|
+
directMatch = STATUTORY_OUTCOMES_DICTIONARY.find(item =>
|
|
397
|
+
item.code.includes(query) ||
|
|
398
|
+
(item.aliases && item.aliases.some(a => a.toUpperCase().includes(query) || query.includes(a.toUpperCase()))) ||
|
|
399
|
+
(item.missingFacts && item.missingFacts.some(f => f.toUpperCase() === query))
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (directMatch) {
|
|
404
|
+
return {
|
|
405
|
+
status: 'SUCCESS',
|
|
406
|
+
query,
|
|
407
|
+
matchType: 'EXACT_OR_KEY_MATCH',
|
|
408
|
+
totalMatches: 1,
|
|
409
|
+
matches: [directMatch],
|
|
410
|
+
remedy: directMatch.remedy,
|
|
411
|
+
missingFactsExplanation: directMatch.missingFactsExplanation || null,
|
|
412
|
+
consequenceIfUnresolved: directMatch.consequenceIfUnresolved || null
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Fuzzy search across titles, descriptions, and categories
|
|
417
|
+
const fuzzyMatches = STATUTORY_OUTCOMES_DICTIONARY.filter(item =>
|
|
418
|
+
item.title.toUpperCase().includes(query) ||
|
|
419
|
+
item.description.toUpperCase().includes(query) ||
|
|
420
|
+
item.category.toUpperCase().includes(query)
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
status: fuzzyMatches.length > 0 ? 'SUCCESS' : 'NOT_FOUND',
|
|
425
|
+
query,
|
|
426
|
+
matchType: fuzzyMatches.length > 0 ? 'FUZZY_MATCH' : 'NO_MATCH',
|
|
427
|
+
totalMatches: fuzzyMatches.length,
|
|
428
|
+
matches: fuzzyMatches,
|
|
429
|
+
availableCodes: STATUTORY_OUTCOMES_DICTIONARY.map(i => i.code)
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Automatically enriches a calculation result with canonical explanations, remediation steps,
|
|
435
|
+
* and missing fact schemas when the result is INDETERMINATE or when verbose is requested.
|
|
436
|
+
*
|
|
437
|
+
* @param {Object} result Calculation response or outcome object
|
|
438
|
+
* @param {Object} [options]
|
|
439
|
+
* @param {boolean} [options.verbose=false] Force rich explanatory fields on all dispositions
|
|
440
|
+
* @param {boolean} [options.explain=false] Alias for verbose
|
|
441
|
+
* @returns {Object} Enriched result object
|
|
442
|
+
*/
|
|
443
|
+
export function enrichWithOutcomeExplanation(result, options = {}) {
|
|
444
|
+
if (!result || typeof result !== 'object') return result;
|
|
445
|
+
|
|
446
|
+
const determination = result.statutoryDetermination || result.status || null;
|
|
447
|
+
const isIndeterminate = determination && (
|
|
448
|
+
determination === 'INDETERMINATE' ||
|
|
449
|
+
determination.startsWith('INDETERMINATE_') ||
|
|
450
|
+
determination === 'DISPUTE_CLOCK_PAUSED'
|
|
451
|
+
);
|
|
452
|
+
const hasReason = Boolean(result.indeterminacyReason);
|
|
453
|
+
const isVerbose = options.verbose === true || options.explain === true;
|
|
454
|
+
|
|
455
|
+
if (isIndeterminate || hasReason || isVerbose) {
|
|
456
|
+
const lookupKey = result.indeterminacyReason || determination;
|
|
457
|
+
if (lookupKey) {
|
|
458
|
+
try {
|
|
459
|
+
const expl = explainStatutoryDisposition(lookupKey);
|
|
460
|
+
if (expl && expl.matches && expl.matches.length > 0) {
|
|
461
|
+
const match = expl.matches[0];
|
|
462
|
+
const shouldAttachExplanation = options.attachExplanationProperty !== false &&
|
|
463
|
+
options.explain !== false &&
|
|
464
|
+
options.includeExplanation !== false;
|
|
465
|
+
if (shouldAttachExplanation && !result.explanation) {
|
|
466
|
+
result.explanation = match.description || result.regimeNotice || result.contingentNotice || null;
|
|
467
|
+
}
|
|
468
|
+
if (!result.remedy && match.remedy) {
|
|
469
|
+
result.remedy = match.remedy;
|
|
470
|
+
}
|
|
471
|
+
if (!result.consequenceIfUnresolved && match.consequenceIfUnresolved) {
|
|
472
|
+
result.consequenceIfUnresolved = match.consequenceIfUnresolved;
|
|
473
|
+
}
|
|
474
|
+
if (!result.missingFactsExplanation && match.missingFactsExplanation) {
|
|
475
|
+
result.missingFactsExplanation = match.missingFactsExplanation;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} catch (_) {}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return result;
|
|
483
|
+
}
|