From 63f9b33baf7563529de1e3999316de7c81459c13 Mon Sep 17 00:00:00 2001 From: bishwas Date: Sat, 4 Oct 2025 22:05:00 +0200 Subject: [PATCH 01/11] added more example and fixed issue with linting --- examples/calculator.dmai | 47 ++++++++++++++++++++++++++ examples/shopping_list.dmai | 55 +++++++++++++++++++++++++++++++ maithili_dsl/transpiler/linter.py | 22 ++++++++----- 3 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 examples/calculator.dmai create mode 100644 examples/shopping_list.dmai diff --git a/examples/calculator.dmai b/examples/calculator.dmai new file mode 100644 index 0000000..0f8df8a --- /dev/null +++ b/examples/calculator.dmai @@ -0,0 +1,47 @@ +कार्य जोड़(अ, ब): + फेर करू अ + ब + +कार्य घटाउ(अ, ब): + फेर करू अ - ब + +कार्य गुणा(अ, ब): + फेर करू अ * ब + +कार्य भाग(अ, ब): + फेर करू अ / ब + +कार्य मुख्य_कैलकुलेटर(): + शीर्षक = "=== मैथिली कैलकुलेटर ===" + छपाउ(शीर्षक) + + संख्या१ = १० + संख्या२ = ५ + + जोड़_परिणाम = जोड़(संख्या१, संख्या२) + जोड़_संदेश = "जोड़ का परिणाम: " + str(जोड़_परिणाम) + छपाउ(जोड़_संदेश) + + घटाउ_परिणाम = घटाउ(संख्या१, संख्या२) + घटाउ_संदेश = "घटाउ का परिणाम: " + str(घटाउ_परिणाम) + छपाउ(घटाउ_संदेश) + + गुणा_परिणाम = गुणा(संख्या१, संख्या२) + गुणा_संदेश = "गुणा का परिणाम: " + str(गुणा_परिणाम) + छपाउ(गुणा_संदेश) + + भाग_परिणाम = भाग(संख्या१, संख्या२) + भाग_संदेश = "भाग का परिणाम: " + str(भाग_परिणाम) + छपाउ(भाग_संदेश) + +कार्य संख्या_तालिका(): + रिक्त_पंक्ति = "" + छपाउ(रिक्त_पंक्ति) + शीर्षक = "=== ५ का पहाड़ा ===" + छपाउ(शीर्षक) + प्रत्येक i में range(१, ११): + गुणनफल = ५ * i + पंक्ति = "५ × " + str(i) + " = " + str(गुणनफल) + छपाउ(पंक्ति) + +मुख्य_कैलकुलेटर() +संख्या_तालिका() diff --git a/examples/shopping_list.dmai b/examples/shopping_list.dmai new file mode 100644 index 0000000..45de929 --- /dev/null +++ b/examples/shopping_list.dmai @@ -0,0 +1,55 @@ +कार्य मुख्य_सूची(): + शीर्षक = "=== खरीदारी सूची ===" + छपाउ(शीर्षक) + + सूची = [] + सूची.append("दूध") + सूची.append("रोटी") + सूची.append("अंडा") + सूची.append("चावल") + + सूची_शीर्षक = "खरीदारी सूची:" + छपाउ(सूची_शीर्षक) + प्रत्येक वस्तु में सूची: + वस्तु_पंक्ति = "- " + वस्तु + छपाउ(वस्तु_पंक्ति) + + रिक्त_पंक्ति = "" + छपाउ(रिक्त_पंक्ति) + कुल_संदेश = "कुल वस्तु: " + str(len(सूची)) + छपाउ(कुल_संदेश) + + यदि "दूध" में सूची: + दूध_संदेश = "दूध सूची में अछि!" + छपाउ(दूध_संदेश) + + सूची.remove("अंडा") + रिक्त_पंक्ति२ = "" + छपाउ(रिक्त_पंक्ति२) + हटाना_संदेश = "अंडा हटाएल गेल। नई सूची:" + छपाउ(हटाना_संदेश) + प्रत्येक वस्तु में सूची: + वस्तु_पंक्ति = "- " + वस्तु + छपाउ(वस्तु_पंक्ति) + +कार्य गणना_फल(): + रिक्त_पंक्ति = "" + छपाउ(रिक्त_पंक्ति) + शीर्षक = "=== गणना फल ===" + छपाउ(शीर्षक) + + संख्याएं = [१, २, ३, ४, ५] + योग = ० + + प्रत्येक संख्या में संख्याएं: + योग = योग + संख्या + + संख्या_संदेश = "संख्याएं: " + str(संख्याएं) + छपाउ(संख्या_संदेश) + योग_संदेश = "योग: " + str(योग) + छपाउ(योग_संदेश) + औसत_संदेश = "औसत: " + str(योग / len(संख्याएं)) + छपाउ(औसत_संदेश) + +मुख्य_सूची() +गणना_फल() diff --git a/maithili_dsl/transpiler/linter.py b/maithili_dsl/transpiler/linter.py index 8150fa0..f5162d0 100644 --- a/maithili_dsl/transpiler/linter.py +++ b/maithili_dsl/transpiler/linter.py @@ -33,9 +33,12 @@ def lint_maithili_code(code, config=LINT_CONFIG): stripped = line.strip() # Check for suspicious control flow or structure lines - if stripped.endswith(":") and not stripped.startswith(tuple(" \t")): - if not stripped.split()[0] in ["कार्य", "यदि", "नहि त", "वर्ग", "प्रत्येक"]: - errors.append(f"पंक्ति {i}: संदिग्ध ढाँचा – '{stripped}'") + if stripped.endswith(":"): + first_word = stripped.split()[0] if stripped.split() else "" + if first_word not in ["कार्य", "यदि", "नहि त", "वर्ग", "प्रत्येक"]: + # Allow nested if-else structures and simple else statements + if not (stripped == "नहि त:" or (stripped.startswith("नहि त") and "यदि" in stripped)): + errors.append(f"पंक्ति {i}: संदिग्ध ढाँचा – '{stripped}'") # Check for improper assignment syntax if "=" in stripped and stripped.startswith("="): @@ -55,11 +58,14 @@ def lint_maithili_code(code, config=LINT_CONFIG): # Naming style and assignment validation if "=" in stripped and not stripped.startswith("#"): - var_name = stripped.split("=")[0].strip() - if not var_name.isidentifier(): - errors.append(f"पंक्ति {i}: चर नाम '{var_name}' वैध नहि अछि") - elif config.get("enforce_snake_case") and not is_snake_case(var_name): - errors.append(f"पंक्ति {i}: चर नाम '{var_name}' snake_case में नहि अछि") + # Skip print statements and function calls + left_side = stripped.split("=")[0].strip() + if not (stripped.startswith("छपाउ(") or "(" in left_side or left_side.startswith("छपाउ")): + var_name = left_side + if not var_name.isidentifier(): + errors.append(f"पंक्ति {i}: चर नाम '{var_name}' वैध नहि अछि") + elif config.get("enforce_snake_case") and not is_snake_case(var_name): + errors.append(f"पंक्ति {i}: चर नाम '{var_name}' snake_case में नहि अछि") # Line length check if config.get("max_line_length") and len(line) > config["max_line_length"]: From 6d38532ca804f9dac841423315d344ea79151a37 Mon Sep 17 00:00:00 2001 From: bishwas Date: Sat, 4 Oct 2025 22:08:10 +0200 Subject: [PATCH 02/11] version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index aeebe18..bd9d1d9 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='python_maithili', - version='0.1.0', + version='0.2.0', description='Run Python code written in Maithili using Devanagari script', author='Bishwas Jha', author_email='jha.bishwas@gmail.com', From 13bf1e185d0b78e85faf40d667aa3b8fe94c78f6 Mon Sep 17 00:00:00 2001 From: Bishwas Jha Date: Fri, 17 Apr 2026 19:45:09 +0200 Subject: [PATCH 03/11] docs(audit): add AUDIT_REPORT and BACKLOG tracking P0-P3 findings Introduces the formal audit artifacts that drive the rest of this change set: 16 findings across security, license, code quality, packaging, and compliance, prioritized P0-P3. The follow-up commits on this branch close every P0 item. Also adds scripts/create_issues.sh for pushing the backlog into GitHub Issues, and updates .gitignore to exclude pytest/coverage artifacts and local Claude Code config. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 11 +++ AUDIT_REPORT.md | 135 +++++++++++++++++++++++++++++ BACKLOG.md | 133 ++++++++++++++++++++++++++++ scripts/create_issues.sh | 181 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 460 insertions(+) create mode 100644 AUDIT_REPORT.md create mode 100644 BACKLOG.md create mode 100755 scripts/create_issues.sh diff --git a/.gitignore b/.gitignore index d4477a6..5604e68 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,14 @@ dist/ # Editor folders .vscode/ .idea/ + +# Test + coverage artifacts +.pytest_cache/ +.coverage +.coverage.* +htmlcov/ +coverage.xml +.tox/ + +# Claude Code local config +.claude/ diff --git a/AUDIT_REPORT.md b/AUDIT_REPORT.md new file mode 100644 index 0000000..6d3bed5 --- /dev/null +++ b/AUDIT_REPORT.md @@ -0,0 +1,135 @@ +# Maithili DSL — Full Audit Report + +> Audit Date: 2026-04-11 +> Auditor: Project Maintainer (automated + manual review) +> Scope: Security, License, Code Quality, Packaging, Compliance, Dependencies + +--- + +## 1. Security Audit + +### Finding SEC-001: Unrestricted code execution via exec() [CRITICAL] +- **Location:** `maithili_dsl/cli.py:46` +- **Description:** `exec(python_code, globals())` executes transpiled user code with full access to the CLI process's global namespace. Any `.dmai` file can import `os`, `subprocess`, or `sys` and perform arbitrary system operations. +- **Evidence:** Direct code inspection — no sandboxing, no restricted builtins. +- **Recommendation:** Replace `globals()` with a restricted execution namespace. + +### Finding SEC-002: No input validation [HIGH] +- **Location:** `maithili_dsl/cli.py:26-33` +- **Description:** `run_dmai_file()` reads any file path without size limits, type checking, or timeout protection. +- **Evidence:** The function accepts any path and reads the entire file into memory. +- **Recommendation:** Add file size limit (e.g., 1MB), file extension check, and execution timeout. + +### Finding SEC-003: No vulnerability reporting process [MEDIUM] +- **Description:** No `SECURITY.md` exists. No documented way for security researchers to report vulnerabilities. +- **Recommendation:** Add SECURITY.md with responsible disclosure process. + +--- + +## 2. License Audit + +### Finding LIC-001: LICENSE file is incomplete [CRITICAL] +- **Location:** `LICENSE` +- **Evidence:** File contains placeholder text: + - Line 3: `Copyright (c) 2025 [Your Name]` + - Line 13: `[MIT terms continued...]` +- **Impact:** The project is effectively unlicensed. Contributors and users have no legal permission to use the code. +- **Recommendation:** Replace with complete MIT license text with correct copyright holder. + +### Finding LIC-002: No license headers in source files [LOW] +- **Description:** Individual `.py` files do not contain license headers or copyright notices. +- **Recommendation:** Optional for MIT-licensed projects, but good practice. Consider adding a brief header. + +### Finding LIC-003: No third-party dependency licenses to audit [PASS] +- **Description:** The project has zero runtime dependencies. Only uses Python standard library. +- **Status:** No license conflicts possible. + +--- + +## 3. Code Quality Audit + +### Finding CQ-001: Transpiler uses naive string replacement [CRITICAL] +- **Location:** `maithili_dsl/transpiler/transpile.py:4-7` +- **Evidence (reproduced):** + ``` + Input: छपाउ("यह में है") + Output: print("यह in है") ← keyword replaced inside string literal + + Input: नवयुग = ५ + Output: __init__युग = 5 ← keyword replaced inside identifier + ``` +- **Impact:** Any Maithili text containing a keyword substring will be corrupted. This affects every non-trivial program. +- **Recommendation:** Implement proper tokenizer that respects string boundaries and word boundaries. + +### Finding CQ-002: Zero test coverage [HIGH] +- **Description:** No test files, no test configuration, no test runner. +- **Recommendation:** Add pytest with tests for transpiler, linter, numeral conversion, CLI. + +### Finding CQ-003: No type annotations [LOW] +- **Description:** No type hints on any function. +- **Recommendation:** Add type hints progressively. Add mypy to CI. + +--- + +## 4. Packaging Audit + +### Finding PKG-001: No pyproject.toml [MEDIUM] +- **Description:** Only `setup.py` exists. PEP 517/518 compliance requires `pyproject.toml`. +- **Recommendation:** Add pyproject.toml with build system and metadata. + +### Finding PKG-002: setup.py version mismatch risk [LOW] +- **Description:** Version is `0.2.0` in setup.py but there's no `__version__` in `__init__.py`. No single source of truth. +- **Recommendation:** Define version in one place and import/reference it. + +### Finding PKG-003: Empty __init__.py files [LOW] +- **Description:** No public API defined. Users importing the package get nothing. +- **Recommendation:** Export key functions via `__all__`. + +--- + +## 5. Compliance Audit + +| Item | Status | Notes | +|------|--------|-------| +| LICENSE file | FAIL | Incomplete — has placeholders | +| SECURITY.md | MISSING | No vulnerability reporting process | +| CODE_OF_CONDUCT.md | MISSING | No community guidelines | +| CONTRIBUTING.md | PARTIAL | Exists but contains duplicated README content | +| .gitignore | PASS | Covers common patterns | +| README.md | PARTIAL | Clone URL has placeholder `youruser` | + +--- + +## 6. Infrastructure Audit + +| Item | Status | Notes | +|------|--------|-------| +| CI/CD (GitHub Actions) | MISSING | No automated testing on push/PR | +| Pre-commit hooks | MISSING | No code quality gates | +| Dependency pinning | N/A | Zero runtime deps; dev deps not specified | +| Release process | MISSING | No tags, no changelog, no PyPI publishing | + +--- + +## 7. Summary + +| Severity | Count | +|----------|-------| +| CRITICAL | 3 (exec sandbox, LICENSE, transpiler bugs) | +| HIGH | 3 (input validation, no tests, no CI) | +| MEDIUM | 3 (SECURITY.md, pyproject.toml, linter false positives) | +| LOW | 5 (type hints, license headers, version, init, deps) | +| PASS | 2 (no dependency license conflicts, .gitignore) | +| **Total**| **16 findings** | + +--- + +## 8. Recommended Fix Order + +1. Fix LICENSE file (5 minutes — removes legal risk immediately) +2. Fix transpiler str.replace bugs (core functionality is broken) +3. Sandbox exec() (security critical) +4. Add basic test suite (prevents regressions while fixing above) +5. Add GitHub Actions CI (automates quality going forward) +6. Add SECURITY.md + CODE_OF_CONDUCT.md (compliance basics) +7. Everything else by priority diff --git a/BACKLOG.md b/BACKLOG.md new file mode 100644 index 0000000..19de5e7 --- /dev/null +++ b/BACKLOG.md @@ -0,0 +1,133 @@ +# Maithili DSL — Project Backlog + +> Generated: 2026-04-11 | Status: Active +> Tracking: GitHub Issues (use `scripts/create_issues.sh` to push) + +--- + +## P0 — Critical (Must Fix) + +### 1. [SECURITY] exec() runs with globals() — sandbox needed +- **File:** `maithili_dsl/cli.py:46` +- **Issue:** `exec(python_code, globals())` allows .dmai scripts to modify the CLI tool itself at runtime. A malicious `.dmai` file can import `os`, delete files, or exfiltrate data. +- **Fix:** Execute in a restricted namespace: `exec(python_code, {"__builtins__": safe_builtins}, local_ns)` +- **Labels:** security, P0 + +### 2. [BUG] Transpiler replaces keywords inside string literals +- **File:** `maithili_dsl/transpiler/transpile.py` +- **Proven:** `छपाउ("यह में है")` → `print("यह in है")` — the word `में` inside quotes gets replaced with `in`. +- **Fix:** Use tokenization or regex with negative lookbehind for quoted regions instead of blind `str.replace()`. +- **Labels:** bug, P0 + +### 3. [BUG] Transpiler replaces keywords inside longer words +- **File:** `maithili_dsl/transpiler/transpile.py` +- **Proven:** `नवयुग = ५` → `__init__युग = 5` — `नव` (maps to `__init__`) is matched as a substring. +- **Fix:** Use word-boundary-aware replacement (`\bकार्य\b` equivalent for Devanagari). +- **Labels:** bug, P0 + +### 4. [LICENSE] LICENSE file is incomplete +- **File:** `LICENSE` +- **Issue:** Contains `[Your Name]` placeholder and `[MIT terms continued...]` — not a valid MIT license. +- **Fix:** Replace with the full MIT license text with correct copyright holder name. +- **Labels:** compliance, P0 + +--- + +## P1 — High (Should Fix Soon) + +### 5. [QUALITY] No tests exist +- **Issue:** Zero test files in the entire project. No unit tests, no integration tests, no test runner configured. +- **Fix:** Add `tests/` directory with pytest. Cover transpiler, linter, numeral conversion, and CLI. +- **Labels:** testing, P1 + +### 6. [INFRA] No CI/CD pipeline +- **Issue:** No GitHub Actions, no tox.ini, no pre-commit hooks. Code can be pushed without any automated checks. +- **Fix:** Add `.github/workflows/ci.yml` with lint + test + build steps. +- **Labels:** infra, P1 + +### 7. [SECURITY] No input validation before exec +- **Issue:** `run_dmai_file()` reads any file and execs it. No file size limits, no timeout, no sandboxing. +- **Fix:** Add file size limits, execution timeout, and restricted builtins. +- **Labels:** security, P1 + +### 8. [COMPLIANCE] No SECURITY.md +- **Issue:** No documented process for reporting security vulnerabilities. +- **Fix:** Add `SECURITY.md` with responsible disclosure instructions. +- **Labels:** compliance, P1 + +### 9. [PACKAGING] Missing pyproject.toml (PEP 517/518) +- **Issue:** Only `setup.py` exists. Modern Python tooling expects `pyproject.toml`. +- **Fix:** Add `pyproject.toml` alongside or replacing `setup.py`. +- **Labels:** packaging, P1 + +--- + +## P2 — Medium (Plan for Next Sprint) + +### 10. [BUG] Linter may produce false positives for Devanagari identifiers +- **File:** `maithili_dsl/transpiler/linter.py:65` +- **Issue:** `var_name.isidentifier()` works for Devanagari in Python 3, but the linter's `is_snake_case()` regex only matches ASCII — it will always fail for Devanagari variable names. +- **Fix:** Update naming checks to support Devanagari script identifiers. +- **Labels:** bug, P2 + +### 11. [DOCS] CONTRIBUTING.md has duplicated README content +- **File:** `CONTRIBUTING.md` +- **Issue:** The file contains the full README.md pasted above the actual contributing guidelines. +- **Fix:** Remove duplicated content, keep only contributing-specific information. +- **Labels:** docs, P2 + +### 12. [QUALITY] No type hints in codebase +- **Issue:** No function signatures have type annotations. Makes it harder for contributors to understand the API. +- **Fix:** Add type hints to all public functions. Add `mypy` to CI. +- **Labels:** quality, P2 + +### 13. [COMPLIANCE] No CODE_OF_CONDUCT.md +- **Issue:** Open source projects should have a code of conduct for community health. +- **Fix:** Add Contributor Covenant or similar. +- **Labels:** compliance, P2 + +### 14. [PACKAGING] Empty __init__.py files +- **Issue:** `maithili_dsl/__init__.py` and `maithili_dsl/transpiler/__init__.py` are empty — no public API is exported. +- **Fix:** Define `__all__` and export key functions for programmatic use. +- **Labels:** packaging, P2 + +--- + +## P3 — Low (Nice to Have) + +### 15. [DOCS] README references `youruser` in clone URL +- **Issue:** `git clone https://github.com/youruser/maithili-dsl.git` — should point to the real repo. +- **Fix:** Update to `alphacrack/python-maithili-dsl`. +- **Labels:** docs, P3 + +### 16. [FEATURE] Add `while` loop support (`जबतक`) +- **Issue:** No `while` keyword mapping exists. Only `for` (`प्रत्येक`) is supported. +- **Fix:** Add to `DEVNAGIRI_KEYWORD_MAP`. +- **Labels:** enhancement, P3 + +### 17. [FEATURE] Add `try/except` support for error handling +- **Issue:** No exception handling keywords mapped. Users can't write try/except in Maithili. +- **Fix:** Add `प्रयास` → `try`, `अपवाद` → `except`, `अंततः` → `finally`. +- **Labels:** enhancement, P3 + +### 18. [FEATURE] REPL mode for interactive Maithili coding +- **Issue:** Listed as a future goal in README but not started. +- **Fix:** Add `python_maithili --repl` interactive mode. +- **Labels:** enhancement, P3 + +### 19. [INFRA] No dependency pinning +- **Issue:** No `requirements.txt` or `requirements-dev.txt`. Project currently has zero deps, but test/lint tools need to be pinned. +- **Fix:** Add `requirements-dev.txt` with pytest, mypy, flake8 versions pinned. +- **Labels:** infra, P3 + +--- + +## Summary + +| Priority | Count | Focus | +|----------|-------|-------| +| P0 | 4 | Security holes, data-corrupting bugs, invalid license | +| P1 | 5 | Testing, CI/CD, compliance foundations | +| P2 | 5 | Code quality, docs cleanup, contributor experience | +| P3 | 5 | Features, nice-to-haves, polish | +| **Total**| **19**| | diff --git a/scripts/create_issues.sh b/scripts/create_issues.sh new file mode 100755 index 0000000..2b393da --- /dev/null +++ b/scripts/create_issues.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +# Creates GitHub Issues from the backlog. +# Run from the repo root: bash scripts/create_issues.sh +# Requires: gh CLI authenticated (https://cli.github.com) + +set -euo pipefail + +REPO="alphacrack/python-maithili-dsl" + +echo "Creating P0 issues..." + +gh issue create --repo "$REPO" \ + --title "[SECURITY][P0] exec() runs with globals() — sandbox needed" \ + --body "**File:** \`maithili_dsl/cli.py:46\` + +\`exec(python_code, globals())\` allows .dmai scripts to modify the CLI tool itself at runtime. A malicious \`.dmai\` file can import \`os\`, delete files, or exfiltrate data. + +**Fix:** Execute in a restricted namespace: \`exec(python_code, {\"__builtins__\": safe_builtins}, local_ns)\`" \ + --label "security,P0" + +gh issue create --repo "$REPO" \ + --title "[BUG][P0] Transpiler replaces keywords inside string literals" \ + --body "**File:** \`maithili_dsl/transpiler/transpile.py\` + +**Reproduction:** +\`\`\` +Input: छपाउ(\"यह में है\") +Output: print(\"यह in है\") +\`\`\` + +The word \`में\` inside quotes gets replaced with \`in\`. The transpiler uses naive \`str.replace()\` with no awareness of string boundaries. + +**Fix:** Use tokenization or regex with negative lookbehind for quoted regions." \ + --label "bug,P0" + +gh issue create --repo "$REPO" \ + --title "[BUG][P0] Transpiler replaces keywords inside longer words" \ + --body "**File:** \`maithili_dsl/transpiler/transpile.py\` + +**Reproduction:** +\`\`\` +Input: नवयुग = ५ +Output: __init__युग = 5 +\`\`\` + +\`नव\` (maps to \`__init__\`) is matched as a substring inside \`नवयुग\`. + +**Fix:** Use word-boundary-aware replacement for Devanagari script." \ + --label "bug,P0" + +gh issue create --repo "$REPO" \ + --title "[LICENSE][P0] LICENSE file is incomplete — has placeholders" \ + --body "The LICENSE file contains: +- \`[Your Name]\` instead of the actual copyright holder +- \`[MIT terms continued...]\` instead of the full MIT license text + +This means the project is technically unlicensed. Must be fixed immediately." \ + --label "compliance,P0" + +echo "Creating P1 issues..." + +gh issue create --repo "$REPO" \ + --title "[QUALITY][P1] No tests exist — add pytest suite" \ + --body "Zero test files in the project. Need unit tests covering: +- Transpiler keyword replacement +- Devanagari numeral conversion +- Linter rule checks +- CLI file loading +- Import translation + +**Action:** Add \`tests/\` directory with pytest. Target >80% coverage on core modules." \ + --label "testing,P1" + +gh issue create --repo "$REPO" \ + --title "[INFRA][P1] No CI/CD pipeline — add GitHub Actions" \ + --body "No automated checks on push or PR. Code can be merged without any validation. + +**Action:** Add \`.github/workflows/ci.yml\` with: +- Python 3.8, 3.10, 3.12 matrix +- pytest +- flake8 or ruff lint +- Build validation (pip install .)" \ + --label "infra,P1" + +gh issue create --repo "$REPO" \ + --title "[SECURITY][P1] No input validation before exec" \ + --body "\`run_dmai_file()\` reads any file and execs it with no guardrails. + +**Action:** Add file size limits, execution timeout, and restricted builtins." \ + --label "security,P1" + +gh issue create --repo "$REPO" \ + --title "[COMPLIANCE][P1] Add SECURITY.md for vulnerability reporting" \ + --body "No documented process for reporting security vulnerabilities. Open source projects need this. + +**Action:** Add \`SECURITY.md\` with responsible disclosure instructions and contact info." \ + --label "compliance,P1" + +gh issue create --repo "$REPO" \ + --title "[PACKAGING][P1] Add pyproject.toml (PEP 517/518)" \ + --body "Only \`setup.py\` exists. Modern Python tooling expects \`pyproject.toml\`. + +**Action:** Add \`pyproject.toml\` with build-system, project metadata, and tool configs (pytest, mypy, ruff)." \ + --label "packaging,P1" + +echo "Creating P2 issues..." + +gh issue create --repo "$REPO" \ + --title "[BUG][P2] Linter false positives for Devanagari identifiers" \ + --body "**File:** \`maithili_dsl/transpiler/linter.py:65\` + +\`is_snake_case()\` regex only matches ASCII — it will always fail for Devanagari variable names. + +**Action:** Update naming checks to support Devanagari script identifiers." \ + --label "bug,P2" + +gh issue create --repo "$REPO" \ + --title "[DOCS][P2] CONTRIBUTING.md has duplicated README content" \ + --body "The CONTRIBUTING.md file contains the full README.md pasted above the actual contributing guidelines. + +**Action:** Remove duplicated content." \ + --label "docs,P2" + +gh issue create --repo "$REPO" \ + --title "[QUALITY][P2] Add type hints to all public functions" \ + --body "No function signatures have type annotations. + +**Action:** Add type hints. Add \`mypy\` to CI." \ + --label "quality,P2" + +gh issue create --repo "$REPO" \ + --title "[COMPLIANCE][P2] Add CODE_OF_CONDUCT.md" \ + --body "Open source projects should have a code of conduct for community health. + +**Action:** Add Contributor Covenant." \ + --label "compliance,P2" + +gh issue create --repo "$REPO" \ + --title "[PACKAGING][P2] Define public API in __init__.py" \ + --body "Both \`__init__.py\` files are empty — no public API exported. + +**Action:** Define \`__all__\` and export key functions." \ + --label "packaging,P2" + +echo "Creating P3 issues..." + +gh issue create --repo "$REPO" \ + --title "[DOCS][P3] README clone URL references 'youruser'" \ + --body "README says \`git clone https://github.com/youruser/maithili-dsl.git\` — should be \`alphacrack/python-maithili-dsl\`." \ + --label "docs,P3" + +gh issue create --repo "$REPO" \ + --title "[FEATURE][P3] Add while loop support (जबतक)" \ + --body "No \`while\` keyword mapping exists. Only \`for\` (\`प्रत्येक\`) is supported. + +**Action:** Add \`जबतक\` → \`while\` to \`DEVNAGIRI_KEYWORD_MAP\`." \ + --label "enhancement,P3" + +gh issue create --repo "$REPO" \ + --title "[FEATURE][P3] Add try/except support for error handling" \ + --body "No exception handling keywords mapped. + +**Action:** Add \`प्रयास\` → \`try\`, \`अपवाद\` → \`except\`, \`अंततः\` → \`finally\`." \ + --label "enhancement,P3" + +gh issue create --repo "$REPO" \ + --title "[FEATURE][P3] REPL mode for interactive Maithili coding" \ + --body "Listed as a future goal in README but not started. + +**Action:** Add \`python_maithili --repl\` interactive mode." \ + --label "enhancement,P3" + +gh issue create --repo "$REPO" \ + --title "[INFRA][P3] Add requirements-dev.txt with pinned dev dependencies" \ + --body "No dependency pinning for dev tools. + +**Action:** Add \`requirements-dev.txt\` with pytest, mypy, ruff versions pinned." \ + --label "infra,P3" + +echo "" +echo "✅ All 19 backlog issues created successfully!" From 4fd95ba2bc8c8e397554ae7c777d4eb2a52eb541 Mon Sep 17 00:00:00 2001 From: Bishwas Jha Date: Fri, 17 Apr 2026 19:45:24 +0200 Subject: [PATCH 04/11] fix(license): complete MIT license text with proper copyright holder Replaces the two placeholders that made the previous LICENSE legally non-functional: - "Copyright (c) 2025 [Your Name]" -> "Copyright (c) 2025 Bishwas Jha" - "[MIT terms continued...]" -> full MIT grant + warranty disclaimer Closes AUDIT_REPORT.md finding LIC-001 (CRITICAL): until now the project had no enforceable license and could not legally be used or redistributed. Co-Authored-By: Claude Opus 4.7 (1M context) --- LICENSE | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 3b8ca7b..eff197f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 [Your Name] +Copyright (c) 2025 Bishwas Jha Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,4 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -[MIT terms continued...] +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 45103f9041a0ff1ab1a6be29b0ae4965086fae97 Mon Sep 17 00:00:00 2001 From: Bishwas Jha Date: Fri, 17 Apr 2026 19:45:32 +0200 Subject: [PATCH 05/11] fix(transpiler): string-aware tokenization + Devanagari word boundaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces naive str.replace() with a two-pass algorithm that splits input into (string-literal, code) regions and only applies keyword substitution to code regions, using custom lookbehind/lookahead for the Devanagari block so keywords embedded in longer identifiers are not matched. Fixes two data-corrupting bugs proven in AUDIT_REPORT.md finding CQ-001 (CRITICAL): - छपाउ("यह में है") no longer becomes print("यह in है") (the word में inside the string literal is preserved) - नवयुग = ५ no longer becomes __init__युग = 5 (नव -> __init__ only fires at word boundaries) The pre-compiled pattern cache keeps the hot path fast despite the regex overhead. Co-Authored-By: Claude Opus 4.7 (1M context) --- maithili_dsl/transpiler/transpile.py | 98 +++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 3 deletions(-) diff --git a/maithili_dsl/transpiler/transpile.py b/maithili_dsl/transpiler/transpile.py index 0ea0c97..b5d5d5a 100644 --- a/maithili_dsl/transpiler/transpile.py +++ b/maithili_dsl/transpiler/transpile.py @@ -1,8 +1,100 @@ +import re from .mappings import DEVNAGIRI_KEYWORD_MAP from .numeral import convert_devanagari_numerals +# Devanagari "word character" class: letters (Lo), marks (Mn, Mc), and digits. +# Standard \b doesn't work for Devanagari because combining marks (virama ्, +# anusvara ं, visarga ः, matras) are category Mn/Mc and not matched by \w. +_DEVANAGARI_WORD_CHAR = ( + r'[\u0900-\u097F]' # Devanagari block (letters, marks, digits, signs) +) + +# A "word character" for our purposes: ASCII \w OR anything in Devanagari block +_WORD_CHAR = r'(?:[\w' + '\u0900-\u097F' + r'])' + + +def _tokenize_preserving_strings(code): + """Split code into tokens, separating string literals from code regions. + + Returns a list of (is_string, text) tuples. String literals are returned + intact and should not be modified by keyword replacement. + """ + tokens = [] + # Match single-quoted, double-quoted strings, and # comments + pattern = re.compile(r'''("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|#.*)''') + + last_end = 0 + for match in pattern.finditer(code): + start, end = match.span() + # Code region before this string/comment + if start > last_end: + tokens.append((False, code[last_end:start])) + # The string literal or comment itself + tokens.append((True, match.group(0))) + last_end = end + + # Remaining code after last string/comment + if last_end < len(code): + tokens.append((False, code[last_end:])) + + return tokens + + +def _make_keyword_pattern(keyword): + """Create a regex pattern that matches a keyword at word boundaries. + + Uses custom boundary logic that understands Devanagari combining marks + (virama, anusvara, visarga, matras) as part of words, since Python's + \\b only recognizes \\w characters (which excludes Mn/Mc categories). + """ + # Negative lookbehind/lookahead for our extended word characters + before = r'(? Date: Fri, 17 Apr 2026 19:45:40 +0200 Subject: [PATCH 06/11] fix(linter): skip == comparisons and constructor from unused warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two false-positive fixes that blocked idiomatic Maithili code from linting cleanly: 1. Comparison operators (==, !=, <=, >=) were being parsed as assignments by the naive .split("=") logic, producing spurious "invalid variable name" errors on any conditional. Now detects the operator context before treating an = as an assignment. 2. नव (mapped to __init__) was flagged as an unused function because it is never called by name — Python invokes it implicitly on class instantiation. Skipped from the unused- function post-scan. Also ignores attribute assignments (e.g., स्वयं.नाम = ...) which are never new-variable declarations. Co-Authored-By: Claude Opus 4.7 (1M context) --- maithili_dsl/transpiler/linter.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/maithili_dsl/transpiler/linter.py b/maithili_dsl/transpiler/linter.py index f5162d0..0459e93 100644 --- a/maithili_dsl/transpiler/linter.py +++ b/maithili_dsl/transpiler/linter.py @@ -58,14 +58,22 @@ def lint_maithili_code(code, config=LINT_CONFIG): # Naming style and assignment validation if "=" in stripped and not stripped.startswith("#"): - # Skip print statements and function calls - left_side = stripped.split("=")[0].strip() - if not (stripped.startswith("छपाउ(") or "(" in left_side or left_side.startswith("छपाउ")): - var_name = left_side - if not var_name.isidentifier(): - errors.append(f"पंक्ति {i}: चर नाम '{var_name}' वैध नहि अछि") - elif config.get("enforce_snake_case") and not is_snake_case(var_name): - errors.append(f"पंक्ति {i}: चर नाम '{var_name}' snake_case में नहि अछि") + # Skip comparison operators (==, !=, <=, >=) + eq_pos = stripped.index("=") + if eq_pos > 0 and stripped[eq_pos - 1] in "!<>": + pass # comparison operator, not assignment + elif eq_pos + 1 < len(stripped) and stripped[eq_pos + 1] == "=": + pass # == comparison + else: + left_side = stripped.split("=")[0].strip() + # Skip print statements, function calls, and attribute assignments (e.g., स्वयं.नाम) + if not (stripped.startswith("छपाउ(") or "(" in left_side + or left_side.startswith("छपाउ") or "." in left_side): + var_name = left_side + if not var_name.isidentifier(): + errors.append(f"पंक्ति {i}: चर नाम '{var_name}' वैध नहि अछि") + elif config.get("enforce_snake_case") and not is_snake_case(var_name): + errors.append(f"पंक्ति {i}: चर नाम '{var_name}' snake_case में नहि अछि") # Line length check if config.get("max_line_length") and len(line) > config["max_line_length"]: @@ -87,8 +95,11 @@ def lint_maithili_code(code, config=LINT_CONFIG): except IndexError: errors.append(f"पंक्ति {i}: कार्य नाम सही रूप में परिभाषित नहि अछि") - # Post-scan: warn if declared functions are unused (except main) + # Post-scan: warn if declared functions are unused + # Skip नव (__init__) — it's always called implicitly via class instantiation for f in declared_functions: + if f == "नव": + continue # constructor is called implicitly used = any(f in line for line in lines if not line.strip().startswith("कार्य")) if not used: errors.append(f"चेतावनी: कार्य '{f}' केहनो ठाम प्रयोग नहि कएल गेल अछि") From c1765756514b7da778979cd9fae68271c17d077d Mon Sep 17 00:00:00 2001 From: Bishwas Jha Date: Fri, 17 Apr 2026 19:45:50 +0200 Subject: [PATCH 07/11] feat(security): sandbox exec() with safe builtins and import whitelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes AUDIT_REPORT.md finding SEC-001 (CRITICAL): previously any .dmai file could do "आयात os" (which transpiled to "import os") and then perform arbitrary filesystem or process operations with the full authority of the CLI user. The new execution model: - exec_globals no longer inherits the CLI's globals(); it gets a fresh {"__builtins__": safe_builtins} dict. - safe_builtins excludes __import__, exec, eval, compile, open, and breakpoint. __import__ is replaced with a whitelist-only variant that only permits modules in MAITHILI_MODULES values. - translate_imports() now returns the set of modules it actually translated so raw Python "import os" lines injected post- transpile (never surfaced from a Maithili import) are rejected. - _validate_imports() catches the rejected imports BEFORE exec runs, since Python's import statement does not always route through __import__. Belt-and-suspenders: both layers (static validator + runtime __import__ hook) must pass for a module to load, so bypassing one still leaves the other in place. Co-Authored-By: Claude Opus 4.7 (1M context) --- maithili_dsl/cli.py | 103 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 5 deletions(-) diff --git a/maithili_dsl/cli.py b/maithili_dsl/cli.py index 55413ba..ae4cf27 100644 --- a/maithili_dsl/cli.py +++ b/maithili_dsl/cli.py @@ -1,9 +1,11 @@ import sys +import re +import builtins as _builtins from pathlib import Path from maithili_dsl.transpiler.transpile import transpile_maithili_code from maithili_dsl.transpiler.linter import lint_maithili_code, translate_exception_to_maithili -# Optional: Extendable built-in Python library map (Maithili ⇄ Python) +# Extendable built-in Python library map (Maithili → Python) MAITHILI_MODULES = { "गणित": "math", "समय": "time", @@ -17,13 +19,86 @@ "आँकड़ा": "statistics" } +# Modules that .dmai scripts are allowed to import +_ALLOWED_MODULES = set(MAITHILI_MODULES.values()) + +# Safe builtins — excludes __import__, exec, eval, compile, open, breakpoint +_SAFE_BUILTIN_NAMES = [ + 'abs', 'all', 'any', 'bin', 'bool', 'chr', 'dict', 'dir', + 'divmod', 'enumerate', 'filter', 'float', 'format', 'frozenset', + 'getattr', 'hasattr', 'hash', 'hex', 'id', 'input', 'int', + 'isinstance', 'issubclass', 'iter', 'len', 'list', 'map', 'max', + 'min', 'next', 'object', 'oct', 'ord', 'pow', 'print', 'property', + 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', + 'sorted', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip', + # Required for class definitions (Python internally uses __build_class__) + '__build_class__', + # Required for name lookup in class bodies + '__name__', +] + + +def _make_safe_import(): + """Create an __import__ that only allows whitelisted modules.""" + real_import = _builtins.__import__ + + def safe_import(name, globals=None, locals=None, fromlist=(), level=0): + # Allow the base module (e.g., 'os' from 'os.path') + base_module = name.split('.')[0] + if name not in _ALLOWED_MODULES and base_module not in _ALLOWED_MODULES: + raise ImportError(f"आयात अनुमति नहि अछि: '{name}'") + return real_import(name, globals, locals, fromlist, level) + + return safe_import + + def translate_imports(maithili_code): + """Translate Maithili module names to Python equivalents. + + Returns (translated_code, set_of_translated_module_names) so the + sandbox can distinguish legitimately translated imports from + raw Python imports injected by a malicious .dmai file. + """ + translated_modules = set() for maithili_mod, py_mod in MAITHILI_MODULES.items(): + if f"आयात {maithili_mod}" in maithili_code or f"{maithili_mod}." in maithili_code: + translated_modules.add(py_mod.split('.')[0]) maithili_code = maithili_code.replace(f"आयात {maithili_mod}", f"import {py_mod}") maithili_code = maithili_code.replace(f"{maithili_mod}.", f"{py_mod}.") - return maithili_code + return maithili_code, translated_modules + + +def _validate_imports(python_code, translated_modules): + """Check that all import statements only reference modules that were + translated from Maithili names (not raw Python module names). + + This catches imports BEFORE exec runs them, since Python's import + statement doesn't always go through __import__ in all cases. + + Args: + python_code: The transpiled Python code. + translated_modules: Set of module names that were legitimately + translated from Maithili names in this file. + """ + errors = [] + for line in python_code.splitlines(): + stripped = line.strip() + if stripped.startswith('import '): + module_part = stripped[len('import '):].strip() + module_name = module_part.split('.')[0].split(' ')[0] + if module_name not in translated_modules: + errors.append(f"आयात अनुमति नहि अछि: '{module_name}'") + elif stripped.startswith('from '): + match = re.match(r'from\s+(\S+)\s+import', stripped) + if match: + module_name = match.group(1).split('.')[0] + if module_name not in translated_modules: + errors.append(f"आयात अनुमति नहि अछि: '{module_name}'") + return errors + def run_dmai_file(file_path): + """Load, lint, transpile, and execute a .dmai file.""" path = Path(file_path) if not path.exists(): print(f"Error: File '{file_path}' not found.") @@ -41,12 +116,29 @@ def run_dmai_file(file_path): return try: - maithili_code = translate_imports(maithili_code) + maithili_code, translated_modules = translate_imports(maithili_code) python_code = transpile_maithili_code(maithili_code) - exec(python_code, globals()) + + # Validate imports before execution — only allow modules that + # were translated from Maithili names, not raw Python imports + import_errors = _validate_imports(python_code, translated_modules) + if import_errors: + for err in import_errors: + print(f"⚠️ त्रुटि: {err}") + return + + # Build sandboxed execution environment + safe_builtins = {name: getattr(_builtins, name) for name in _SAFE_BUILTIN_NAMES} + safe_builtins['__import__'] = _make_safe_import() + + exec_globals = {"__builtins__": safe_builtins} + exec(python_code, exec_globals) + except Exception as e: translated = translate_exception_to_maithili(e) print("⚠️ त्रुटि:", translated) + + def main(): import sys if len(sys.argv) < 2: @@ -54,8 +146,9 @@ def main(): else: run_dmai_file(sys.argv[1]) + if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: python run_dmai.py ") else: - run_dmai_file(sys.argv[1]) \ No newline at end of file + run_dmai_file(sys.argv[1]) From b70368e713d7ce3f56b078fa9252e71a1bd40362 Mon Sep 17 00:00:00 2001 From: Bishwas Jha Date: Fri, 17 Apr 2026 19:52:24 +0200 Subject: [PATCH 08/11] chore(packaging): modernize build, add __version__, exit codes, python -m entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps several packaging and CLI-ergonomics improvements that the audit and tests both depend on: - Adds pyproject.toml (PEP 517/518) as the canonical metadata source, with pytest + coverage configuration co-located (closes PKG-001). - setup.py kept as legacy shim; reads version via regex (not import) so it works in isolated build envs. Bumps python_requires from >=3.6 (EOL) to >=3.9, aligning with the CI matrix. - maithili_dsl/__init__.py now exposes __version__ = "0.3.0" and __all__ (closes PKG-002, PKG-003). Single source of truth. - Adds maithili_dsl/__main__.py so `python -m maithili_dsl ` works without relying on the installed console script. - cli.main() now returns an integer exit code, accepts argv for testability, and supports --version / -V. run_dmai_file() uses named EXIT_* constants (NOT_FOUND, LINT_ERROR, IMPORT_BLOCKED, RUNTIME_ERROR, USAGE) so CI and smoke tests can assert on specific failure paths instead of just "non-zero". - requirements-dev.txt pins pytest + pytest-cov + pytest-timeout with compatible-range bounds. - .gitignore ignores .venv/ (in addition to the previously added pytest/coverage/claude entries). No runtime dependencies added — the package still has zero runtime deps. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + maithili_dsl/__init__.py | 14 +++++++ maithili_dsl/__main__.py | 5 +++ maithili_dsl/cli.py | 46 ++++++++++++++------ maithili_dsl/transpiler/__init__.py | 14 +++++++ pyproject.toml | 65 +++++++++++++++++++++++++++++ requirements-dev.txt | 5 +++ setup.py | 31 +++++++++++--- 8 files changed, 163 insertions(+), 18 deletions(-) create mode 100644 maithili_dsl/__main__.py create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt diff --git a/.gitignore b/.gitignore index 5604e68..2033c70 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__/ # Virtualenv .env/ +.venv/ venv/ ENV/ diff --git a/maithili_dsl/__init__.py b/maithili_dsl/__init__.py index e69de29..a564c5e 100644 --- a/maithili_dsl/__init__.py +++ b/maithili_dsl/__init__.py @@ -0,0 +1,14 @@ +"""Maithili DSL — run Python code written in Devanagari Maithili script.""" +from maithili_dsl.transpiler.transpile import transpile_maithili_code +from maithili_dsl.transpiler.linter import lint_maithili_code, translate_exception_to_maithili +from maithili_dsl.transpiler.numeral import convert_devanagari_numerals + +__version__ = "0.3.0" + +__all__ = [ + "__version__", + "transpile_maithili_code", + "lint_maithili_code", + "translate_exception_to_maithili", + "convert_devanagari_numerals", +] diff --git a/maithili_dsl/__main__.py b/maithili_dsl/__main__.py new file mode 100644 index 0000000..b75d362 --- /dev/null +++ b/maithili_dsl/__main__.py @@ -0,0 +1,5 @@ +"""Allow `python -m maithili_dsl ` invocation.""" +from maithili_dsl.cli import main + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/maithili_dsl/cli.py b/maithili_dsl/cli.py index ae4cf27..6516e5a 100644 --- a/maithili_dsl/cli.py +++ b/maithili_dsl/cli.py @@ -97,12 +97,24 @@ def _validate_imports(python_code, translated_modules): return errors +EXIT_OK = 0 +EXIT_USAGE = 2 +EXIT_NOT_FOUND = 3 +EXIT_LINT_ERROR = 4 +EXIT_IMPORT_BLOCKED = 5 +EXIT_RUNTIME_ERROR = 6 + + def run_dmai_file(file_path): - """Load, lint, transpile, and execute a .dmai file.""" + """Load, lint, transpile, and execute a .dmai file. + + Returns an integer exit code (0 on success, non-zero on error) so + callers (the CLI wrapper and CI) can detect failures reliably. + """ path = Path(file_path) if not path.exists(): print(f"Error: File '{file_path}' not found.") - return + return EXIT_NOT_FOUND with open(path, 'r', encoding='utf-8') as file: maithili_code = file.read() @@ -113,7 +125,7 @@ def run_dmai_file(file_path): for err in errors: print(" -", err) print("कोड चलायल नहि गेल।\n") - return + return EXIT_LINT_ERROR try: maithili_code, translated_modules = translate_imports(maithili_code) @@ -125,7 +137,7 @@ def run_dmai_file(file_path): if import_errors: for err in import_errors: print(f"⚠️ त्रुटि: {err}") - return + return EXIT_IMPORT_BLOCKED # Build sandboxed execution environment safe_builtins = {name: getattr(_builtins, name) for name in _SAFE_BUILTIN_NAMES} @@ -133,22 +145,30 @@ def run_dmai_file(file_path): exec_globals = {"__builtins__": safe_builtins} exec(python_code, exec_globals) + return EXIT_OK except Exception as e: translated = translate_exception_to_maithili(e) print("⚠️ त्रुटि:", translated) + return EXIT_RUNTIME_ERROR -def main(): - import sys - if len(sys.argv) < 2: +def main(argv=None): + """CLI entry point. Returns an integer exit code.""" + if argv is None: + argv = sys.argv[1:] + + if argv and argv[0] in ('-V', '--version'): + from maithili_dsl import __version__ + print(f"python_maithili {__version__}") + return EXIT_OK + + if not argv: print("Usage: python_maithili ") - else: - run_dmai_file(sys.argv[1]) + return EXIT_USAGE + + return run_dmai_file(argv[0]) if __name__ == "__main__": - if len(sys.argv) < 2: - print("Usage: python run_dmai.py ") - else: - run_dmai_file(sys.argv[1]) + raise SystemExit(main()) diff --git a/maithili_dsl/transpiler/__init__.py b/maithili_dsl/transpiler/__init__.py index e69de29..e04cac7 100644 --- a/maithili_dsl/transpiler/__init__.py +++ b/maithili_dsl/transpiler/__init__.py @@ -0,0 +1,14 @@ +"""Transpiler package: Devanagari Maithili → Python.""" +from maithili_dsl.transpiler.transpile import transpile_maithili_code +from maithili_dsl.transpiler.linter import lint_maithili_code, translate_exception_to_maithili +from maithili_dsl.transpiler.numeral import convert_devanagari_numerals +from maithili_dsl.transpiler.mappings import DEVNAGIRI_KEYWORD_MAP, DEVNAGIRI_NUMERAL_MAP + +__all__ = [ + "transpile_maithili_code", + "lint_maithili_code", + "translate_exception_to_maithili", + "convert_devanagari_numerals", + "DEVNAGIRI_KEYWORD_MAP", + "DEVNAGIRI_NUMERAL_MAP", +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..77745a3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,65 @@ +[build-system] +requires = ["setuptools>=61", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "python_maithili" +version = "0.3.0" +description = "Run Python code written in Maithili using Devanagari script" +readme = "README.md" +license = { file = "LICENSE" } +authors = [ + { name = "Bishwas Jha", email = "jha.bishwas@gmail.com" }, +] +requires-python = ">=3.9" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Natural Language :: Hindi", + "Topic :: Software Development :: Interpreters", +] + +[project.urls] +Repository = "https://github.com/alphacrack/python-maithili-dsl" +Issues = "https://github.com/alphacrack/python-maithili-dsl/issues" + +[project.scripts] +python_maithili = "maithili_dsl.cli:main" + +[tool.setuptools.packages.find] +include = ["maithili_dsl*"] +exclude = ["tests*"] + +[tool.pytest.ini_options] +minversion = "7.0" +testpaths = ["tests"] +addopts = [ + "-ra", + "--strict-markers", + "--strict-config", +] +markers = [ + "smoke: end-to-end CLI smoke tests using example .dmai files", + "security: sandbox and import-whitelist enforcement tests", +] + +[tool.coverage.run] +source = ["maithili_dsl"] +branch = true +omit = [ + "maithili_dsl/__main__.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if __name__ == .__main__.:", + "raise NotImplementedError", +] +show_missing = true +skip_covered = false diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..3a4df92 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +# Development dependencies for python_maithili. +# Runtime has zero dependencies — everything here is for tests and tooling. +pytest>=7.4,<9 +pytest-cov>=4.1,<6 +pytest-timeout>=2.2,<3 diff --git a/setup.py b/setup.py index bd9d1d9..f6ea8c9 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,32 @@ +"""Legacy setup shim — canonical metadata lives in pyproject.toml. -# setup.py +Kept for compatibility with tools that still invoke setup.py directly. +Version is read from the package __init__ via regex (not import) so +the shim works in isolated build environments where the package is +not yet importable. +""" +import re +from pathlib import Path from setuptools import setup, find_packages + +def _read_version() -> str: + init_py = Path(__file__).parent / "maithili_dsl" / "__init__.py" + text = init_py.read_text(encoding="utf-8") + match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', text, re.M) + if not match: + raise RuntimeError("Could not locate __version__ in maithili_dsl/__init__.py") + return match.group(1) + + setup( name='python_maithili', - version='0.2.0', + version=_read_version(), description='Run Python code written in Maithili using Devanagari script', author='Bishwas Jha', author_email='jha.bishwas@gmail.com', - packages=find_packages(), + packages=find_packages(exclude=['tests', 'tests.*']), include_package_data=True, entry_points={ 'console_scripts': [ @@ -18,8 +35,12 @@ }, classifiers=[ 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', ], - python_requires='>=3.6', -) \ No newline at end of file + python_requires='>=3.9', +) From 42b2253f75470725ff71f27c14dffa27bd9e9336 Mon Sep 17 00:00:00 2001 From: Bishwas Jha Date: Fri, 17 Apr 2026 19:52:42 +0200 Subject: [PATCH 09/11] test: add pytest suite with functional, security, linter, CLI, smoke tiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes AUDIT_REPORT.md finding CQ-002 (HIGH): the project had zero automated tests. This commit adds 142 tests across 6 files: - tests/test_numeral.py — every Devanagari digit + mixed text - tests/test_transpile.py — every keyword in DEVNAGIRI_KEYWORD_MAP parametrized + explicit CQ-001 regression cases (string literals and word boundaries) + "transpiled output is valid Python" - tests/test_linter.py — clean-code pass, == not flagged, नव constructor not flagged as unused, unbalanced paren/quote, line length, snake_case toggle, all known exception translations - tests/test_security.py — _validate_imports rejects raw Python imports; _make_safe_import only permits whitelist; exec/eval/open/compile/breakpoint absent from safe_builtins; end-to-end attacks (import os, exec(), eval(), open()) rejected by run_dmai_file with correct exit codes - tests/test_cli.py — every EXIT_* code is reachable by a test; subprocess invocation via `python -m maithili_dsl` works for --version, usage, and file execution - tests/test_smoke.py — every example in examples/ runs end-to-end (parametrized); error.dmai is expected to be rejected by the linter, not executed Coverage (measured locally with pytest-cov): maithili_dsl/__init__.py 100% maithili_dsl/cli.py 98% maithili_dsl/transpiler/__init__.py 100% maithili_dsl/transpiler/linter.py 90% maithili_dsl/transpiler/mappings.py 100% maithili_dsl/transpiler/numeral.py 100% maithili_dsl/transpiler/transpile.py 98% TOTAL 95% Critical modules (cli, transpile, linter) all at or above the 90% gate set in the implementation plan. Run with: pytest -v --cov=maithili_dsl --cov-report=term-missing Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/__init__.py | 0 tests/conftest.py | 49 ++++++++ tests/test_cli.py | 121 +++++++++++++++++++ tests/test_linter.py | 216 ++++++++++++++++++++++++++++++++++ tests/test_numeral.py | 34 ++++++ tests/test_security.py | 253 ++++++++++++++++++++++++++++++++++++++++ tests/test_smoke.py | 73 ++++++++++++ tests/test_transpile.py | 217 ++++++++++++++++++++++++++++++++++ 8 files changed, 963 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_cli.py create mode 100644 tests/test_linter.py create mode 100644 tests/test_numeral.py create mode 100644 tests/test_security.py create mode 100644 tests/test_smoke.py create mode 100644 tests/test_transpile.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7586356 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,49 @@ +"""Shared fixtures for the Maithili DSL test suite.""" +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parent.parent +EXAMPLES_DIR = REPO_ROOT / "examples" + + +@pytest.fixture +def tmp_dmai_file(tmp_path): + """Factory that writes a .dmai file into a tmp dir and returns its path.""" + + def _make(content: str, name: str = "test.dmai") -> Path: + path = tmp_path / name + path.write_text(content, encoding="utf-8") + return path + + return _make + + +@pytest.fixture +def run_cli(): + """Run the CLI as a subprocess and return CompletedProcess. + + Uses `python -m maithili_dsl` so the tests exercise the real entry + point a user would hit, not just importable functions. + """ + + def _run(*args, cwd=None, timeout=30) -> subprocess.CompletedProcess: + return subprocess.run( + [sys.executable, "-m", "maithili_dsl", *args], + capture_output=True, + text=True, + cwd=cwd or str(REPO_ROOT), + timeout=timeout, + ) + + return _run + + +@pytest.fixture +def examples_dir() -> Path: + return EXAMPLES_DIR diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..ce1d080 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,121 @@ +"""Tests for the CLI entry point (run_dmai_file + main). + +Exercises every exit-code path and confirms the user-facing messages +are stable. +""" +from pathlib import Path + +import pytest + +from maithili_dsl.cli import ( + EXIT_IMPORT_BLOCKED, + EXIT_LINT_ERROR, + EXIT_NOT_FOUND, + EXIT_OK, + EXIT_RUNTIME_ERROR, + EXIT_USAGE, + main, + run_dmai_file, +) + + +# --------------------------------------------------------------------------- +# run_dmai_file exit-code paths +# --------------------------------------------------------------------------- + +def test_missing_file_returns_not_found(capsys): + code = run_dmai_file("/nonexistent/path/to/file.dmai") + captured = capsys.readouterr() + assert code == EXIT_NOT_FOUND + assert "not found" in captured.out.lower() + + +def test_lint_error_returns_lint_exit_code(tmp_dmai_file, capsys): + # The leading = triggers the "'=' चिह्नक पहिले" lint error. + path = tmp_dmai_file("= ५\n") + code = run_dmai_file(str(path)) + captured = capsys.readouterr() + assert code == EXIT_LINT_ERROR + assert "लिंटर" in captured.out + + +def test_import_blocked_returns_blocked_exit_code(tmp_dmai_file): + # Raw Python import → blocked by _validate_imports. + path = tmp_dmai_file("import os\n") + code = run_dmai_file(str(path)) + assert code == EXIT_IMPORT_BLOCKED + + +def test_valid_file_returns_ok(tmp_dmai_file, capsys): + path = tmp_dmai_file("छपाउ(\"hello\")\n") + code = run_dmai_file(str(path)) + captured = capsys.readouterr() + assert code == EXIT_OK + assert "hello" in captured.out + + +def test_runtime_error_returns_runtime_exit_code(tmp_dmai_file, capsys): + # undefined variable → NameError inside exec → translated and exit 6. + path = tmp_dmai_file("छपाउ(अपरिभाषित_चर)\n") + code = run_dmai_file(str(path)) + captured = capsys.readouterr() + assert code == EXIT_RUNTIME_ERROR + assert "त्रुटि" in captured.out + + +# --------------------------------------------------------------------------- +# main() argv handling +# --------------------------------------------------------------------------- + +def test_main_with_no_args_shows_usage(capsys): + code = main([]) + captured = capsys.readouterr() + assert code == EXIT_USAGE + assert "Usage" in captured.out + + +def test_main_with_version_flag(capsys): + from maithili_dsl import __version__ + code = main(["--version"]) + captured = capsys.readouterr() + assert code == EXIT_OK + assert __version__ in captured.out + + +def test_main_with_short_version_flag(capsys): + code = main(["-V"]) + captured = capsys.readouterr() + assert code == EXIT_OK + + +def test_main_dispatches_to_run_dmai_file(tmp_dmai_file, capsys): + path = tmp_dmai_file("छपाउ(\"dispatched\")\n") + code = main([str(path)]) + captured = capsys.readouterr() + assert code == EXIT_OK + assert "dispatched" in captured.out + + +# --------------------------------------------------------------------------- +# Subprocess: verify `python -m maithili_dsl` works end-to-end +# --------------------------------------------------------------------------- + +def test_python_m_maithili_dsl_version(run_cli): + result = run_cli("--version") + assert result.returncode == 0 + assert "python_maithili" in result.stdout + + +def test_python_m_maithili_dsl_usage_on_no_args(run_cli): + result = run_cli() + assert result.returncode == EXIT_USAGE + assert "Usage" in result.stdout + + +def test_python_m_maithili_dsl_runs_file(run_cli, tmp_dmai_file): + path = tmp_dmai_file("छपाउ(\"subprocess-ok\")\n") + result = run_cli(str(path)) + assert result.returncode == 0, ( + f"stdout={result.stdout!r} stderr={result.stderr!r}" + ) + assert "subprocess-ok" in result.stdout diff --git a/tests/test_linter.py b/tests/test_linter.py new file mode 100644 index 0000000..e13bf93 --- /dev/null +++ b/tests/test_linter.py @@ -0,0 +1,216 @@ +"""Unit tests for the Maithili linter. + +Covers the post-fix behavior: == is not flagged as assignment, नव +(constructor) is not flagged as unused, and all the structural checks +still fire on genuinely broken input. +""" +import pytest + +from maithili_dsl.transpiler.linter import ( + LINT_CONFIG, + is_snake_case, + lint_maithili_code, + translate_exception_to_maithili, +) + + +# --------------------------------------------------------------------------- +# Clean / passing code +# --------------------------------------------------------------------------- + +def test_empty_code_has_no_errors(): + assert lint_maithili_code("") == [] + + +def test_simple_function_with_usage_passes(): + code = ( + "कार्य नमस्कार():\n" + " छपाउ(\"hi\")\n" + "\n" + "नमस्कार()\n" + ) + assert lint_maithili_code(code) == [] + + +def test_class_with_constructor_passes(): + code = ( + "वर्ग व्यक्ति:\n" + " कार्य नव(स्वयं, नाम):\n" + " स्वयं.नाम = नाम\n" + "\n" + "व्यक्ति१ = व्यक्ति(\"सुमन\")\n" + ) + errors = lint_maithili_code(code) + # The नव constructor is called implicitly via instantiation, + # so it MUST NOT be flagged as unused. + unused_errors = [e for e in errors if "नव" in e and "प्रयोग नहि" in e] + assert unused_errors == [], f"नव falsely flagged as unused: {errors}" + + +# --------------------------------------------------------------------------- +# Comparison operators are not assignments +# --------------------------------------------------------------------------- + +class TestComparisonNotFlaggedAsAssignment: + """Pre-fix bug: `x == 5` parsed as an assignment with var name + `x ` and produced a spurious 'invalid variable name' error.""" + + def test_equality_operator(self): + code = "यदि x == ५:\n छपाउ(x)\n" + errors = lint_maithili_code(code) + assert not any("चर नाम" in e for e in errors), ( + f"== comparison falsely flagged: {errors}" + ) + + def test_inequality_operator(self): + code = "यदि x != ५:\n छपाउ(x)\n" + errors = lint_maithili_code(code) + assert not any("चर नाम" in e for e in errors) + + def test_lte_operator(self): + code = "यदि x <= ५:\n छपाउ(x)\n" + errors = lint_maithili_code(code) + assert not any("चर नाम" in e for e in errors) + + def test_gte_operator(self): + code = "यदि x >= ५:\n छपाउ(x)\n" + errors = lint_maithili_code(code) + assert not any("चर नाम" in e for e in errors) + + +# --------------------------------------------------------------------------- +# Structural errors still fire +# --------------------------------------------------------------------------- + +def test_leading_equals_sign_flagged(): + errors = lint_maithili_code("= ५\n") + assert any("'=' चिह्नक पहिले" in e for e in errors) + + +def test_unbalanced_parens_flagged(): + errors = lint_maithili_code("छपाउ(x\n") + assert any("गोल ब्रैकेट" in e for e in errors) + + +def test_unbalanced_quotes_flagged(): + errors = lint_maithili_code('छपाउ("hi)\n') + assert any("उद्धरण" in e for e in errors) + + +def test_suspicious_block_flagged(): + errors = lint_maithili_code("कोनो चीज:\nwde\n") + assert any("संदिग्ध ढाँचा" in e for e in errors) + + +def test_missing_indent_after_colon_flagged(): + code = "कार्य f():\nछपाउ(\"x\")\n" + errors = lint_maithili_code(code) + assert any("इनडेन्टेशन" in e for e in errors) + + +# --------------------------------------------------------------------------- +# Line length +# --------------------------------------------------------------------------- + +def test_long_line_flagged_by_default(): + long_line = "x = \"" + ("अ" * 100) + "\"\n" + errors = lint_maithili_code(long_line) + assert any("पंक्ति बहुत लंबा" in e for e in errors) + + +def test_line_length_config_override(): + config = dict(LINT_CONFIG, max_line_length=200) + long_line = "x = \"" + ("अ" * 100) + "\"\n" + errors = lint_maithili_code(long_line, config=config) + assert not any("पंक्ति बहुत लंबा" in e for e in errors) + + +# --------------------------------------------------------------------------- +# Unused function detection +# --------------------------------------------------------------------------- + +def test_unused_regular_function_flagged(): + code = "कार्य अप्रयुक्त():\n फेर करू १\n" + errors = lint_maithili_code(code) + assert any("अप्रयुक्त" in e and "प्रयोग नहि" in e for e in errors) + + +def test_used_function_not_flagged(): + code = ( + "कार्य प्रयुक्त():\n" + " फेर करू १\n" + "\n" + "प्रयुक्त()\n" + ) + errors = lint_maithili_code(code) + assert not any("प्रयुक्त" in e and "प्रयोग नहि" in e for e in errors) + + +def test_init_never_flagged_even_when_not_called_by_name(): + # नव is called implicitly via class instantiation — never by name. + code = ( + "वर्ग क:\n" + " कार्य नव(स्वयं):\n" + " स्वयं.x = १\n" + "\n" + "ओब = क()\n" + ) + errors = lint_maithili_code(code) + assert not any("नव" in e and "प्रयोग नहि" in e for e in errors) + + +# --------------------------------------------------------------------------- +# is_snake_case / snake_case enforcement toggle +# --------------------------------------------------------------------------- + +def test_is_snake_case_positive_cases(): + assert is_snake_case("foo_bar") + assert is_snake_case("x") + assert is_snake_case("_private") + assert is_snake_case("name123") + + +def test_is_snake_case_negative_cases(): + assert not is_snake_case("CamelCase") + assert not is_snake_case("") + assert not is_snake_case("1startswithdigit") + + +def test_snake_case_not_enforced_by_default(): + code = "camelCase = १\n" + errors = lint_maithili_code(code) + assert not any("snake_case" in e for e in errors) + + +def test_snake_case_enforced_when_enabled(): + config = dict(LINT_CONFIG, enforce_snake_case=True) + code = "camelCase = 1\n" + errors = lint_maithili_code(code, config=config) + assert any("snake_case" in e for e in errors) + + +# --------------------------------------------------------------------------- +# Exception translation +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("exc_type,must_contain", [ + (SyntaxError, "वाक्य संरचना"), + (NameError, "नाम"), + (TypeError, "प्रकार"), + (ValueError, "मान"), + (AttributeError, "गुण"), + (IndexError, "सूची सीमा"), + (KeyError, "कुंजी"), +]) +def test_known_exceptions_translated(exc_type, must_contain): + exc = exc_type("demo") + out = translate_exception_to_maithili(exc) + assert must_contain in out + + +def test_unknown_exception_falls_through(): + class CustomError(Exception): + pass + + out = translate_exception_to_maithili(CustomError("demo")) + assert "CustomError" in out diff --git a/tests/test_numeral.py b/tests/test_numeral.py new file mode 100644 index 0000000..5641ea5 --- /dev/null +++ b/tests/test_numeral.py @@ -0,0 +1,34 @@ +"""Unit tests for Devanagari → ASCII numeral conversion.""" +import pytest + +from maithili_dsl.transpiler.numeral import convert_devanagari_numerals +from maithili_dsl.transpiler.mappings import DEVNAGIRI_NUMERAL_MAP + + +@pytest.mark.parametrize("dev,ascii_", list(DEVNAGIRI_NUMERAL_MAP.items())) +def test_each_devanagari_digit_converts(dev, ascii_): + assert convert_devanagari_numerals(dev) == ascii_ + + +def test_multi_digit_number(): + assert convert_devanagari_numerals("१२३४५६७८९०") == "1234567890" + + +def test_mixed_devanagari_and_ascii(): + assert convert_devanagari_numerals("x = १0 + २") == "x = 10 + 2" + + +def test_empty_string_passes_through(): + assert convert_devanagari_numerals("") == "" + + +def test_no_digits_unchanged(): + text = "नमस्कार संसार" + assert convert_devanagari_numerals(text) == text + + +def test_digits_inside_identifier(): + # The converter does NOT know about word boundaries — this is + # intentional (digits are freely convertible everywhere in the + # file, including inside identifiers). + assert convert_devanagari_numerals("संख्या१") == "संख्या1" diff --git a/tests/test_security.py b/tests/test_security.py new file mode 100644 index 0000000..1b6787e --- /dev/null +++ b/tests/test_security.py @@ -0,0 +1,253 @@ +"""Security tests for the sandboxed execution model. + +Closes AUDIT_REPORT.md finding SEC-001 (CRITICAL). These tests prove +that a malicious .dmai file cannot escape the sandbox via: + + - Raw Python imports injected post-transpile + - Access to disallowed modules through the Maithili import map + - Direct calls to exec / eval / compile / open / breakpoint + - __import__ bypass + +Each test pins a specific attack surface so that a regression that +re-opens one hole is immediately caught. +""" +import pytest + +from maithili_dsl import cli +from maithili_dsl.cli import ( + MAITHILI_MODULES, + _ALLOWED_MODULES, + _SAFE_BUILTIN_NAMES, + _make_safe_import, + _validate_imports, + run_dmai_file, + translate_imports, + EXIT_OK, + EXIT_IMPORT_BLOCKED, + EXIT_RUNTIME_ERROR, +) + + +# --------------------------------------------------------------------------- +# _validate_imports: raw Python import statements are rejected unless the +# module name was legitimately translated from a Maithili import. +# --------------------------------------------------------------------------- + +class TestValidateImportsRejectsRawPythonImports: + + def test_raw_import_os_rejected(self): + errors = _validate_imports("import os\n", translated_modules=set()) + assert errors, "import os should be blocked" + + def test_raw_import_subprocess_rejected(self): + errors = _validate_imports( + "import subprocess\n", translated_modules=set() + ) + assert errors + + def test_raw_import_socket_rejected(self): + errors = _validate_imports( + "import socket\n", translated_modules=set() + ) + assert errors + + def test_raw_import_ctypes_rejected(self): + errors = _validate_imports( + "import ctypes\n", translated_modules=set() + ) + assert errors + + def test_raw_import_pickle_rejected(self): + errors = _validate_imports( + "import pickle\n", translated_modules=set() + ) + assert errors + + def test_raw_from_import_rejected(self): + errors = _validate_imports( + "from subprocess import run\n", translated_modules=set() + ) + assert errors + + def test_import_after_maithili_translation_allowed(self): + # A Maithili `आयात गणित` becomes `import math` and is added + # to the translated_modules set — then _validate_imports must + # allow it through. + errors = _validate_imports( + "import math\n", translated_modules={"math"} + ) + assert errors == [] + + def test_non_import_lines_are_ignored(self): + code = "x = 1\nprint(x)\n" + assert _validate_imports(code, translated_modules=set()) == [] + + +# --------------------------------------------------------------------------- +# _make_safe_import: __import__ replacement only permits whitelist. +# --------------------------------------------------------------------------- + +class TestSafeImportWhitelist: + + def setup_method(self): + self.safe_import = _make_safe_import() + + def test_whitelisted_module_allowed(self): + # math is in MAITHILI_MODULES values, so whitelisted. + mod = self.safe_import("math") + assert mod.pi > 3 + + def test_non_whitelisted_module_raises_importerror(self): + with pytest.raises(ImportError): + self.safe_import("subprocess") + + def test_ctypes_blocked(self): + with pytest.raises(ImportError): + self.safe_import("ctypes") + + def test_socket_blocked(self): + with pytest.raises(ImportError): + self.safe_import("socket") + + def test_pickle_blocked(self): + with pytest.raises(ImportError): + self.safe_import("pickle") + + def test_submodule_of_whitelisted_allowed(self): + # os.path is in the whitelist values explicitly; verify that + # `os` base-module lookups resolve. + assert "os.path" in _ALLOWED_MODULES + mod = self.safe_import("os.path") + assert mod.sep in ("/", "\\") + + def test_error_message_is_in_maithili(self): + with pytest.raises(ImportError) as exc_info: + self.safe_import("subprocess") + assert "आयात" in str(exc_info.value) + + +# --------------------------------------------------------------------------- +# _SAFE_BUILTIN_NAMES: forbidden builtins are absent. +# --------------------------------------------------------------------------- + +class TestForbiddenBuiltinsAbsent: + + @pytest.mark.parametrize("name", [ + "exec", "eval", "compile", "open", "breakpoint", + "__import__", # only safe_import wrapper, not the real thing + ]) + def test_name_not_in_safe_builtins_list(self, name): + assert name not in _SAFE_BUILTIN_NAMES, ( + f"{name!r} must not appear in _SAFE_BUILTIN_NAMES" + ) + + def test_expected_safe_names_present(self): + # Sanity: harmless builtins we DO want are still exposed. + for name in ("print", "len", "range", "str", "int", "list"): + assert name in _SAFE_BUILTIN_NAMES + + +# --------------------------------------------------------------------------- +# translate_imports: records which modules were legitimately translated. +# --------------------------------------------------------------------------- + +class TestTranslateImports: + + def test_maithili_import_recorded(self): + code = "आयात गणित\n" + translated, modules = translate_imports(code) + assert "import math" in translated + assert "math" in modules + + def test_multiple_imports_all_recorded(self): + code = "आयात गणित\nआयात यादृच्छिक\n" + translated, modules = translate_imports(code) + assert "math" in modules + assert "random" in modules + + def test_no_maithili_imports_produces_empty_set(self): + translated, modules = translate_imports("x = १\n") + assert modules == set() + + def test_attribute_access_also_counts(self): + # Using गणित.pi pulls math into translated_modules even without + # an explicit आयात statement, because the transpiler still + # rewrites गणित. to math. and the sandbox needs to allow it. + code = "x = गणित.pi\n" + translated, modules = translate_imports(code) + assert "math" in modules + + +# --------------------------------------------------------------------------- +# End-to-end: malicious .dmai files are rejected by run_dmai_file +# --------------------------------------------------------------------------- + +@pytest.mark.security +class TestMaliciousDmaiBlocked: + """Write real .dmai files that try to break out, and confirm + run_dmai_file rejects them with a non-zero exit code.""" + + def test_attempting_import_os_via_raw_python_rejected( + self, tmp_dmai_file, capsys + ): + # The lexical form "import os" isn't a Maithili keyword so it + # survives transpilation unchanged, but _validate_imports + # should catch it before exec. + path = tmp_dmai_file("import os\nos.system(\"echo pwned\")\n") + code = run_dmai_file(str(path)) + captured = capsys.readouterr() + # It may be blocked at lint (long-line false positive is unlikely + # for this short line) or at _validate_imports. Either path must + # produce a non-zero exit. + assert code != EXIT_OK + assert "आयात अनुमति नहि" in captured.out or "लिंटर" in captured.out + + def test_attempting_to_call_exec_raises(self, tmp_dmai_file, capsys): + # exec is not in safe_builtins, so this NameError-s inside exec. + path = tmp_dmai_file("exec(\"print(1)\")\n") + code = run_dmai_file(str(path)) + captured = capsys.readouterr() + assert code == EXIT_RUNTIME_ERROR + # NameError gets translated to Maithili by the exception handler. + assert "त्रुटि" in captured.out + + def test_attempting_to_call_open_raises(self, tmp_dmai_file, capsys): + path = tmp_dmai_file("open(\"/etc/passwd\")\n") + code = run_dmai_file(str(path)) + assert code == EXIT_RUNTIME_ERROR + + def test_attempting_to_call_eval_raises(self, tmp_dmai_file, capsys): + path = tmp_dmai_file("eval(\"1+1\")\n") + code = run_dmai_file(str(path)) + assert code == EXIT_RUNTIME_ERROR + + def test_maithili_import_of_allowed_module_works( + self, tmp_dmai_file, capsys + ): + # Positive control: a legitimate Maithili import of a + # whitelisted module should succeed. + path = tmp_dmai_file("आयात गणित\nछपाउ(गणित.pi)\n") + code = run_dmai_file(str(path)) + captured = capsys.readouterr() + assert code == EXIT_OK, ( + f"Clean program blocked unexpectedly: {captured.out}" + ) + assert "3.14" in captured.out + + +# --------------------------------------------------------------------------- +# Whitelist shape: every MAITHILI_MODULES target is in _ALLOWED_MODULES. +# --------------------------------------------------------------------------- + +def test_allowed_modules_matches_maithili_modules(): + assert _ALLOWED_MODULES == set(MAITHILI_MODULES.values()) + + +@pytest.mark.parametrize("dangerous", [ + "subprocess", "socket", "ctypes", "pickle", "shutil", + "urllib", "http", "ftplib", "telnetlib", +]) +def test_dangerous_module_not_in_whitelist(dangerous): + assert dangerous not in _ALLOWED_MODULES, ( + f"{dangerous} must never be in the import whitelist" + ) diff --git a/tests/test_smoke.py b/tests/test_smoke.py new file mode 100644 index 0000000..d5400bf --- /dev/null +++ b/tests/test_smoke.py @@ -0,0 +1,73 @@ +"""Smoke tests: every example .dmai in examples/ executes end-to-end. + +These tests drive the CLI as a real subprocess — the same code path a +user hits when running `python -m maithili_dsl examples/hello.dmai` — +so they catch packaging, entry-point, and integration regressions that +in-process tests would miss. + +`error.dmai` is expected to fail linting (it contains intentionally +broken code for demo purposes) — so it gets an inverted assertion. +""" +from __future__ import annotations + +import pytest + +from maithili_dsl.cli import EXIT_LINT_ERROR, EXIT_OK + + +EXPECTED_OK = { + "hello.dmai": ["हम मैथिली में कोड"], + "person.dmai": ["हमर नाम सुमन"], + # Devanagari numerals get converted to ASCII even inside string + # literals (documented behavior of the numeral pre-pass). + "calculator.dmai": ["जोड़ का परिणाम: 15", "5 × 1 = 5", "5 × 10 = 50"], + "shopping_list.dmai": ["दूध", "रोटी", "कुल वस्तु: 4"], +} + + +@pytest.mark.smoke +@pytest.mark.parametrize("filename,expected_fragments", sorted(EXPECTED_OK.items())) +def test_example_runs_successfully(run_cli, examples_dir, filename, expected_fragments): + """Each clean example .dmai file produces exit 0 and its expected output fragments.""" + path = examples_dir / filename + assert path.exists(), f"Example missing: {path}" + + result = run_cli(str(path)) + + assert result.returncode == EXIT_OK, ( + f"{filename} failed with exit {result.returncode}\n" + f"stdout:\n{result.stdout}\n" + f"stderr:\n{result.stderr}" + ) + for fragment in expected_fragments: + assert fragment in result.stdout, ( + f"{filename}: expected {fragment!r} in output, got:\n{result.stdout}" + ) + + +@pytest.mark.smoke +def test_error_example_fails_linting(run_cli, examples_dir): + """error.dmai contains intentionally broken code — it must be + rejected by the linter, not executed.""" + path = examples_dir / "error.dmai" + assert path.exists() + + result = run_cli(str(path)) + + assert result.returncode == EXIT_LINT_ERROR + assert "लिंटर" in result.stdout + + +@pytest.mark.smoke +def test_import_example_from_module_name(run_cli, tmp_dmai_file): + """Spot-check that Maithili module imports work end-to-end via CLI.""" + path = tmp_dmai_file( + "आयात गणित\n" + "छपाउ(\"pi ~= \" + str(round(गणित.pi, २)))\n", + name="import_math.dmai", + ) + result = run_cli(str(path)) + assert result.returncode == EXIT_OK, ( + f"stdout={result.stdout!r} stderr={result.stderr!r}" + ) + assert "3.14" in result.stdout diff --git a/tests/test_transpile.py b/tests/test_transpile.py new file mode 100644 index 0000000..439f872 --- /dev/null +++ b/tests/test_transpile.py @@ -0,0 +1,217 @@ +"""Functional tests for the Maithili → Python transpiler. + +Covers three major behaviors: + 1. Every keyword in DEVNAGIRI_KEYWORD_MAP maps correctly at word + boundaries. + 2. Regression tests for AUDIT_REPORT.md CQ-001 (CRITICAL): keywords + inside string literals and inside longer identifiers must NOT + be replaced. + 3. The transpiled output is valid Python that can be compiled. +""" +import ast +import pytest + +from maithili_dsl.transpiler.transpile import transpile_maithili_code +from maithili_dsl.transpiler.mappings import DEVNAGIRI_KEYWORD_MAP + + +# --------------------------------------------------------------------------- +# 1. Keyword coverage — every mapping works in isolation +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("maithili_kw,py_kw", list(DEVNAGIRI_KEYWORD_MAP.items())) +def test_each_keyword_translates(maithili_kw, py_kw): + """Every entry in DEVNAGIRI_KEYWORD_MAP must translate when used + as a standalone token.""" + source = f"{maithili_kw}" + result = transpile_maithili_code(source) + assert result == py_kw, ( + f"Expected {maithili_kw!r} → {py_kw!r}, got {result!r}" + ) + + +def test_function_definition_transpiles(): + src = "कार्य नमस्कार():\n छपाउ(\"hi\")\n" + out = transpile_maithili_code(src) + assert "def नमस्कार():" in out + assert "print(\"hi\")" in out + + +def test_class_with_constructor_and_self(): + src = ( + "वर्ग व्यक्ति:\n" + " कार्य नव(स्वयं, नाम):\n" + " स्वयं.नाम = नाम\n" + ) + out = transpile_maithili_code(src) + assert "class व्यक्ति:" in out + assert "def __init__(self, नाम):" in out + assert "self.नाम = नाम" in out + + +def test_for_in_range_with_numerals(): + src = "प्रत्येक i में range(१, ५):\n छपाउ(i)\n" + out = transpile_maithili_code(src) + assert "for i in range(1, 5):" in out + assert "print(i)" in out + + +def test_if_else_bool_none(): + src = ( + "यदि सत्य:\n" + " x = शून्य\n" + "नहि त:\n" + " x = मिथ्या\n" + ) + out = transpile_maithili_code(src) + assert "if True:" in out + assert "x = None" in out + assert "else:" in out + assert "x = False" in out + + +def test_import_from_as(): + src = "सँ गणित आयात sqrt स्वरूपे s\n" + out = transpile_maithili_code(src) + # Note: module-name translation happens in cli.translate_imports, + # not in the transpiler. The transpiler only touches keywords. + assert out == "from गणित import sqrt as s\n" + + +def test_return_statement(): + src = "कार्य f():\n फेर करू ४२\n" + out = transpile_maithili_code(src) + assert "return 42" in out + + +# --------------------------------------------------------------------------- +# 2. CQ-001 regressions — string-aware + word-boundary behavior +# --------------------------------------------------------------------------- + +class TestStringLiteralPreservation: + """AUDIT_REPORT.md CQ-001: keywords inside string literals must + NOT be replaced. The original bug: + छपाउ("यह में है") -> print("यह in है") ← WRONG + """ + + def test_maithili_keyword_inside_double_quotes_preserved(self): + src = 'छपाउ("यह में है")' + out = transpile_maithili_code(src) + assert out == 'print("यह में है")', ( + f"में was wrongly replaced inside string literal: {out!r}" + ) + + def test_maithili_keyword_inside_single_quotes_preserved(self): + src = "छपाउ('यह में है')" + out = transpile_maithili_code(src) + assert out == "print('यह में है')" + + def test_multiple_keywords_in_one_string(self): + src = 'छपाउ("कार्य यदि में वर्ग")' + out = transpile_maithili_code(src) + # The surrounding छपाउ becomes print, but inside the string + # every keyword must survive. + assert out == 'print("कार्य यदि में वर्ग")' + + def test_escaped_quote_in_string(self): + src = r'छपाउ("यह \"में\" है")' + out = transpile_maithili_code(src) + assert 'print(' in out + assert r'\"में\"' in out + + def test_keyword_in_comment_preserved(self): + src = "x = १ # यह में सत्य अछि\n" + out = transpile_maithili_code(src) + assert "# यह में सत्य अछि" in out, ( + "Comments should not have keywords replaced" + ) + + +class TestWordBoundaries: + """AUDIT_REPORT.md CQ-001: keywords matched as substrings of + longer identifiers must NOT be replaced. The original bug: + नवयुग = ५ -> __init__युग = 5 ← WRONG + """ + + def test_keyword_prefix_of_identifier_preserved(self): + # नव (→ __init__) should NOT fire inside नवयुग + src = "नवयुग = ५" + out = transpile_maithili_code(src) + assert out == "नवयुग = 5", f"नव fired inside नवयुग: {out!r}" + + def test_keyword_suffix_of_identifier_preserved(self): + # में (→ in) should NOT fire inside कारमें + src = "कारमें = १" + out = transpile_maithili_code(src) + assert out == "कारमें = 1" + + def test_keyword_as_prefix_then_space_does_fire(self): + # Sanity check the other side: with a space after, नव fires. + src = "नव " + out = transpile_maithili_code(src) + assert out == "__init__ " + + def test_two_adjacent_keywords(self): + # Keywords separated by whitespace should both fire. + src = "यदि सत्य:" + out = transpile_maithili_code(src) + assert out == "if True:" + + +# --------------------------------------------------------------------------- +# 3. End-to-end: transpiled Python must parse +# --------------------------------------------------------------------------- + +class TestTranspiledPythonIsValid: + def test_hello_parses(self): + src = "कार्य नमस्कार():\n छपाउ(\"hi\")\n\nनमस्कार()\n" + out = transpile_maithili_code(src) + ast.parse(out) # raises if invalid + + def test_class_parses(self): + src = ( + "वर्ग व्यक्ति:\n" + " कार्य नव(स्वयं, नाम):\n" + " स्वयं.नाम = नाम\n" + "\n" + "व्यक्ति१ = व्यक्ति(\"सुमन\")\n" + ) + out = transpile_maithili_code(src) + ast.parse(out) + + def test_loop_parses(self): + src = "प्रत्येक i में range(१, ५):\n छपाउ(i)\n" + out = transpile_maithili_code(src) + ast.parse(out) + + +# --------------------------------------------------------------------------- +# 4. Edge cases +# --------------------------------------------------------------------------- + +def test_empty_input(): + assert transpile_maithili_code("") == "" + + +def test_only_whitespace(): + assert transpile_maithili_code(" \n\n ") == " \n\n " + + +def test_only_ascii_passes_through(): + src = "x = 1 + 2\n" + assert transpile_maithili_code(src) == src + + +def test_numerals_inside_strings_still_convert(): + # Documented current behavior: the numeral pass runs BEFORE + # tokenization, so digits inside strings also get converted. + # This may or may not be desirable, but it's the current contract. + src = 'छपाउ("५ × ४")' + out = transpile_maithili_code(src) + assert out == 'print("5 × 4")' + + +def test_multiple_statements_preserve_newlines(): + src = "x = १\ny = २\nz = x + y\n" + out = transpile_maithili_code(src) + assert out == "x = 1\ny = 2\nz = x + y\n" From 6e2d0a4ce28815fef8aa285bb062608c9a35c972 Mon Sep 17 00:00:00 2001 From: Bishwas Jha Date: Fri, 17 Apr 2026 19:53:17 +0200 Subject: [PATCH 10/11] ci: add GitHub Actions matrix (py3.9-3.12 x ubuntu/macos) + PR template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes AUDIT_REPORT.md finding #6 (P1): the project had no CI/CD gate — code could merge without any automated verification. The workflow runs on every push and PR to main and development: - Matrix: 4 Python versions x 2 operating systems = 8 jobs. - Installs the package editable + dev deps with pip caching keyed on requirements-dev.txt + pyproject.toml. - Verifies both CLI entry points (`python -m maithili_dsl` and the `python_maithili` console script) respond to --version. - Runs the full pytest suite with coverage. - Runs a bash smoke loop over every .dmai in examples/, treating error.dmai as expected-to-fail so a regression that starts executing it would be caught immediately. - Uploads coverage.xml as an artifact from the ubuntu + 3.12 job. Also adds .github/PULL_REQUEST_TEMPLATE.md to enforce test evidence and audit-reference fields on future PRs — so the thing that's in the root of this branch (a PR body tied to specific BACKLOG items) becomes the default for everyone. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/PULL_REQUEST_TEMPLATE.md | 42 +++++++++++++++++ .github/workflows/ci.yml | 77 ++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/ci.yml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..4df21a4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,42 @@ + + +## Summary + + + +## Motivation / Audit Reference + + + +## Test Evidence + +- [ ] `pytest -v` runs green locally +- [ ] Coverage is at or above the critical-module gate (cli / transpile / linter ≥90%) +- [ ] New tests added for any new behavior or regression +- [ ] `python -m maithili_dsl examples/*.dmai` smoke-tested the examples +- [ ] `python -m maithili_dsl --version` shows the expected version + + + +## Security Considerations + + + +## Breaking Changes + + + +## Checklist + +- [ ] CHANGELOG.md updated (Unreleased section) +- [ ] Version bumped if this is a release PR +- [ ] No new runtime dependencies added (or justification provided below) +- [ ] Docs updated if user-visible behavior changed diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..41e9f9c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,77 @@ +name: CI + +on: + push: + branches: [main, development] + pull_request: + branches: [main, development] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: test (${{ matrix.os }} / py${{ matrix.python-version }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: | + requirements-dev.txt + pyproject.toml + + - name: Install package + dev deps + run: | + python -m pip install --upgrade pip + pip install -e . + pip install -r requirements-dev.txt + + - name: Verify CLI entry point + run: | + python -m maithili_dsl --version + python_maithili --version + + - name: Run tests with coverage + run: | + pytest -v --cov=maithili_dsl --cov-report=term-missing --cov-report=xml + + - name: Smoke test — run every example + shell: bash + run: | + set -e + for f in examples/*.dmai; do + if [ "$(basename "$f")" = "error.dmai" ]; then + # error.dmai is intentionally broken; expect non-zero exit + if python -m maithili_dsl "$f"; then + echo "FAIL: $f should have been rejected by linter" + exit 1 + fi + echo "OK (expected failure): $f" + else + python -m maithili_dsl "$f" + echo "OK: $f" + fi + done + + - name: Upload coverage artifact + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' + uses: actions/upload-artifact@v4 + with: + name: coverage-xml + path: coverage.xml + if-no-files-found: warn From 17b262835be3000872015ae5583a4a037aaaf9e8 Mon Sep 17 00:00:00 2001 From: Bishwas Jha Date: Fri, 17 Apr 2026 19:55:03 +0200 Subject: [PATCH 11/11] docs: add SECURITY.md + CHANGELOG.md, fix README placeholder, rewrite CONTRIBUTING Closes four compliance / docs findings from AUDIT_REPORT.md: - SEC-003 / compliance "SECURITY.md MISSING": adds SECURITY.md with a responsible-disclosure contact, supported-version table, an explicit threat model pinned to the sandbox's enforcement layers, and an SLA for acknowledgement / triage / patch. - DOCS P3 "README references `youruser`": clone URL now points at alphacrack/python-maithili-dsl. Also adds a Security section linking to SECURITY.md, a Testing section documenting the pytest + coverage-gate workflow, and moves "Unit testing + CI/CD" out of Future Goals (it ships in this release). Replaces the future-goals list with still-open items (while / try-except / mypy). - DOCS P2 "CONTRIBUTING.md has duplicated README content": the entire README was pasted above the actual contributing section. Rewrites CONTRIBUTING.md from scratch with real dev setup (venv + editable install + requirements-dev), a test-tier table, the coverage gate, and a security-change policy requiring a new test + a "Security Considerations" PR section. - Adds CHANGELOG.md in Keep-a-Changelog format. The 0.3.0 section documents every Added / Fixed / Changed item on this branch, cross-referenced to the AUDIT_REPORT.md finding IDs. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 93 +++++++++++++++++++++++++++ CONTRIBUTING.md | 168 +++++++++++++++++------------------------------- README.md | 36 +++++++++-- SECURITY.md | 73 +++++++++++++++++++++ 4 files changed, 255 insertions(+), 115 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 SECURITY.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b3c4cc3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,93 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +_Nothing yet. Open a PR to add your change here._ + +## [0.3.0] — 2026-04-17 + +This is a hardening and correctness release. Every P0 finding in +`AUDIT_REPORT.md` is closed. + +### Added +- **Security sandbox.** `.dmai` files now execute inside a restricted + `__builtins__` dict (no `exec`, `eval`, `compile`, `open`, + `breakpoint`, direct `__import__`) and an import whitelist driven + from `MAITHILI_MODULES`. Raw Python `import` lines that weren't + translated from Maithili are rejected before `exec` by a static + validator; a runtime `__import__` wrapper enforces the same + whitelist as a second line of defense. Closes SEC-001. +- **Full pytest suite.** 142 tests across six files covering + functional (every keyword, every numeral), linter rules, security + (sandbox escapes, malicious imports), CLI exit codes, and + end-to-end smoke tests over every `.dmai` in `examples/`. Overall + coverage 95%; critical modules all ≥90%. Closes CQ-002. +- **GitHub Actions CI.** Matrix of Python 3.9/3.10/3.11/3.12 × Ubuntu + + macOS = 8 jobs. Runs tests + coverage + examples smoke loop on + every push and PR to `main` / `development`. Closes #6. +- **`python -m maithili_dsl` entry point.** New `__main__.py` allows + module-style invocation without needing the console script. +- **CLI `--version` / `-V` flag.** +- **Named exit codes.** `EXIT_OK` / `EXIT_USAGE` / `EXIT_NOT_FOUND` / + `EXIT_LINT_ERROR` / `EXIT_IMPORT_BLOCKED` / `EXIT_RUNTIME_ERROR` so + CI and callers can distinguish failure modes. Closes PKG-002. +- **`pyproject.toml`.** PEP 517/518 build metadata with pytest + + coverage config co-located. Closes PKG-001. +- **`SECURITY.md`** — responsible disclosure process + threat model. +- **`CHANGELOG.md`** — this file. +- **`.github/PULL_REQUEST_TEMPLATE.md`** — enforces test evidence and + audit-reference fields on future PRs. +- **`requirements-dev.txt`** — pinned `pytest`, `pytest-cov`, + `pytest-timeout`. +- **Public API exports.** `maithili_dsl/__init__.py` now exposes + `__version__`, `transpile_maithili_code`, `lint_maithili_code`, + `translate_exception_to_maithili`, `convert_devanagari_numerals`. + Closes PKG-003. + +### Fixed +- **Transpiler replaced keywords inside string literals.** `छपाउ("यह + में है")` used to become `print("यह in है")`. The transpiler now + tokenizes input and only replaces keywords in code regions, not in + quoted strings or comments. Closes CQ-001 (first half). +- **Transpiler replaced keyword substrings of longer identifiers.** + `नवयुग = ५` used to become `__init__युग = 5`. The transpiler now + uses Devanagari-aware negative lookbehind/lookahead so keyword + matches fire only at true word boundaries. Closes CQ-001 (second + half). +- **Linter flagged `==` comparisons as assignments.** Any conditional + using `==` / `!=` / `<=` / `>=` used to surface a spurious "invalid + variable name" error. The linter now distinguishes comparison + operators from assignments before validating the left-hand side. +- **Linter flagged `नव` constructors as unused functions.** `नव` + (mapped to `__init__`) is invoked implicitly via class + instantiation, so it's never called by name. The unused-function + post-scan now skips it. +- **Linter flagged attribute assignments.** Lines like + `स्वयं.नाम = नाम` were being checked as new-variable declarations. + Attribute assignments (anything with `.` in the LHS) are now + skipped. +- **LICENSE was unenforceable.** Contained `[Your Name]` and + `[MIT terms continued...]` placeholders. Replaced with the full + MIT license text and a real copyright holder. Closes LIC-001. + +### Changed +- **Python minimum bumped from 3.6 to 3.9.** 3.6–3.8 are EOL and the + new transpiler uses regex and f-string features that aren't worth + conditionally supporting. +- **`README.md`** — fixed `youruser` placeholder, added Security and + Testing sections, moved CI/CD out of "future goals" (it's shipped). +- **`CONTRIBUTING.md`** — removed the full README that was + accidentally pasted above the actual contributing guide; expanded + with real dev setup, test workflow, and security-change policy. + +### Removed +- Nothing. + +## [0.2.0] + +Initial versioned release. See git history before 2026-04-17. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce24e19..8a9c50a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,142 +1,90 @@ -# README.md +# 🤝 Contributing to the Devnagiri Maithili DSL -# 📜 Devnagiri Maithili DSL - -A modern Python-compatible programming DSL that lets you write code in **Maithili** using the **Devanagari script**. +Welcome! Your contributions help make the Maithili DSL stronger and more +accessible. This guide covers local setup, the test workflow, and how to +submit a change for review. --- -## 🔧 Features -- ✅ Full Maithili syntax mapped to Python -- ✅ Supports OOP (classes, methods, self) -- ✅ Devanagari numerals (`०-९`) -- ✅ Built-in module translation (e.g., `गणित` → `math`) -- ✅ Runtime error messages in Maithili -- ✅ Linter with style & syntax feedback in Maithili - ---- +## 🛠 Development setup -## 🚀 Getting Started +Clone and install the package in editable mode together with the +development dependencies: -### 📦 Installation -Clone the repo: ```bash -git clone https://github.com/youruser/maithili-dsl.git -cd maithili-dsl +git clone https://github.com/alphacrack/python-maithili-dsl.git +cd python-maithili-dsl +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -e . +pip install -r requirements-dev.txt ``` -Run a sample file: -```bash -python run_dmai.py examples/hello.dmai -``` - ---- +Run a sample file to verify the CLI works: -## 📄 Example: `examples/person.dmai` -```python -वर्ग व्यक्ति: - कार्य नव(स्वयं, नाम): - स्वयं.नाम = नाम - - कार्य बोलू(स्वयं): - छपाउ("हमर नाम " + स्वयं.नाम + " अछि।") - -व्यक्ति१ = व्यक्ति("सुमन") -व्यक्ति१.बोलू() -``` -Output: -``` -हमर नाम सुमन अछि। +```bash +python -m maithili_dsl examples/hello.dmai +# or, equivalently, via the console script: +python_maithili examples/hello.dmai ``` --- -## 🧠 Maithili Keywords -| Maithili | Python | -|----------------|--------------| -| `कार्य` | `def` | -| `वर्ग` | `class` | -| `फेर करू` | `return` | -| `स्वयं` | `self` | -| `यदि` | `if` | -| `नहि त` | `else` | -| `प्रत्येक` | `for` | -| `में` | `in` | -| `सत्य` | `True` | -| `मिथ्या` | `False` | -| `शून्य` | `None` | +## 🧪 Running tests ---- +**Always run the full suite before opening a PR.** It takes ~1 second. -## 📦 Built-in Modules Mapping -| Maithili | Python | -|---------------|---------------| -| `गणित` | `math` | -| `समय` | `time` | -| `यादृच्छिक` | `random` | -| `तिथि` | `datetime` | -| `पुन` | `re` | -| `संग्रह` | `collections` | -| `सिस्टम` | `sys` | -| `पथ` | `os.path` | -| `ओएस` | `os` | -| `आँकड़ा` | `statistics` | - ---- - -## 🧹 Linting & Errors -- ❗ Common mistakes caught before running -- ❗ Runtime errors shown in Maithili: ```bash -⚠️ त्रुटि: कोनो नाम घोषित नहि अछि। +pytest -v --cov=maithili_dsl --cov-report=term-missing ``` ---- +The test suite is organized by tier: -## 🤝 Contributing -See [`CONTRIBUTING.md`](CONTRIBUTING.md) to learn how you can help build and improve this DSL. +| File | What it covers | +|------|----------------| +| `tests/test_numeral.py` | Devanagari → ASCII numeral conversion | +| `tests/test_transpile.py` | Functional: every keyword + string-literal + word-boundary regressions | +| `tests/test_linter.py` | Every lint rule, exception translations | +| `tests/test_security.py` | Sandbox: import whitelist, safe builtins, malicious-file rejection | +| `tests/test_cli.py` | CLI entry point, every exit code, subprocess invocation | +| `tests/test_smoke.py` | End-to-end: every example in `examples/` runs via the CLI | ---- +The project enforces a coverage gate of **≥85% overall** and **≥90% on +the critical modules** (`cli.py`, `transpiler/transpile.py`, +`transpiler/linter.py`). If your change drops coverage below that, +please add tests. -## 📄 License -This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details. +### Security changes ---- +Any change touching `cli.py`, the transpiler's string handling, the +import whitelist (`MAITHILI_MODULES`), or the safe-builtins list +(`_SAFE_BUILTIN_NAMES`) must include: -## 🌐 Future Goals -- 🌍 Tirhuta script support -- 🌐 Web REPL for `.dmai` -- 🔌 VS Code extension -- 🧪 Unit testing + CI/CD +1. A new test in `tests/test_security.py` proving the intended behavior. +2. A short "Security Considerations" section in the PR description. --- -# CONTRIBUTING.md +## 🧩 How you can contribute -# 🤝 Contributing to Maithili DSL - -Welcome! Your contributions help make the Maithili DSL stronger and more accessible. - -## 🛠 Setup Instructions -1. Clone the repo: - ```bash - git clone https://github.com/youruser/maithili-dsl.git - cd maithili-dsl - ``` -2. Run a sample file: - ```bash - python run_dmai.py examples/hello.dmai - ``` - -## 🧩 How You Can Contribute -- 📚 Improve keyword mappings and add new features -- 🧪 Write tests and examples +- 📚 Improve keyword mappings and add new features (see `BACKLOG.md` P3) +- 🧪 Expand test coverage, especially on edge cases +- 🐛 Fix a backlog item (see `BACKLOG.md`) - 🌐 Help add Tirhuta script support - ✍️ Translate documentation into Maithili +- 🔐 Report a security vulnerability via the process in `SECURITY.md` + +--- + +## 📬 Submitting changes -## 📬 Submitting Changes -- Fork the repository -- Make your changes on a branch -- Open a Pull Request (PR) with a clear title & description +1. Fork the repository and create a topic branch off `development` + (not `main` — `main` is the released line). +2. Make your changes in logical commits — one concern per commit. + Follow conventional-commit style where reasonable + (`fix(linter): ...`, `feat(cli): ...`). +3. Run `pytest` and make sure it's green. +4. Open a Pull Request targeting `development`. The PR template will + prompt you for test evidence and any security considerations. -Thanks for your interest! ❤️ \ No newline at end of file +Thanks for your interest! ❤️ diff --git a/README.md b/README.md index 7b9a3a8..f6e1b84 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ A modern Python-compatible programming DSL that lets you write code in **Maithil ### 📦 Installation Clone the repo: ```bash -git clone https://github.com/youruser/maithili-dsl.git -cd maithili-dsl +git clone https://github.com/alphacrack/python-maithili-dsl.git +cd python-maithili-dsl ``` ### 🔧 Install CLI Tool (cross-platform) @@ -31,11 +31,13 @@ cd maithili-dsl pip install . ``` -Now you can run Maithili scripts using: +Now you can run Maithili scripts using either entry point: ```bash python_maithili examples/hello.dmai +# or, without installing the console script: +python -m maithili_dsl examples/hello.dmai ``` -✅ Works on Mac, Windows, and Linux! +✅ Works on Mac, Windows, and Linux, on Python 3.9 – 3.12. --- @@ -100,6 +102,29 @@ Output: --- +## 🔐 Security +`.dmai` scripts are executed in a sandbox: a restricted `__builtins__` +mapping (no `exec` / `eval` / `open` / `compile` / `breakpoint`) and a +module import whitelist mapped from `MAITHILI_MODULES`. Raw Python +`import` statements that weren't translated from Maithili names are +rejected before execution. + +To report a vulnerability, see [SECURITY.md](SECURITY.md). + +--- + +## 🧪 Testing +Contributions are expected to come with tests. Run the full suite: +```bash +pip install -r requirements-dev.txt +pytest -v --cov=maithili_dsl --cov-report=term-missing +``` +Coverage gates: ≥85% overall, ≥90% on `cli.py`, `transpile.py`, and +`linter.py`. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full +workflow. + +--- + ## 🤝 Contributing See [`CONTRIBUTING.md`](CONTRIBUTING.md) to learn how you can help build and improve this DSL. @@ -114,6 +139,7 @@ This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) f - 🌍 Tirhuta script support - 🌐 Web REPL for `.dmai` - 🔌 VS Code extension -- 🧪 Unit testing + CI/CD +- `while` (`जबतक`) and `try`/`except` (`प्रयास`/`अपवाद`) keywords +- Type hints and `mypy` in CI --- diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..b194610 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,73 @@ +# Security Policy + +## Supported versions + +Only the latest minor release receives security patches. Older versions +are best-effort. + +| Version | Supported | +|---------|---------------------| +| 0.3.x | ✅ current | +| 0.2.x | ⚠️ critical fixes only | +| < 0.2 | ❌ not supported | + +## Threat model + +`python_maithili` executes user-supplied `.dmai` files. The sandbox in +`maithili_dsl/cli.py` is the project's primary trust boundary. It +enforces: + +1. **Import whitelist.** Only modules present in `MAITHILI_MODULES` (the + Maithili → Python module map) are importable from a `.dmai` file. + Raw Python `import` statements that were not translated from Maithili + names are rejected by `_validate_imports` before `exec` runs. The + `__import__` builtin inside the sandbox is replaced with a wrapper + that re-checks the whitelist at runtime. +2. **Safe builtins.** The execution namespace's `__builtins__` is a + curated dict (`_SAFE_BUILTIN_NAMES`). Notably absent: `exec`, `eval`, + `compile`, `open`, `breakpoint`. Any `.dmai` script that calls these + gets a `NameError` translated to Maithili. +3. **No shared globals.** `exec` runs with a fresh globals dict — it + cannot read or modify the CLI's own process state. + +If you find a way to bypass any of these, please report it. + +## Reporting a vulnerability + +Please do **not** open a public issue for security problems. Instead: + +- Email: **jha.bishwas@gmail.com** with the subject line + `[SECURITY] python_maithili` and a proof-of-concept. +- Or use GitHub's private vulnerability reporting at + https://github.com/alphacrack/python-maithili-dsl/security/advisories/new + +Please include: + +- A `.dmai` file that demonstrates the issue. +- The Python version and OS. +- What the sandbox *should* have prevented, and what actually happened. + +## Response SLA + +- **Acknowledgement**: within 3 business days. +- **Triage + severity classification**: within 7 business days. +- **Patch + coordinated disclosure**: depends on severity; typically + within 30 days for HIGH/CRITICAL findings. + +## Scope + +In scope: + +- Sandbox escape from within a `.dmai` file. +- Ability to read or modify files outside the CWD without being invoked + by an explicitly-whitelisted module call. +- Denial of service via a small `.dmai` input (e.g., catastrophic regex + backtracking in the transpiler). + +Out of scope: + +- Issues that require first compromising the machine running + `python_maithili` (e.g., modifying the installed package). +- Social engineering. +- Resource-exhaustion attacks that require arbitrarily large input + (basic DoS via huge files).