diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..e18e147
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,8 @@
+# Default owner for everything in the repo.
+* @alphacrack
+
+# Security-sensitive paths: sandbox, transpiler string handling, CI/publish.
+/maithili_dsl/cli.py @alphacrack
+/maithili_dsl/transpiler/ @alphacrack
+/.github/workflows/ @alphacrack
+/SECURITY.md @alphacrack
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..30474fc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,69 @@
+name: ЁЯРЫ Bug report
+description: Something in the transpiler, linter, sandbox, or CLI doesn't work as documented.
+title: "[BUG] "
+labels: ["bug", "needs-triage"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to report a bug! рдзрдиреНрдпрд╡рд╛рдж ЁЯЩП
+
+ тЪая╕П **Security issues** (sandbox escapes, exec bypasses) must NOT be reported here.
+ Use [private vulnerability reporting](https://github.com/alphacrack/python-maithili-dsl/security/advisories/new)
+ or the process in [SECURITY.md](https://github.com/alphacrack/python-maithili-dsl/blob/main/SECURITY.md).
+ - type: textarea
+ id: description
+ attributes:
+ label: What happened?
+ description: A clear description of the bug.
+ validations:
+ required: true
+ - type: textarea
+ id: repro
+ attributes:
+ label: Minimal .dmai reproduction
+ description: The smallest Maithili snippet that triggers the bug.
+ placeholder: |
+ рдЫрдкрд╛рдЙ("рдпрд╣ рдореЗрдВ рд╣реИ")
+ render: text
+ validations:
+ required: true
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected behavior
+ description: What you expected to happen (e.g. the transpiled Python, or output).
+ validations:
+ required: true
+ - type: textarea
+ id: actual
+ attributes:
+ label: Actual behavior
+ description: What actually happened. Paste the full output / traceback.
+ render: shell
+ validations:
+ required: true
+ - type: input
+ id: version
+ attributes:
+ label: python_maithili version
+ description: Output of `python_maithili --version`
+ placeholder: "0.3.0"
+ validations:
+ required: true
+ - type: input
+ id: environment
+ attributes:
+ label: Python version and OS
+ placeholder: "Python 3.12, macOS 15"
+ validations:
+ required: true
+ - type: checkboxes
+ id: checks
+ attributes:
+ label: Checks
+ options:
+ - label: I searched existing issues and this is not a duplicate.
+ required: true
+ - label: This is not a security vulnerability (those go through SECURITY.md).
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..408d7d2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: true
+contact_links:
+ - name: ЁЯФР Report a security vulnerability
+ url: https://github.com/alphacrack/python-maithili-dsl/security/advisories/new
+ about: Sandbox escapes and exec bypasses must be reported privately тАФ never as a public issue. See SECURITY.md.
+ - name: ЁЯТм Questions & general discussion
+ url: https://github.com/alphacrack/python-maithili-dsl/blob/main/README.md
+ about: Check the README and docs first; for usage questions, open a blank issue with the "question" label.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..32d94be
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,36 @@
+name: тЬи Feature request
+description: Propose a new capability for the DSL, CLI, or tooling.
+title: "[FEATURE] "
+labels: ["enhancement", "needs-triage"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ For a **new Maithili keyword or module mapping**, please use the
+ dedicated "ЁЯФд Keyword / mapping proposal" template instead.
+ - type: textarea
+ id: problem
+ attributes:
+ label: Problem
+ description: What problem does this solve? Who benefits?
+ validations:
+ required: true
+ - type: textarea
+ id: proposal
+ attributes:
+ label: Proposed solution
+ description: What you'd like to see, ideally with an example `.dmai` snippet.
+ validations:
+ required: true
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Alternatives considered
+ description: Other approaches you thought about, and why they're worse.
+ - type: checkboxes
+ id: checks
+ attributes:
+ label: Checks
+ options:
+ - label: I searched existing issues and BACKLOG.md and this is not already tracked.
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/keyword_proposal.yml b/.github/ISSUE_TEMPLATE/keyword_proposal.yml
new file mode 100644
index 0000000..87cc6bb
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/keyword_proposal.yml
@@ -0,0 +1,57 @@
+name: ЁЯФд Keyword / mapping proposal
+description: Propose a new Maithili keyword, builtin, or module-name mapping.
+title: "[KEYWORD] "
+labels: ["enhancement", "keyword-mapping", "needs-triage"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Keyword choices shape the language for every Maithili speaker, so
+ native-speaker input on word choice is especially valuable here.
+ - type: input
+ id: maithili
+ attributes:
+ label: Proposed Maithili word (Devanagari)
+ placeholder: "рдЬрдмрддрдХ"
+ validations:
+ required: true
+ - type: input
+ id: python
+ attributes:
+ label: Python equivalent
+ placeholder: "while"
+ validations:
+ required: true
+ - type: textarea
+ id: rationale
+ attributes:
+ label: Why this word?
+ description: |
+ Is this the natural Maithili term? Are there regional variants or
+ alternative spellings a reader might expect? Could it collide with
+ an existing keyword or with common identifier names?
+ validations:
+ required: true
+ - type: textarea
+ id: example
+ attributes:
+ label: Example usage
+ description: A short `.dmai` snippet showing the keyword in use, with the expected transpiled Python.
+ render: text
+ placeholder: |
+ рдЧрд┐рдирддреА = реж
+ рдЬрдмрддрдХ рдЧрд┐рдирддреА < рел:
+ рдЫрдкрд╛рдЙ(рдЧрд┐рдирддреА)
+ рдЧрд┐рдирддреА = рдЧрд┐рдирддреА + рез
+ validations:
+ required: true
+ - type: dropdown
+ id: speaker
+ attributes:
+ label: Are you a Maithili speaker?
+ options:
+ - "Yes тАФ native"
+ - "Yes тАФ non-native / learner"
+ - "No тАФ proposing on technical grounds only"
+ validations:
+ required: true
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..b3d7aae
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,18 @@
+version: 2
+updates:
+ # Keep GitHub Actions pinned versions current (checkout, setup-python, etc.)
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ target-branch: "development"
+ labels: ["infra", "needs-triage"]
+
+ # Dev dependencies (pytest, coverage tooling). The package itself has
+ # zero runtime deps, so this mostly tracks requirements-dev.txt.
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ target-branch: "development"
+ labels: ["infra", "needs-triage"]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 41e9f9c..d0c97ec 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,14 +20,14 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
- python-version: ["3.9", "3.10", "3.11", "3.12"]
+ python-version: ["3.10", "3.11", "3.12"]
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: pip
@@ -70,8 +70,24 @@ jobs:
- name: Upload coverage artifact
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: coverage-xml
path: coverage.xml
if-no-files-found: warn
+
+ # Aggregate gate: branch protection requires this single check instead of
+ # every matrix leg, so the matrix can change without touching repo settings.
+ ci-ok:
+ name: ci-ok
+ if: always()
+ needs: [test]
+ runs-on: ubuntu-latest
+ steps:
+ - name: Fail if any test job failed
+ run: |
+ if [ "${{ needs.test.result }}" != "success" ]; then
+ echo "test matrix result: ${{ needs.test.result }}"
+ exit 1
+ fi
+ echo "all test jobs green"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..fb6f9d7
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,192 @@
+name: Publish to PyPI
+
+# Trusted Publishing (OIDC) тАФ no long-lived API tokens stored.
+#
+# Triggers:
+# - release.published on a non-prerelease GitHub Release
+# тЖТ publishes to PyPI (environment: pypi)
+# - release.published on a pre-release GitHub Release
+# тЖТ publishes to TestPyPI (environment: testpypi)
+# - workflow_dispatch with target=testpypi
+# тЖТ publishes to TestPyPI (staging verification)
+# - workflow_dispatch with target=build-only
+# тЖТ builds + twine-checks, no network publish (dry run)
+#
+# One-time setup required before the publish jobs succeed:
+# See docs/RELEASE.md for Trusted Publisher configuration on
+# pypi.org and test.pypi.org.
+
+on:
+ release:
+ types: [published]
+ workflow_dispatch:
+ inputs:
+ target:
+ description: "Publish target"
+ required: true
+ default: "build-only"
+ type: choice
+ options:
+ - build-only
+ - testpypi
+
+concurrency:
+ group: publish-${{ github.event_name }}-${{ github.ref }}
+ cancel-in-progress: false
+
+permissions:
+ contents: read
+
+jobs:
+ # ------------------------------------------------------------------
+ # Build sdist + wheel, run twine check, run the full test suite
+ # against the built wheel so we never ship a wheel that fails tests.
+ # ------------------------------------------------------------------
+ build:
+ name: Build distributions
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ outputs:
+ version: ${{ steps.version.outputs.value }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Set up Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.12"
+ cache: pip
+ cache-dependency-path: requirements-dev.txt
+
+ - name: Install build tooling
+ run: |
+ python -m pip install --upgrade pip
+ pip install build twine
+
+ - name: Extract version from package
+ id: version
+ run: |
+ v=$(python -c "import re, pathlib; print(re.search(r'^__version__\s*=\s*[\"\']([^\"\']+)', pathlib.Path('maithili_dsl/__init__.py').read_text(), re.M).group(1))")
+ echo "value=$v" >> "$GITHUB_OUTPUT"
+ echo "Package version: $v"
+
+ - name: Build sdist + wheel
+ run: python -m build
+
+ - name: twine check
+ run: twine check --strict dist/*
+
+ - name: Install built wheel in clean venv
+ run: |
+ python -m venv /tmp/smoketest
+ /tmp/smoketest/bin/pip install dist/*.whl
+ /tmp/smoketest/bin/python -m maithili_dsl --version
+ /tmp/smoketest/bin/python_maithili examples/hello.dmai
+
+ - name: Run tests against the installed wheel
+ run: |
+ /tmp/smoketest/bin/pip install -r requirements-dev.txt
+ /tmp/smoketest/bin/pytest -q --no-cov
+
+ - name: Upload distributions artifact
+ uses: actions/upload-artifact@v7
+ with:
+ name: dist
+ path: dist/
+ if-no-files-found: error
+ retention-days: 7
+
+ # ------------------------------------------------------------------
+ # TestPyPI тАФ fires on workflow_dispatch (target=testpypi) or on
+ # a pre-release GitHub Release. Safe to re-run with a different
+ # version number.
+ # ------------------------------------------------------------------
+ publish-testpypi:
+ name: Publish to TestPyPI
+ needs: [build]
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ if: |
+ (github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi') ||
+ (github.event_name == 'release' && github.event.release.prerelease == true)
+ environment:
+ name: testpypi
+ url: https://test.pypi.org/p/python-maithili
+ permissions:
+ id-token: write # OIDC token for Trusted Publishing
+ steps:
+ - name: Download distributions
+ uses: actions/download-artifact@v8
+ with:
+ name: dist
+ path: dist/
+
+ - name: Publish to TestPyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ repository-url: https://test.pypi.org/legacy/
+ skip-existing: false
+ verbose: true
+
+ - name: Summary
+ run: |
+ echo "### Published to TestPyPI :rocket:" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "Version: **${{ needs.build.outputs.version }}**" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "\`\`\`bash" >> "$GITHUB_STEP_SUMMARY"
+ echo "pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ python-maithili==${{ needs.build.outputs.version }}" >> "$GITHUB_STEP_SUMMARY"
+ echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY"
+
+ # ------------------------------------------------------------------
+ # Production PyPI тАФ fires only on a non-prerelease GitHub Release.
+ # This is the single authoritative way to publish a real version.
+ # ------------------------------------------------------------------
+ publish-pypi:
+ name: Publish to PyPI
+ needs: [build]
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ if: |
+ github.event_name == 'release' && github.event.release.prerelease == false
+ environment:
+ name: pypi
+ url: https://pypi.org/p/python-maithili
+ permissions:
+ id-token: write # OIDC token for Trusted Publishing
+ steps:
+ - name: Download distributions
+ uses: actions/download-artifact@v8
+ with:
+ name: dist
+ path: dist/
+
+ - name: Verify release tag matches package version
+ env:
+ TAG: ${{ github.event.release.tag_name }}
+ VERSION: ${{ needs.build.outputs.version }}
+ run: |
+ expected="v${VERSION}"
+ if [ "$TAG" != "$expected" ]; then
+ echo "::error::Release tag '$TAG' does not match package version '$expected'. Refusing to publish."
+ exit 1
+ fi
+ echo "Tag $TAG matches package version $VERSION тАФ proceeding."
+
+ - name: Publish to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ skip-existing: false
+ verbose: true
+
+ - name: Summary
+ run: |
+ echo "### Published to PyPI :tada:" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "Version: **${{ needs.build.outputs.version }}**" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "\`\`\`bash" >> "$GITHUB_STEP_SUMMARY"
+ echo "pip install python-maithili==${{ needs.build.outputs.version }}" >> "$GITHUB_STEP_SUMMARY"
+ echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY"
diff --git a/BACKLOG.md b/BACKLOG.md
index 19de5e7..3b1e420 100644
--- a/BACKLOG.md
+++ b/BACKLOG.md
@@ -1,7 +1,9 @@
# Maithili DSL тАФ Project Backlog
-> Generated: 2026-04-11 | Status: Active
-> Tracking: GitHub Issues (use `scripts/create_issues.sh` to push)
+> Generated: 2026-04-11 | Status: **Historical snapshot**
+> Live tracking has moved to [GitHub Issues](https://github.com/alphacrack/python-maithili-dsl/issues).
+> All P0 and P1 items below (plus items 11, 13тАУ15, 19) have been resolved as of v0.3.0;
+> the remaining open items are tracked as individual issues.
---
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b3c4cc3..f310d88 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
-_Nothing yet. Open a PR to add your change here._
+## [0.4.0] тАФ 2026-07-23
+
+First release published to PyPI since 0.2.0 (0.3.0 was an internal
+release that was never pushed to the index). This release bundles the
+0.3.0 hardening work plus the fixes and tooling below.
+
+### Added
+- **PyPI publish pipeline** (`.github/workflows/publish.yml`) using
+ Trusted Publishing (OIDC). Triggers: GitHub Release for production
+ PyPI, pre-release or `workflow_dispatch` for TestPyPI, `build-only`
+ dispatch for a credential-free dry run. The workflow builds sdist +
+ wheel, runs `twine check --strict`, installs the wheel in a clean
+ venv, runs the full pytest suite against the installed wheel, and
+ verifies the release tag matches `__version__` before publishing.
+- **`docs/RELEASE.md`** тАФ step-by-step release process including
+ one-time Trusted Publisher setup on PyPI + TestPyPI, routine
+ release flow, version-bump convention, and recovery procedure for
+ bad releases.
+- **`build` and `twine`** added to `requirements-dev.txt`.
+- **`examples/README.md`** documenting every bundled example program,
+ the keywords each demonstrates, and expected output. Closes #41.
+- **Enforced coverage gate.** `fail_under = 85` in `pyproject.toml`'s
+ `[tool.coverage.report]` so a coverage regression fails CI. Closes #42.
+
+### Fixed
+- **Augmented assignments flagged as invalid variable names.** `+=`,
+ `-=`, `*=`, `/=`, `//=`, `%=`, `**=` no longer produce a spurious
+ "invalid variable name" linter error. Closes #29.
+- **Unused-function checks accepted identifier substrings as calls.**
+ Function names now require a real Devanagari-aware call site outside
+ strings and comments. Closes #31.
+ - Known trade-off: because a call now requires `name(`, passing a
+ function as a value (`x = рдЬреЛрдбрд╝`, `map(рдЬреЛрдбрд╝, тАж)`) counts as *unused*
+ and warns. This is a deliberate choice favoring clarity for
+ learner-oriented code over higher-order usage.
+
+### Removed
+- **Dropped support for Python 3.9** (end-of-life since October 2025).
+ Minimum supported version is now Python 3.10. This unblocks dev-tool
+ upgrades (pytest 9, build 1.5+) that no longer support 3.9. Closes #44.
## [0.3.0] тАФ 2026-04-17
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..25ef0ac
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,132 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+ community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or advances
+ of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email address,
+ without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+**jha.bishwas@gmail.com**.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8a9c50a..612a80a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,6 +4,9 @@ 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.
+This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md).
+By participating, you are expected to uphold it.
+
---
## ЁЯЫа Development setup
@@ -74,6 +77,27 @@ import whitelist (`MAITHILI_MODULES`), or the safe-builtins list
- тЬНя╕П Translate documentation into Maithili
- ЁЯФР Report a security vulnerability via the process in `SECURITY.md`
+### Finding something to work on
+
+Issues labeled [`good first issue`](https://github.com/alphacrack/python-maithili-dsl/labels/good%20first%20issue)
+are scoped for newcomers; [`help wanted`](https://github.com/alphacrack/python-maithili-dsl/labels/help%20wanted)
+issues are open to anyone. Labels follow a simple scheme:
+
+| Label group | Meaning |
+|-------------|---------|
+| `P0`тАУ`P3` | Priority, from critical to nice-to-have |
+| `bug`, `enhancement`, `documentation`, `question` | Issue type |
+| `security`, `testing`, `infra`, `packaging`, `quality`, `compliance` | Category |
+| `area:transpiler`, `area:linter`, `area:cli` | Part of the codebase affected |
+| `keyword-mapping` | Proposals for new Maithili keywords or module mappings |
+| `translation` | Translating docs or messages into Maithili |
+| `needs-triage` | New issue awaiting a maintainer's look |
+
+Comment on an issue before starting significant work so it can be assigned
+to you and effort isn't duplicated. New keyword ideas should go through the
+**ЁЯФд Keyword / mapping proposal** issue template тАФ native-speaker input on
+word choice is especially welcome.
+
---
## ЁЯУм Submitting changes
@@ -87,4 +111,12 @@ import whitelist (`MAITHILI_MODULES`), or the safe-builtins list
4. Open a Pull Request targeting `development`. The PR template will
prompt you for test evidence and any security considerations.
+## ЁЯУж Releasing
+
+Releases are cut by a project maintainer following the checklist in
+[`docs/RELEASE.md`](docs/RELEASE.md). The short version:
+publishing is triggered by creating a **GitHub Release** and uses
+**Trusted Publishing (OIDC)** тАФ no long-lived PyPI tokens are stored
+in the repo.
+
Thanks for your interest! тЭдя╕П
diff --git a/README.md b/README.md
index f6e1b84..6ac346d 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,9 @@ python_maithili examples/hello.dmai
# or, without installing the console script:
python -m maithili_dsl examples/hello.dmai
```
-тЬЕ Works on Mac, Windows, and Linux, on Python 3.9 тАУ 3.12.
+тЬЕ Works on Mac, Windows, and Linux, on Python 3.10 тАУ 3.12.
+
+See [`examples/README.md`](examples/README.md) for a description of every bundled example program.
---
@@ -127,6 +129,9 @@ workflow.
## ЁЯдЭ Contributing
See [`CONTRIBUTING.md`](CONTRIBUTING.md) to learn how you can help build and improve this DSL.
+Good entry points are issues labeled
+[`good first issue`](https://github.com/alphacrack/python-maithili-dsl/labels/good%20first%20issue).
+This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md).
---
diff --git a/SUPPORT.md b/SUPPORT.md
new file mode 100644
index 0000000..a790e6d
--- /dev/null
+++ b/SUPPORT.md
@@ -0,0 +1,27 @@
+# Getting Help
+
+## ЁЯУЪ Documentation
+
+- [README](README.md) тАФ installation, keyword table, module mappings, examples
+- [CONTRIBUTING](CONTRIBUTING.md) тАФ development setup and PR workflow
+- [`examples/`](examples/) тАФ runnable `.dmai` programs
+
+## ЁЯРЫ Found a bug?
+
+Open a [bug report](https://github.com/alphacrack/python-maithili-dsl/issues/new?template=bug_report.yml)
+with a minimal `.dmai` reproduction.
+
+## тЬи Want a feature or a new keyword?
+
+- New Maithili keyword or module mapping тЖТ [keyword proposal](https://github.com/alphacrack/python-maithili-dsl/issues/new?template=keyword_proposal.yml)
+- Anything else тЖТ [feature request](https://github.com/alphacrack/python-maithili-dsl/issues/new?template=feature_request.yml)
+
+## тЭУ Usage questions
+
+Open a blank issue and add the `question` label. Please include your
+`python_maithili --version` output and a code snippet.
+
+## ЁЯФР Security issues
+
+**Never open a public issue.** Follow [SECURITY.md](SECURITY.md) тАФ
+email or use [private vulnerability reporting](https://github.com/alphacrack/python-maithili-dsl/security/advisories/new).
diff --git a/docs/RELEASE.md b/docs/RELEASE.md
new file mode 100644
index 0000000..3c15485
--- /dev/null
+++ b/docs/RELEASE.md
@@ -0,0 +1,163 @@
+# Release Process
+
+This document describes how to cut a new release of `python_maithili`
+to PyPI. The workflow is **`.github/workflows/publish.yml`** and uses
+**Trusted Publishing (OIDC)** тАФ no API tokens are stored in GitHub.
+
+---
+
+## One-time setup (required before the first publish succeeds)
+
+### 1. Create GitHub Environments
+
+In the repo, go to **Settings тЖТ Environments** and create two
+environments тАФ they give us a place to pin the OIDC trust and (for
+production) an optional manual-approval gate.
+
+#### `testpypi`
+- No required reviewers.
+- Deployment branches: all branches (we publish to TestPyPI from
+ feature branches too).
+
+#### `pypi`
+- **Required reviewers**: add yourself. Every production publish will
+ wait for explicit approval. This is the final safety gate.
+- Deployment branches: protected tags matching `v*` only
+ (`v[0-9]+.[0-9]+.[0-9]+` and `v[0-9]+.[0-9]+.[0-9]+-*`).
+
+### 2. Register the Trusted Publisher on PyPI
+
+Go to
+(owner only) and add a new "Trusted publisher":
+
+| Field | Value |
+|---------------------|--------------------------------|
+| Owner | `alphacrack` |
+| Repository | `python-maithili-dsl` |
+| Workflow filename | `publish.yml` |
+| Environment name | `pypi` |
+
+### 3. Register the Trusted Publisher on TestPyPI
+
+TestPyPI doesn't have the project yet, so use a **pending publisher**.
+Go to and under
+"Pending trusted publishers" add:
+
+| Field | Value |
+|---------------------|--------------------------------|
+| PyPI Project Name | `python-maithili` |
+| Owner | `alphacrack` |
+| Repository | `python-maithili-dsl` |
+| Workflow filename | `publish.yml` |
+| Environment name | `testpypi` |
+
+The pending publisher converts to a real one the first time a workflow
+run successfully creates the project on TestPyPI.
+
+---
+
+## Routine release workflow
+
+### A. Dry-run build (no publish)
+
+Push a PR or run **Actions тЖТ Publish to PyPI тЖТ Run workflow тЖТ `build-only`**.
+This executes the full pipeline except the publish step:
+
+- Build sdist + wheel.
+- `twine check --strict` metadata validation.
+- Install the wheel in a clean venv.
+- Run the full pytest suite against the installed wheel.
+- Upload the `dist/` artifact for inspection.
+
+No credentials are required and nothing touches the network package
+indexes. Use this whenever you want to confirm the package will build.
+
+### B. Publish to TestPyPI (staging verification)
+
+Two ways to trigger:
+
+1. **Manual**: Actions тЖТ Publish to PyPI тЖТ Run workflow тЖТ `testpypi`.
+2. **Automatic**: create a GitHub **pre-release** (check "This is a
+ pre-release" when making the Release). Tag format: `v0.X.Y-rc1`,
+ `v0.X.Y-dev1`, etc.
+
+Version numbers you publish to TestPyPI are **separate** from
+production PyPI тАФ TestPyPI has its own index. Verify the install:
+
+```bash
+pip install \
+ --index-url https://test.pypi.org/simple/ \
+ --extra-index-url https://pypi.org/simple/ \
+ python-maithili==0.X.Y
+python_maithili --version
+```
+
+Any version on TestPyPI cannot be re-used. If you need to iterate,
+bump the version (`0.3.0.dev0`, `0.3.0.dev1`, ...) in
+`maithili_dsl/__init__.py` before re-triggering.
+
+### C. Publish to production PyPI
+
+**Pre-flight checklist:**
+
+- [ ] `CHANGELOG.md` has a section for the new version with date filled in.
+- [ ] `maithili_dsl/__init__.py` has `__version__` set to the new version.
+- [ ] `pyproject.toml` has matching `version = "..."`.
+- [ ] `pytest` runs green on `main` CI.
+- [ ] The same version was successfully published to TestPyPI and installed
+ cleanly (step B).
+
+**Cut the release:**
+
+1. Ensure `main` has the final release commit on it (all three version
+ strings updated, CHANGELOG finalized).
+2. Create a new **GitHub Release** (not a pre-release):
+ - Tag: `v0.X.Y` (must match `__version__` exactly; the workflow
+ enforces this and refuses to publish on mismatch).
+ - Target: `main`.
+ - Title: `v0.X.Y`.
+ - Body: the corresponding CHANGELOG section.
+ - Leave "This is a pre-release" **unchecked**.
+3. Click **Publish release**. The workflow fires.
+4. If the `pypi` environment has required reviewers, approve the
+ deployment in the Actions run.
+5. After the run finishes, verify:
+
+ ```bash
+ pip install python-maithili==0.X.Y
+ python_maithili --version
+ ```
+
+---
+
+## Version bump convention
+
+This project uses [Semantic Versioning](https://semver.org/):
+
+| Change | Bump |
+|------------------------------------------------|-------|
+| Breaking API change (exit code semantics, etc) | MAJOR |
+| New keyword / module / feature | MINOR |
+| Bugfix, doc change, internal refactor | PATCH |
+
+Version is tracked in three places that must stay in sync:
+
+1. `maithili_dsl/__init__.py` тАФ `__version__` (single source of truth).
+2. `pyproject.toml` тАФ `version`.
+3. `CHANGELOG.md` тАФ section header and date.
+
+The publish workflow's "Verify release tag matches package version"
+step will fail the production publish if these drift.
+
+---
+
+## Recovery: what if a bad version reaches PyPI?
+
+PyPI does not allow re-uploading the same filename. If you published
+a broken release:
+
+1. **Yank** (do not delete) the broken version on PyPI:
+ тЖТ
+ pick the version тЖТ "Yank release". This hides it from `pip install`
+ without breaking already-pinned consumers.
+2. Bump PATCH and release the fix via the normal flow above.
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..880248c
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,85 @@
+# Examples
+
+This directory contains runnable Maithili DSL (`.dmai`) programs that demonstrate
+the language. Run any of them with either entry point:
+
+```bash
+python_maithili examples/hello.dmai
+# or, without installing the console script:
+python -m maithili_dsl examples/hello.dmai
+```
+
+> **Note:** `error.dmai` is intentionally broken тАФ it is used by CI to verify
+> that the linter/transpiler correctly rejects invalid programs. It is expected
+> to exit with a non-zero status and should not be run as a normal example.
+
+## `hello.dmai`
+
+Prints a greeting in Maithili.
+
+- **Keywords/modules shown:** `рдХрд╛рд░реНрдп` (function definition), `рдЫрдкрд╛рдЙ` (print),
+ top-level call.
+- **Expected output:**
+
+ ```
+ рд╣рдо рдореИрдерд┐рд▓реА рдореЗрдВ рдХреЛрдб рдХрд╜ рд░рд╣рд▓ рдЫреАред
+ ```
+
+## `person.dmai`
+
+Defines a `рд╡реНрдпрдХреНрддрд┐` (person) class, instantiates it, and calls a method.
+
+- **Keywords/modules shown:** `рд╡рд░реНрдЧ` (class), `рдирд╡` (constructor), `рд╕реНрд╡рдпрдВ` (self),
+ method definition and invocation.
+- **Expected output:**
+
+ ```
+ рд╣рдорд░ рдирд╛рдо рд╕реБрдорди рдЕрдЫрд┐ред
+ ```
+
+## `calculator.dmai`
+
+Implements basic arithmetic (`рдЬреЛрдбрд╝`/`рдШрдЯрд╛рдЙ`/`рдЧреБрдгрд╛`/`рднрд╛рдЧ`) and prints a
+multiplication table.
+
+- **Keywords/modules shown:** functions returning values, `str()`, `range()`,
+ string concatenation, loops (`рдкреНрд░рддреНрдпреЗрдХ`).
+- **Expected output (abridged):**
+
+ ```
+ === рдореИрдерд┐рд▓реА рдХреИрд▓рдХреБрд▓реЗрдЯрд░ ===
+ рдЬреЛрдбрд╝ рдХрд╛ рдкрд░рд┐рдгрд╛рдо: 15
+ рдШрдЯрд╛рдЙ рдХрд╛ рдкрд░рд┐рдгрд╛рдо: 5
+ рдЧреБрдгрд╛ рдХрд╛ рдкрд░рд┐рдгрд╛рдо: 50
+ рднрд╛рдЧ рдХрд╛ рдкрд░рд┐рдгрд╛рдо: 2.0
+ ```
+
+## `shopping_list.dmai`
+
+Builds a list, appends/removes items, iterates, and computes a sum and average.
+
+- **Keywords/modules shown:** lists (`[]`), `append`/`remove`, `len()`,
+ `рдпрджрд┐` (if), `рдкреНрд░рддреНрдпреЗрдХ` (for), arithmetic and averages.
+- **Expected output (abridged):**
+
+ ```
+ === рдЦрд░реАрджрд╛рд░реА рд╕реВрдЪреА ===
+ - рджреВрдз
+ - рд░реЛрдЯреА
+ - рдЕрдВрдбрд╛
+ - рдЪрд╛рд╡рд▓
+ рдХреБрд▓ рд╡рд╕реНрддреБ: 4
+ рджреВрдз рд╕реВрдЪреА рдореЗрдВ рдЕрдЫрд┐!
+ рдЕрдВрдбрд╛ рд╣рдЯрд╛рдПрд▓ рдЧреЗрд▓ред рдирдИ рд╕реВрдЪреА:
+ - рджреВрдз
+ - рд░реЛрдЯреА
+ - рдЪрд╛рд╡рд▓
+ ```
+
+## `error.dmai`
+
+Intentionally invalid program (uses non-Maithili tokens and broken syntax) used
+to confirm the linter rejects bad input.
+
+- **Keywords/modules shown:** n/a (negative test case).
+- **Expected output:** none тАФ the run exits with a non-zero status.
diff --git a/maithili_dsl/__init__.py b/maithili_dsl/__init__.py
index a564c5e..64837b5 100644
--- a/maithili_dsl/__init__.py
+++ b/maithili_dsl/__init__.py
@@ -3,7 +3,7 @@
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"
+__version__ = "0.4.0"
__all__ = [
"__version__",
diff --git a/maithili_dsl/transpiler/linter.py b/maithili_dsl/transpiler/linter.py
index 0459e93..75510cc 100644
--- a/maithili_dsl/transpiler/linter.py
+++ b/maithili_dsl/transpiler/linter.py
@@ -2,6 +2,8 @@
import re
+from .transpile import _make_keyword_pattern, _tokenize_preserving_strings
+
LINT_CONFIG = {
"enforce_snake_case": False,
"max_line_length": 80,
@@ -66,6 +68,9 @@ def lint_maithili_code(code, config=LINT_CONFIG):
pass # == comparison
else:
left_side = stripped.split("=")[0].strip()
+ left_side = re.sub(
+ r"(?:\*\*|//|[+\-*/%])$", "", left_side
+ ).rstrip()
# 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):
@@ -100,7 +105,14 @@ def lint_maithili_code(code, config=LINT_CONFIG):
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("рдХрд╛рд░реНрдп"))
+ call_pattern = re.compile(_make_keyword_pattern(f).pattern + r"\s*\(")
+ used = any(
+ call_pattern.search(text)
+ for line in lines
+ if not line.strip().startswith("рдХрд╛рд░реНрдп")
+ for is_string, text in _tokenize_preserving_strings(line)
+ if not is_string
+ )
if not used:
errors.append(f"рдЪреЗрддрд╛рд╡рдиреА: рдХрд╛рд░реНрдп '{f}' рдХреЗрд╣рдиреЛ рдард╛рдо рдкреНрд░рдпреЛрдЧ рдирд╣рд┐ рдХрдПрд▓ рдЧреЗрд▓ рдЕрдЫрд┐")
diff --git a/pyproject.toml b/pyproject.toml
index 77745a3..08b6158 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,17 +4,16 @@ build-backend = "setuptools.build_meta"
[project]
name = "python_maithili"
-version = "0.3.0"
+version = "0.4.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"
+requires-python = ">=3.10"
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",
@@ -56,6 +55,7 @@ omit = [
]
[tool.coverage.report]
+fail_under = 85
exclude_lines = [
"pragma: no cover",
"if __name__ == .__main__.:",
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 3a4df92..7065e76 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,5 +1,9 @@
# 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
+pytest>=9.1.1,<10
+pytest-cov>=7.1.0,<8
+pytest-timeout>=2.4.0,<3
+
+# Build + release tooling (used by docs/RELEASE.md workflow).
+build>=1.5.0,<2
+twine>=6.2.0,<7
diff --git a/setup.py b/setup.py
index f6ea8c9..077aea9 100644
--- a/setup.py
+++ b/setup.py
@@ -35,12 +35,11 @@ def _read_version() -> str:
},
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.9',
+ python_requires='>=3.10',
)
diff --git a/tests/test_linter.py b/tests/test_linter.py
index e13bf93..e15f557 100644
--- a/tests/test_linter.py
+++ b/tests/test_linter.py
@@ -32,6 +32,32 @@ def test_simple_function_with_usage_passes():
assert lint_maithili_code(code) == []
+def test_function_name_inside_longer_identifier_is_unused():
+ code = (
+ "рдХрд╛рд░реНрдп рдЬреЛрдбрд╝():\n"
+ " рдлреЗрд░ рдХрд░реВ рез\n"
+ "\n"
+ "рдЬреЛрдбрд╝рд▓ = рел\n"
+ )
+
+ errors = lint_maithili_code(code)
+
+ assert "рдЪреЗрддрд╛рд╡рдиреА: рдХрд╛рд░реНрдп 'рдЬреЛрдбрд╝' рдХреЗрд╣рдиреЛ рдард╛рдо рдкреНрд░рдпреЛрдЧ рдирд╣рд┐ рдХрдПрд▓ рдЧреЗрд▓ рдЕрдЫрд┐" in errors
+
+
+def test_function_name_inside_string_is_unused():
+ code = (
+ "рдХрд╛рд░реНрдп рдЬреЛрдбрд╝():\n"
+ " рдлреЗрд░ рдХрд░реВ рез\n"
+ "\n"
+ "рдЫрдкрд╛рдЙ(\"рдЬреЛрдбрд╝()\")\n"
+ )
+
+ errors = lint_maithili_code(code)
+
+ assert "рдЪреЗрддрд╛рд╡рдиреА: рдХрд╛рд░реНрдп 'рдЬреЛрдбрд╝' рдХреЗрд╣рдиреЛ рдард╛рдо рдкреНрд░рдпреЛрдЧ рдирд╣рд┐ рдХрдПрд▓ рдЧреЗрд▓ рдЕрдЫрд┐" in errors
+
+
def test_class_with_constructor_passes():
code = (
"рд╡рд░реНрдЧ рд╡реНрдпрдХреНрддрд┐:\n"
@@ -78,6 +104,13 @@ def test_gte_operator(self):
assert not any("рдЪрд░ рдирд╛рдо" in e for e in errors)
+@pytest.mark.parametrize("operator", ["+=", "-=", "*=", "/=", "//=", "%=", "**="])
+def test_augmented_assignment_not_flagged_as_invalid_name(operator):
+ code = f"рдХ = реж\nрдХ {operator} рез\n"
+ errors = lint_maithili_code(code)
+ assert not any("рдЪрд░ рдирд╛рдо" in error for error in errors)
+
+
# ---------------------------------------------------------------------------
# Structural errors still fire
# ---------------------------------------------------------------------------