regsim-in 0.1.0__py3-none-any.whl
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.
- regsim/__init__.py +0 -0
- regsim/cli.py +107 -0
- regsim/commands/__init__.py +0 -0
- regsim/commands/simulate.py +5 -0
- regsim/core/__init__.py +3 -0
- regsim/core/evaluator.py +65 -0
- regsim/core/fields.py +29 -0
- regsim/core/simulation.py +110 -0
- regsim/core/validators.py +9 -0
- regsim/engine.py +55 -0
- regsim/errors.py +0 -0
- regsim/parser.py +36 -0
- regsim/schemas/input.schema.json +4 -0
- regsim/schemas/output.schema.json +92 -0
- regsim/schemas/rule.schema.json +90 -0
- regsim/utils/__init__.py +9 -0
- regsim/utils/extract_call_args.py +10 -0
- regsim/utils/extract_dic.py +11 -0
- regsim/utils/get_func_name.py +8 -0
- regsim_in-0.1.0.dist-info/METADATA +324 -0
- regsim_in-0.1.0.dist-info/RECORD +24 -0
- regsim_in-0.1.0.dist-info/WHEEL +5 -0
- regsim_in-0.1.0.dist-info/entry_points.txt +2 -0
- regsim_in-0.1.0.dist-info/top_level.txt +1 -0
regsim/__init__.py
ADDED
|
File without changes
|
regsim/cli.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# regsim/cli.py
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from importlib import metadata
|
|
9
|
+
|
|
10
|
+
from regsim.commands.simulate import run_simulation
|
|
11
|
+
from regsim.core.simulation import load_json, load_rules, simulate
|
|
12
|
+
from regsim.engine import InvalidPayloadError
|
|
13
|
+
from regsim.parser import extract_from_file
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_version():
|
|
17
|
+
return metadata.version("regsim-in")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def print_error(message: str, exit_code: int = 1) -> None:
|
|
21
|
+
print(json.dumps({
|
|
22
|
+
"status": "ERROR",
|
|
23
|
+
"message": message,
|
|
24
|
+
}))
|
|
25
|
+
sys.exit(exit_code)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
parser = argparse.ArgumentParser(prog="regsim-in")
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--version",
|
|
32
|
+
action="store_true",
|
|
33
|
+
help="Print RegSim-IN version",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
37
|
+
|
|
38
|
+
simulate_parser = subparsers.add_parser("simulate", help="Run regulatory simulation")
|
|
39
|
+
simulate_parser.add_argument("--rules", required=True)
|
|
40
|
+
simulate_parser.add_argument("--input", required=True)
|
|
41
|
+
simulate_parser.add_argument(
|
|
42
|
+
"--snapshot-date",
|
|
43
|
+
help="Regulatory snapshot date (YYYY-MM-DD)",
|
|
44
|
+
required=False,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
args = parser.parse_args()
|
|
48
|
+
|
|
49
|
+
if args.version:
|
|
50
|
+
print(get_version())
|
|
51
|
+
sys.exit(0)
|
|
52
|
+
|
|
53
|
+
if not args.command:
|
|
54
|
+
parser.print_help()
|
|
55
|
+
sys.exit(2)
|
|
56
|
+
|
|
57
|
+
if args.command == "simulate":
|
|
58
|
+
try:
|
|
59
|
+
rules = load_rules(args.rules)
|
|
60
|
+
|
|
61
|
+
if os.path.isdir(args.input):
|
|
62
|
+
all_results = []
|
|
63
|
+
extraction_errors = []
|
|
64
|
+
|
|
65
|
+
for root, dirs, files in os.walk(args.input):
|
|
66
|
+
dirs.sort()
|
|
67
|
+
for filename in sorted(files):
|
|
68
|
+
if not filename.endswith(".py"):
|
|
69
|
+
continue
|
|
70
|
+
file_path = os.path.join(root, filename)
|
|
71
|
+
extraction = extract_from_file(file_path)
|
|
72
|
+
if extraction.errors:
|
|
73
|
+
extraction_errors.extend(extraction.errors)
|
|
74
|
+
continue
|
|
75
|
+
for payload in extraction.payloads:
|
|
76
|
+
all_results.append(
|
|
77
|
+
simulate(rules, payload, snapshot_date=args.snapshot_date)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if extraction_errors:
|
|
81
|
+
print_error("; ".join(extraction_errors), exit_code=2)
|
|
82
|
+
|
|
83
|
+
print(json.dumps(all_results, indent=2))
|
|
84
|
+
sys.exit(1 if any(r["status"] == "FAIL" for r in all_results) else 0)
|
|
85
|
+
|
|
86
|
+
if args.input.endswith(".py"):
|
|
87
|
+
extraction = extract_from_file(args.input)
|
|
88
|
+
|
|
89
|
+
if extraction.errors:
|
|
90
|
+
print_error("; ".join(extraction.errors), exit_code=2)
|
|
91
|
+
|
|
92
|
+
all_results = []
|
|
93
|
+
for payload in extraction.payloads:
|
|
94
|
+
all_results.append(
|
|
95
|
+
simulate(rules, payload, snapshot_date=args.snapshot_date)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
print(json.dumps(all_results, indent=2))
|
|
99
|
+
sys.exit(1 if any(r["status"] == "FAIL" for r in all_results) else 0)
|
|
100
|
+
|
|
101
|
+
payload = load_json(args.input)
|
|
102
|
+
result = simulate(rules, payload, snapshot_date=args.snapshot_date)
|
|
103
|
+
print(json.dumps(result, indent=2))
|
|
104
|
+
except InvalidPayloadError as e:
|
|
105
|
+
print_error(str(e), exit_code=1)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print_error(str(e), exit_code=1)
|
|
File without changes
|
regsim/core/__init__.py
ADDED
regsim/core/evaluator.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from regsim.core.fields import get_field
|
|
2
|
+
from regsim.core.validators import is_invalid_gstin
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def evaluate_condition(condition, payload):
|
|
6
|
+
if "field" not in condition:
|
|
7
|
+
for raw_key, expected in condition.items():
|
|
8
|
+
if raw_key.endswith("_gt"):
|
|
9
|
+
field = raw_key[:-3]
|
|
10
|
+
operator = ">"
|
|
11
|
+
elif raw_key.endswith("_gte"):
|
|
12
|
+
field = raw_key[:-4]
|
|
13
|
+
operator = ">="
|
|
14
|
+
elif raw_key.endswith("_eq"):
|
|
15
|
+
field = raw_key[:-3]
|
|
16
|
+
operator = "=="
|
|
17
|
+
else:
|
|
18
|
+
field = raw_key
|
|
19
|
+
operator = "=="
|
|
20
|
+
|
|
21
|
+
found, field_value = get_field(payload, field)
|
|
22
|
+
if not found:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
if operator == ">":
|
|
27
|
+
if not field_value > expected:
|
|
28
|
+
return False
|
|
29
|
+
elif operator == ">=":
|
|
30
|
+
if not field_value >= expected:
|
|
31
|
+
return False
|
|
32
|
+
elif operator == "==":
|
|
33
|
+
if not field_value == expected:
|
|
34
|
+
return False
|
|
35
|
+
else:
|
|
36
|
+
raise ValueError(f"Unsupported operator: {operator}")
|
|
37
|
+
except TypeError:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
found, field_value = get_field(payload, condition["field"])
|
|
43
|
+
operator = condition["operator"]
|
|
44
|
+
expected = condition["value"]
|
|
45
|
+
|
|
46
|
+
if operator == "missing":
|
|
47
|
+
return not found
|
|
48
|
+
|
|
49
|
+
if operator == "invalid_gstin":
|
|
50
|
+
return not found or is_invalid_gstin(field_value)
|
|
51
|
+
|
|
52
|
+
if not found:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
if operator == ">":
|
|
57
|
+
return field_value > expected
|
|
58
|
+
if operator == ">=":
|
|
59
|
+
return field_value >= expected
|
|
60
|
+
if operator == "==":
|
|
61
|
+
return field_value == expected
|
|
62
|
+
except TypeError:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
raise ValueError(f"Unsupported operator: {operator}")
|
regsim/core/fields.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_field(payload: Any, field_path: str):
|
|
5
|
+
if field_path is None or field_path == "":
|
|
6
|
+
return True, payload
|
|
7
|
+
|
|
8
|
+
current = payload
|
|
9
|
+
for part in field_path.split("."):
|
|
10
|
+
if isinstance(current, dict):
|
|
11
|
+
if part in current:
|
|
12
|
+
current = current[part]
|
|
13
|
+
else:
|
|
14
|
+
return False, None
|
|
15
|
+
continue
|
|
16
|
+
|
|
17
|
+
if isinstance(current, list):
|
|
18
|
+
if part.isdigit():
|
|
19
|
+
idx = int(part)
|
|
20
|
+
if 0 <= idx < len(current):
|
|
21
|
+
current = current[idx]
|
|
22
|
+
else:
|
|
23
|
+
return False, None
|
|
24
|
+
continue
|
|
25
|
+
return False, None
|
|
26
|
+
|
|
27
|
+
return False, None
|
|
28
|
+
|
|
29
|
+
return True, current
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import jsonschema
|
|
6
|
+
|
|
7
|
+
from regsim.core.evaluator import evaluate_condition
|
|
8
|
+
from regsim.engine import InvalidPayloadError, validate_rules
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_json(path: str):
|
|
12
|
+
raw = Path(path).read_text().strip()
|
|
13
|
+
if not raw:
|
|
14
|
+
raise ValueError(f"{path} is empty")
|
|
15
|
+
return json.loads(raw)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_rules(path: str):
|
|
19
|
+
if os.path.isdir(path):
|
|
20
|
+
rules = []
|
|
21
|
+
for root, dirs, files in os.walk(path):
|
|
22
|
+
dirs.sort()
|
|
23
|
+
for filename in sorted(files):
|
|
24
|
+
if not filename.endswith(".json"):
|
|
25
|
+
continue
|
|
26
|
+
file_path = os.path.join(root, filename)
|
|
27
|
+
data = load_json(file_path)
|
|
28
|
+
if isinstance(data, list):
|
|
29
|
+
rules.extend(data)
|
|
30
|
+
else:
|
|
31
|
+
rules.append(data)
|
|
32
|
+
return rules
|
|
33
|
+
|
|
34
|
+
data = load_json(path)
|
|
35
|
+
return data if isinstance(data, list) else [data]
|
|
36
|
+
|
|
37
|
+
def load_schema(schema_name: str) -> dict:
|
|
38
|
+
schema_path = Path(__file__).resolve().parents[1] / "schemas" / schema_name
|
|
39
|
+
raw = schema_path.read_text().strip()
|
|
40
|
+
if not raw:
|
|
41
|
+
raise ValueError(f"{schema_path} is empty")
|
|
42
|
+
return json.loads(raw)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def validate_rules_schema(rules):
|
|
46
|
+
if not isinstance(rules, list):
|
|
47
|
+
raise ValueError("Rules must be a JSON array")
|
|
48
|
+
|
|
49
|
+
schema = load_schema("rule.schema.json")
|
|
50
|
+
for rule in rules:
|
|
51
|
+
try:
|
|
52
|
+
jsonschema.validate(instance=rule, schema=schema)
|
|
53
|
+
except jsonschema.ValidationError as e:
|
|
54
|
+
rule_id = rule.get("rule_id") if isinstance(rule, dict) else None
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"Rule schema validation failed: {e.message} ({rule_id})"
|
|
57
|
+
) from e
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def simulate(rules, payload, snapshot_date=None) -> dict:
|
|
61
|
+
if not isinstance(payload, dict):
|
|
62
|
+
raise InvalidPayloadError("Input payload must be a JSON object")
|
|
63
|
+
|
|
64
|
+
validate_rules_schema(rules)
|
|
65
|
+
validate_rules(rules)
|
|
66
|
+
|
|
67
|
+
violations = []
|
|
68
|
+
applied_rules = []
|
|
69
|
+
|
|
70
|
+
for rule in rules:
|
|
71
|
+
applied_rules.append({
|
|
72
|
+
"rule_id": rule.get("rule_id"),
|
|
73
|
+
"rule_version": rule.get("rule_version"),
|
|
74
|
+
"effective_from": rule.get("effective_from"),
|
|
75
|
+
"source_reference": rule.get("source_reference"),
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
condition = rule["condition"]
|
|
79
|
+
action = rule["action"]
|
|
80
|
+
|
|
81
|
+
if evaluate_condition(condition, payload):
|
|
82
|
+
if action["type"] == "FAIL":
|
|
83
|
+
violations.append({
|
|
84
|
+
"rule_id": rule["rule_id"],
|
|
85
|
+
"rule_version": rule.get("rule_version"),
|
|
86
|
+
"severity": action.get("severity", "HIGH"),
|
|
87
|
+
"message": action["message"],
|
|
88
|
+
"risk": action.get("risk"),
|
|
89
|
+
"source_reference": rule.get("source_reference"),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
status = "FAIL" if violations else "PASS"
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
"status": status,
|
|
96
|
+
"violations": violations,
|
|
97
|
+
"metadata": {
|
|
98
|
+
"engine": "regsim-in",
|
|
99
|
+
"engine_version": "0.1.0",
|
|
100
|
+
"rule_snapshot": snapshot_date,
|
|
101
|
+
"applied_rules": applied_rules,
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def run_simulation(rules_path: str, input_path: str) -> dict:
|
|
107
|
+
rules = load_rules(rules_path)
|
|
108
|
+
payload = load_json(input_path)
|
|
109
|
+
|
|
110
|
+
return simulate(rules, payload)
|
regsim/engine.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
def get_field(payload, field_path):
|
|
2
|
+
"""
|
|
3
|
+
Fetch nested fields like payment.amount.
|
|
4
|
+
Returns a tuple: (found, value)
|
|
5
|
+
"""
|
|
6
|
+
parts = field_path.split(".")
|
|
7
|
+
current = payload
|
|
8
|
+
|
|
9
|
+
for part in parts:
|
|
10
|
+
if not isinstance(current, dict):
|
|
11
|
+
return False, None
|
|
12
|
+
if part not in current:
|
|
13
|
+
return False, None
|
|
14
|
+
current = current[part]
|
|
15
|
+
|
|
16
|
+
return True, current
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InvalidPayloadError(Exception):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
REQUIRED_RULE_FIELDS = [
|
|
24
|
+
"rule_id",
|
|
25
|
+
"rule_version",
|
|
26
|
+
"effective_from",
|
|
27
|
+
"condition",
|
|
28
|
+
"action",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def validate_rules(rules):
|
|
33
|
+
for rule in rules:
|
|
34
|
+
for field in REQUIRED_RULE_FIELDS:
|
|
35
|
+
if field not in rule:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"Rule missing required field: {field} ({rule.get('rule_id')})"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def simulate(rules,payload):
|
|
42
|
+
if not isinstance(payload, dict):
|
|
43
|
+
raise InvalidPayloadError("Input payload must be a JSON object")
|
|
44
|
+
|
|
45
|
+
validate_rules(rules)
|
|
46
|
+
|
|
47
|
+
return{
|
|
48
|
+
# place holder logic
|
|
49
|
+
"status":"PASS",
|
|
50
|
+
"violations":[],
|
|
51
|
+
"metadata":{
|
|
52
|
+
"engine":"regsim-in",
|
|
53
|
+
"version":"0.1.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
regsim/errors.py
ADDED
|
File without changes
|
regsim/parser.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
|
|
3
|
+
from regsim.utils import extract_call_args, extract_dict, get_func_name
|
|
4
|
+
|
|
5
|
+
class ExtractionResult:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.payloads = []
|
|
8
|
+
self.errors = []
|
|
9
|
+
|
|
10
|
+
def extract_from_file(file_path):
|
|
11
|
+
result = ExtractionResult()
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
with open(file_path, "r") as f:
|
|
15
|
+
tree = ast.parse(f.read())
|
|
16
|
+
except Exception as e:
|
|
17
|
+
result.errors.append(str(e))
|
|
18
|
+
return result
|
|
19
|
+
|
|
20
|
+
for node in ast.walk(tree):
|
|
21
|
+
# Detect dict literals assigned to variables
|
|
22
|
+
if isinstance(node, ast.Assign):
|
|
23
|
+
if isinstance(node.value, ast.Dict):
|
|
24
|
+
payload = extract_dict(node.value)
|
|
25
|
+
if payload:
|
|
26
|
+
result.payloads.append(payload)
|
|
27
|
+
|
|
28
|
+
# Detect function calls like payout(...)
|
|
29
|
+
if isinstance(node, ast.Call):
|
|
30
|
+
func_name = get_func_name(node)
|
|
31
|
+
if func_name and "payout" in func_name.lower():
|
|
32
|
+
payload = extract_call_args(node)
|
|
33
|
+
if payload:
|
|
34
|
+
result.payloads.append(payload)
|
|
35
|
+
|
|
36
|
+
return result
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "RegSim-IN Evaluation Result",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["status", "violations"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"status": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"enum": ["PASS", "FAIL", "ERROR"],
|
|
10
|
+
"description": "Overall regulatory evaluation outcome"
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
"violations": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"description": "List of regulatory objections raised during evaluation",
|
|
16
|
+
"items": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"required": [
|
|
19
|
+
"rule_id",
|
|
20
|
+
"rule_version",
|
|
21
|
+
"severity",
|
|
22
|
+
"message"
|
|
23
|
+
],
|
|
24
|
+
"properties": {
|
|
25
|
+
"rule_id": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "Identifier of the violated rule"
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
"rule_version": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Version of the rule that was evaluated"
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
"authority": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"enum": ["CBDT", "GST", "RBI"],
|
|
38
|
+
"description": "Regulatory authority raising the objection"
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
"severity": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"],
|
|
44
|
+
"description": "Regulatory risk level of the violation"
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
"message": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "Human-readable explanation of the violation"
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
"source_reference": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "Legal basis for the violation"
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
"risk": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "Operational or compliance risk summary"
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
"evidence": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"description": "Facts that caused this rule to trigger"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
"metadata": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"description": "Execution metadata (engine info, snapshot, applied rules, etc.)",
|
|
73
|
+
"properties": {
|
|
74
|
+
"engine": { "type": "string" },
|
|
75
|
+
"engine_version": { "type": "string" },
|
|
76
|
+
"rule_snapshot": { "type": ["string", "null"] },
|
|
77
|
+
"applied_rules": {
|
|
78
|
+
"type": "array",
|
|
79
|
+
"items": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"properties": {
|
|
82
|
+
"rule_id": { "type": "string" },
|
|
83
|
+
"rule_version": { "type": "string" },
|
|
84
|
+
"effective_from": { "type": "string", "format": "date" },
|
|
85
|
+
"source_reference": { "type": "string" }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "RegSim-IN Rule Schema",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": [
|
|
6
|
+
"rule_id",
|
|
7
|
+
"rule_version",
|
|
8
|
+
"effective_from",
|
|
9
|
+
"condition",
|
|
10
|
+
"action"
|
|
11
|
+
],
|
|
12
|
+
"properties": {
|
|
13
|
+
"rule_id": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Stable identifier for the regulatory rule"
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
"rule_version": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Semantic or date-based version of the rule"
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
"authority": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"enum": ["CBDT", "GST", "RBI"],
|
|
26
|
+
"description": "Regulatory authority owning this rule"
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
"jurisdiction": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "Geographic or legal scope (e.g. IN, IN-KA, CROSS_BORDER)"
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
"effective_from": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"format": "date",
|
|
37
|
+
"description": "Date from which the rule is legally applicable"
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
"effective_to": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"format": "date",
|
|
43
|
+
"description": "Date until which the rule is applicable"
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
"severity": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"],
|
|
49
|
+
"description": "Risk level from a regulatory perspective"
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
"description": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "Human-readable explanation of the rule intent"
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
"condition": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"description": "Declarative conditions that determine rule applicability"
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
"action": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"required": ["type", "message"],
|
|
65
|
+
"properties": {
|
|
66
|
+
"type": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"enum": ["FAIL", "WARN", "PASS"],
|
|
69
|
+
"description": "Outcome when the rule condition is satisfied"
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
"message": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"description": "Explanation shown to developers or auditors"
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
"evidence_fields": {
|
|
78
|
+
"type": "array",
|
|
79
|
+
"items": { "type": "string" },
|
|
80
|
+
"description": "Fact fields that triggered the rule"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
"source_reference": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"description": "Legal reference (Act section, circular, notification)"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
regsim/utils/__init__.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: regsim-in
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Regulatory Simulation CLI for Indian Backend Systems
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: jsonschema>=4.0.0
|
|
8
|
+
|
|
9
|
+
# RegSim-IN
|
|
10
|
+
|
|
11
|
+
**Regulatory Simulation & Failure Memory CLI for Indian Backend Systems**
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## What is RegSim-IN?
|
|
16
|
+
|
|
17
|
+
**RegSim-IN** is a **developer-first CLI** that simulates Indian regulatory rules (TDS, GST, RBI-style constraints) against backend data flows **before production**.
|
|
18
|
+
|
|
19
|
+
It helps backend teams **detect, explain, and remember regulatory failures** early — during development, testing, and CI — instead of discovering them during audits or incidents.
|
|
20
|
+
|
|
21
|
+
RegSim-IN treats regulation as **executable rules**, not PDFs.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## What Problem Does This Solve?
|
|
26
|
+
|
|
27
|
+
Backend teams frequently:
|
|
28
|
+
|
|
29
|
+
* Ship compliant-looking code that fails under real regulatory edge cases
|
|
30
|
+
* Discover issues late (audits, settlements, reversals)
|
|
31
|
+
* Repeat the *same regulatory mistakes* across services and teams
|
|
32
|
+
|
|
33
|
+
RegSim-IN exists to:
|
|
34
|
+
|
|
35
|
+
* Shift regulatory failures **left**
|
|
36
|
+
* Make rules **explicit and testable**
|
|
37
|
+
* Prevent **repeat regulatory incidents**
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## What RegSim-IN v1 Does
|
|
42
|
+
|
|
43
|
+
Version 1 focuses on **deterministic rule simulation**.
|
|
44
|
+
|
|
45
|
+
RegSim-IN v1 can:
|
|
46
|
+
|
|
47
|
+
* Load regulatory rules defined in **JSON**
|
|
48
|
+
* Run those rules against input payloads (also JSON)
|
|
49
|
+
* Evaluate pass/fail conditions
|
|
50
|
+
* Emit **machine-readable JSON output**
|
|
51
|
+
* Explain *why* a rule failed
|
|
52
|
+
|
|
53
|
+
This makes RegSim-IN suitable for:
|
|
54
|
+
|
|
55
|
+
* Local development checks
|
|
56
|
+
* CI/CD gates
|
|
57
|
+
* Backend design validation
|
|
58
|
+
* Regulatory edge-case exploration
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Example: Detecting a Missed TDS Deduction
|
|
63
|
+
|
|
64
|
+
Consider a backend payout flow where a contractor payment is executed without deducting TDS.
|
|
65
|
+
|
|
66
|
+
**Input payload:**
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"payment": {
|
|
70
|
+
"id": "pay_1029",
|
|
71
|
+
"amount": 45000,
|
|
72
|
+
"vendor_type": "contractor",
|
|
73
|
+
"tds_deducted": false
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Simulation result:**
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"status": "FAIL",
|
|
83
|
+
"violations": [
|
|
84
|
+
{
|
|
85
|
+
"rule_id": "TDS_194C_THRESHOLD",
|
|
86
|
+
"severity": "HIGH",
|
|
87
|
+
"message": "TDS must be deducted under section 194C"
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This allows teams to catch deduction timing and threshold violations **before** payouts reach production systems.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## What RegSim-IN Explicitly Does NOT Do
|
|
98
|
+
|
|
99
|
+
To avoid misuse or false confidence, RegSim-IN v1 does **not**:
|
|
100
|
+
|
|
101
|
+
* Provide legal, tax, or regulatory advice
|
|
102
|
+
* File or generate GST / TDS / RBI reports
|
|
103
|
+
* Integrate with government, bank, or tax APIs
|
|
104
|
+
* Automatically update rules from circulars
|
|
105
|
+
* Fully simulate async systems (queues, retries, persistent state)
|
|
106
|
+
|
|
107
|
+
**This is a simulation tool, not a compliance authority.**
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Supported Languages
|
|
112
|
+
|
|
113
|
+
* **Python** (v1)
|
|
114
|
+
|
|
115
|
+
The CLI is language-agnostic, but rule evaluation currently targets Python-style backend data models.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Rule Format
|
|
120
|
+
|
|
121
|
+
Rules are defined in **JSON**.
|
|
122
|
+
|
|
123
|
+
Design goals:
|
|
124
|
+
|
|
125
|
+
* Explicit structure
|
|
126
|
+
* Deterministic evaluation
|
|
127
|
+
* Easy diffing & review
|
|
128
|
+
* CI/CD friendliness
|
|
129
|
+
|
|
130
|
+
### Example Rule
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"rule_id": "TDS_194C_THRESHOLD",
|
|
135
|
+
"rule_version": "1.0",
|
|
136
|
+
"effective_from": "2024-04-01",
|
|
137
|
+
"description": "TDS applies if contractor payment exceeds threshold",
|
|
138
|
+
"condition": {
|
|
139
|
+
"field": "payment.amount",
|
|
140
|
+
"operator": ">",
|
|
141
|
+
"value": 30000
|
|
142
|
+
},
|
|
143
|
+
"action": {
|
|
144
|
+
"type": "FAIL",
|
|
145
|
+
"message": "TDS must be deducted under section 194C"
|
|
146
|
+
},
|
|
147
|
+
"source_reference": "Income Tax Act - Section 194C"
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Input Format
|
|
154
|
+
|
|
155
|
+
Inputs represent **backend payloads or traces**, also in JSON.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Output Format
|
|
160
|
+
|
|
161
|
+
All outputs are **JSON only**.
|
|
162
|
+
|
|
163
|
+
Example failure output:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"status": "FAIL",
|
|
168
|
+
"violations": [
|
|
169
|
+
{
|
|
170
|
+
"rule_id": "TDS_194C_THRESHOLD",
|
|
171
|
+
"severity": "HIGH",
|
|
172
|
+
"message": "TDS must be deducted under section 194C"
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
"metadata": {
|
|
176
|
+
"engine": "regsim-in",
|
|
177
|
+
"engine_version": "0.1.0",
|
|
178
|
+
"rule_snapshot": "2024-04-01",
|
|
179
|
+
"applied_rules": [
|
|
180
|
+
{
|
|
181
|
+
"rule_id": "TDS_194C_THRESHOLD",
|
|
182
|
+
"rule_version": "1.0",
|
|
183
|
+
"effective_from": "2024-04-01",
|
|
184
|
+
"source_reference": "Income Tax Act - Section 194C"
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This makes RegSim-IN suitable for automation and tooling.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Error Output
|
|
196
|
+
|
|
197
|
+
Errors are always returned as JSON and never include a Python traceback:
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"status": "ERROR",
|
|
202
|
+
"message": "Rule validation failed: missing field 'action'"
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Installation (Early Prototype)
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pip install regsim-in
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
> RegSim-IN is under active development.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Usage (v1)
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
regsim-in simulate \
|
|
222
|
+
--rules rules/ \
|
|
223
|
+
--input input.json
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Current v1 behavior:
|
|
227
|
+
|
|
228
|
+
* CLI initializes correctly
|
|
229
|
+
* Rules are parsed and validated
|
|
230
|
+
* Simulation runs deterministically
|
|
231
|
+
* JSON output is emitted
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Project Layout (Current)
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
regsim/
|
|
239
|
+
cli.py
|
|
240
|
+
commands/
|
|
241
|
+
simulate.py
|
|
242
|
+
core/
|
|
243
|
+
evaluator.py
|
|
244
|
+
fields.py
|
|
245
|
+
simulation.py
|
|
246
|
+
validators.py
|
|
247
|
+
schemas/
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The CLI stays thin, while core rule evaluation lives under `regsim/core/`.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Rule Versioning & Regulatory Drift
|
|
255
|
+
|
|
256
|
+
RegSim-IN v1 supports **explicit rule versioning**:
|
|
257
|
+
|
|
258
|
+
* Rules declare:
|
|
259
|
+
|
|
260
|
+
* `rule_version`
|
|
261
|
+
* `effective_from`
|
|
262
|
+
* `source_reference`
|
|
263
|
+
* No rule updates happen implicitly
|
|
264
|
+
* Simulations are always tied to a known regulatory snapshot
|
|
265
|
+
|
|
266
|
+
This ensures:
|
|
267
|
+
|
|
268
|
+
* Reproducibility
|
|
269
|
+
* Reviewability
|
|
270
|
+
* Trust
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## CI/CD Usage Example
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
regsim-in simulate --rules rules/ --input payload.json || exit 1
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
A failing rule causes a non-zero exit code.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Roadmap (Explicit, Not Promised)
|
|
285
|
+
|
|
286
|
+
Planned future directions include:
|
|
287
|
+
|
|
288
|
+
* Regulatory failure memory & correlation
|
|
289
|
+
* Safer rule authoring workflows
|
|
290
|
+
* Async system modeling hooks
|
|
291
|
+
* Community-contributed rule sets
|
|
292
|
+
|
|
293
|
+
These are **not part of v1**.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Philosophy
|
|
298
|
+
|
|
299
|
+
* Simulation over certification
|
|
300
|
+
* Explicit rules over implicit assumptions
|
|
301
|
+
* Deterministic behavior over magic
|
|
302
|
+
* Memory over repetition
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Disclaimer
|
|
307
|
+
|
|
308
|
+
RegSim-IN is a **developer simulation tool**.
|
|
309
|
+
It does **not** guarantee legal or regulatory compliance.
|
|
310
|
+
|
|
311
|
+
Always consult qualified professionals for real-world compliance decisions
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Exact Commands
|
|
316
|
+
|
|
317
|
+
# Run against JSON payload
|
|
318
|
+
regsim-in simulate --rules rules/ --input payload.json
|
|
319
|
+
|
|
320
|
+
# Run against Python service
|
|
321
|
+
regsim-in simulate --rules rules/ --input src/
|
|
322
|
+
|
|
323
|
+
# CI usage
|
|
324
|
+
regsim-in simulate --rules rules/ --input src/ --snapshot-date 2024-04-01
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
regsim/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
regsim/cli.py,sha256=g1PeXT5ZRwdm700FS4Hv5v1hYgDP6X8mrWfiVKjYizU,3481
|
|
3
|
+
regsim/engine.py,sha256=4ApvAIo0aPyHyvugk2hsmRfU4E-7Tja199KbxzrVYZ4,1204
|
|
4
|
+
regsim/errors.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
regsim/parser.py,sha256=kivTrtEn_retMjaFyibZTMIlzRV1oLJudoDRbzpUXT4,1076
|
|
6
|
+
regsim/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
regsim/commands/simulate.py,sha256=Z4c5A_t5lS0D-tV2EVsfTz8Obt4fITHmnhxnRLcAfZg,111
|
|
8
|
+
regsim/core/__init__.py,sha256=IyAjq1BAyLtSfqXjApMUC6EL5kik2dG1LunGuMObTGQ,80
|
|
9
|
+
regsim/core/evaluator.py,sha256=aMPuHUsxthcF-GxUi4YGwktbusSuDAgCaxX31qNcBLg,1979
|
|
10
|
+
regsim/core/fields.py,sha256=r8pk3ry6GdvMNa044x3p_89V0SW3EIjco1Jg5RzPya0,753
|
|
11
|
+
regsim/core/simulation.py,sha256=fqnyeMiNK9QRnNXfzaTmqow13Ni9H_bWApDw5BruvB8,3354
|
|
12
|
+
regsim/core/validators.py,sha256=h6q7wLAIKsnUK7_HOx5HnflLU8iHmFOts3KIg99U81w,230
|
|
13
|
+
regsim/schemas/input.schema.json,sha256=Wm2xgC1_NRUGBnj3eQY2hCDhbwAtx2DVnmLFptiqwjw,55
|
|
14
|
+
regsim/schemas/output.schema.json,sha256=EI1Y18TcNHKv6T1nkPSGOmMEYejdVTWtjGFfeFnpi8I,2566
|
|
15
|
+
regsim/schemas/rule.schema.json,sha256=tG4xSrPWcaKEjpddX1mKAPysx8PeAWPac_eFjs22djc,2201
|
|
16
|
+
regsim/utils/__init__.py,sha256=Y12Xi4a4hVUw_SVAXT0l2uDjBOABlmfPpsyoZtF3HMg,245
|
|
17
|
+
regsim/utils/extract_call_args.py,sha256=hOEFgf-q9VXON7a_UR1nhu2joerzfcKddtu7pZWIyA8,231
|
|
18
|
+
regsim/utils/extract_dic.py,sha256=R2ZzZXndDS77Nj1b8WGp1DXD5N7x7xWzUF5U4DB2NbA,285
|
|
19
|
+
regsim/utils/get_func_name.py,sha256=NF7GpDaH62r5jxCF_wA7rJP44COkRnjs8Ak7lcUp2RA,221
|
|
20
|
+
regsim_in-0.1.0.dist-info/METADATA,sha256=FKVa9v92-cTgpaZlkOdbQ1LLwClAgLRl9ytPtN-OKFs,6330
|
|
21
|
+
regsim_in-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
22
|
+
regsim_in-0.1.0.dist-info/entry_points.txt,sha256=9XtjTQnV-P6spTRQfs8RyvBkAJjyVgsyVCvu3XVWJdw,46
|
|
23
|
+
regsim_in-0.1.0.dist-info/top_level.txt,sha256=v8qlZho0FIeDZFo2-rN6-vSbtXtZJ6LIPwXei87wRzY,7
|
|
24
|
+
regsim_in-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
regsim
|