diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..203db0a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Contributing to MT5-Quant + +Thanks for your interest in improving MT5-Quant! This document explains how to +contribute and what we expect from contributions. + +## How to contribute + +1. **Fork** the repository and create a topic branch from `master`. +2. Make your change, keeping it focused (one logical change per PR). +3. Open a **pull request against `master`**. A pull request template + (`.github/PULL_REQUEST_TEMPLATE.md`) is provided — please fill it in. +4. Ensure CI passes (build, `cargo test`, clippy, formatting, cargo-deny, + CodeQL). Maintainers review and merge once checks and review are satisfied. + +## Development setup + +- Install Rust (stable toolchain) and, for full backtests, Wine/MT5 as described + in `README.md` and `docs/QUICKSTART.md`. +- Build the release binary with either: + - `bash scripts/build-rust.sh`, or + - `cargo build --release` +- The binary is produced at `target/release/mt5-quant`. + +## Testing requirement + +- **Non-trivial changes must include tests.** New functionality should come with + unit or integration tests where practical. +- `cargo test` runs automatically in CI (`rust.yml`) on every push and pull + request, so a change that breaks tests cannot merge. + +## Commit style + +- Follow **Conventional Commits** (`feat:`, `fix:`, `docs:`, `refactor:`, + `test:`, `release:`), as documented in `AGENTS.md`. + +## Issue tracker + +- Bugs and enhancement requests are tracked on the project's + [GitHub Issues](https://github.com/masdevid/mt5-quant/issues), with + bug-report and feature-request templates under `.github/ISSUE_TEMPLATE/`. diff --git a/scripts/cii_criteria.py b/scripts/cii_criteria.py new file mode 100644 index 0000000..e9a4b6f --- /dev/null +++ b/scripts/cii_criteria.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""List incomplete OpenSSF Best Practices criteria and generate a pre-filled edit URL. + +Usage: + cii_criteria.py [section] [--exclude n1,n2,n3] + + section: passing | silver | gold | baseline-1 | baseline-2 | baseline-3 + (default: passing) + --exclude: comma-separated criterion names to SKIP from the pre-filled URL + (they stay at their current Unmet/?/N/A in the form). + +The script fetches the upstream criteria.yml to learn which criteria belong to +which tier/section, then restricts BOTH the printed incomplete list AND the +generated edit-URL parameters to only that section's criteria. +""" +import sys, os, json, urllib.request, urllib.parse, argparse +import ssl + +try: + import certifi + _SSL_CTX = ssl.create_default_context(cafile=certifi.where()) +except Exception: + _SSL_CTX = ssl.create_default_context() + +BASE = "https://www.bestpractices.dev" +CRITERIA_URL = ("https://raw.githubusercontent.com/ossf/best-practices-badge/" + "main/criteria/criteria.yml") + +# CLI section argument -> tier key inside criteria.yml +SECTION_TO_TIER = { + "passing": "0", + "silver": "1", + "gold": "2", + "baseline-1": "baseline-1", + "baseline-2": "baseline-2", + "baseline-3": "baseline-3", +} + + +def load_justifications(): + """Load real justifications from the sibling cii_justifications.json. + + Returns a dict name -> justification string. Falls back to {} if the file + is missing or invalid, so callers should still default to a placeholder. + """ + path = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "cii_justifications.json") + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + if isinstance(data, dict): + return data + except Exception: + pass + return {} + + +def fetch(url): + req = urllib.request.Request(url, headers={"User-Agent": "cii-skill"}) + with urllib.request.urlopen(req, timeout=20, context=_SSL_CTX) as r: + return r.read().decode() + + +def _collect_names(node, out): + """Recursively walk the criteria.yml nested structure. + + The file is a list of (tier_key, [sections]); each section is + (section_name, [ (group, [...]) | (criterion_name, {attrs}) ]). A leaf + criterion is a 2-tuple whose second element is a dict (it has 'category'). + """ + if isinstance(node, tuple) and len(node) == 2: + key, val = node + if isinstance(val, dict): + out.append(key) # leaf criterion + elif isinstance(val, list): + for child in val: + _collect_names(child, out) + elif isinstance(node, list): + for child in node: + _collect_names(child, out) + + +def fetch_section_map(): + """Return {section_arg: set(criterion_names)} for every known tier. + + Returns None if criteria.yml could not be fetched/parsed. + """ + try: + raw = fetch(CRITERIA_URL) + except Exception as e: + print(f"WARNING: could not fetch criteria definitions ({e}); " + f"falling back to all-criteria behavior.", file=sys.stderr) + return None + try: + doc = __import__("yaml").safe_load(raw) + except Exception as e: + print(f"WARNING: could not parse criteria definitions ({e}); " + f"falling back to all-criteria behavior.", file=sys.stderr) + return None + + # doc is a list of (tier_key, sections) + tier_names = {} + for item in doc: + if isinstance(item, tuple) and len(item) == 2: + tier_key, sections = item + names = [] + _collect_names(sections, names) + tier_names[tier_key] = set(names) + + section_map = {} + for section_arg, tier_key in SECTION_TO_TIER.items(): + section_map[section_arg] = tier_names.get(tier_key, set()) + return section_map + + +def main(): + parser = argparse.ArgumentParser( + description="List incomplete CII/OpenSSF Best Practices criteria.") + parser.add_argument("project_id", nargs="?", default="14227") + parser.add_argument("section", nargs="?", default="passing", + choices=list(SECTION_TO_TIER.keys())) + parser.add_argument("--exclude", default="", + help="Comma-separated criterion names to skip in the URL.") + args = parser.parse_args() + + project_id = args.project_id + section = args.section + exclude = {x.strip() for x in args.exclude.split(",") if x.strip()} + + justifications = load_justifications() + + data = json.loads(fetch(f"{BASE}/projects/{project_id}.json")) + + # All criteria reported by the project (every tier). + criteria = {} + for k, v in data.items(): + if k.endswith("_status") and isinstance(v, str): + name = k[: -len("_status")] + criteria[name] = {"status": v, + "justification": data.get(name + "_justification", "") or ""} + + # Build the section -> criterion-name set from upstream definitions. + section_map = fetch_section_map() + target_names = section_map.get(section) if section_map else None + if target_names is not None and not target_names: + print(f"WARNING: no criteria found for section '{section}' in " + f"criteria.yml; falling back to all-criteria behavior.", + file=sys.stderr) + target_names = None # fall back: no section restriction + + # Restrict to the requested section when we have a valid name set. + if target_names is not None: + scoped = {n: c for n, c in criteria.items() if n in target_names} + else: + scoped = criteria + + incomplete = {n: c for n, c in scoped.items() if c["status"] != "Met"} + doable = {n: c for n, c in incomplete.items() if c["status"] in ("Unmet", "?")} + na = {n: c for n, c in incomplete.items() if c["status"] == "N/A"} + + print(f"Project {project_id}: badge_level={data.get('badge_level')} " + f"(passing%={data.get('badge_percentage_0')} " + f"silver%={data.get('badge_percentage_1')} " + f"gold%={data.get('badge_percentage_2')})") + scope_note = f"section '{section}'" if target_names is not None else "ALL sections (fallback)" + print(f"Scope: {scope_note} | criteria in scope: {len(scoped)} | " + f"Incomplete: {len(incomplete)} " + f"(Unmet/? : {len(doable)} | N/A: {len(na)})\n") + + print("Incomplete (Unmet/? — candidate to mark Met):") + for n, c in sorted(doable.items()): + print(f" - {n}: {c['status']} | current note: {c['justification'][:70]}") + print("\nN/A (review separately — usually leave as N/A):") + for n, c in sorted(na.items()): + print(f" - {n}: N/A | note: {c['justification'][:70]}") + + # Build URL params, restricted to the section AND honoring --exclude. + # N/A criteria are never pre-filled as Met (they are excluded from `doable`). + params = [] + for n in sorted(doable): + if exclude and n in exclude: + continue # leave at current Unmet/?/N/A in the form + params.append((f"{n}_status", "Met")) + params.append((f"{n}_justification", + justifications.get(n, "TODO: justify"))) + params.append(("overrides", "*,osps_*")) + edit_url = f"{BASE}/projects/{project_id}/{section}/edit?{urllib.parse.urlencode(params)}" + print(f"\n=== Edit URL ({section}" + + (f", excluding {sorted(exclude)}" if exclude else "") + + f") ===\n{edit_url}") + print(f"\nURL param count for _status/_justification pairs: " + f"{sum(1 for k,_ in params if k.endswith('_status'))}") + print("WARNING: pre-fills Unmet/? criteria as 'Met' with a placeholder justification.") + print("Review EVERY field before Save; only mark Met what you genuinely satisfy.") + + +if __name__ == "__main__": + main() diff --git a/scripts/cii_justifications.json b/scripts/cii_justifications.json new file mode 100644 index 0000000..5d09c91 --- /dev/null +++ b/scripts/cii_justifications.json @@ -0,0 +1,33 @@ +{ + "build": "The project builds with Cargo; CI (.github/workflows/rust.yml) runs `cargo build --verbose` on every push and pull request.", + "build_common_tools": "Builds use Cargo, the standard and widely-adopted Rust build toolchain.", + "build_floss_tools": "All build and CI tooling is FLOSS: Cargo/rustc, GitHub Actions, clippy, and cargo-deny.", + "contribution_requirements": "CONTRIBUTING.md documents forking, PRs against `master`, the PR template, dev setup, the testing requirement, and Conventional Commits.", + "crypto_call": "Outbound internet calls (GitHub API for version checks/downloads) use HTTPS via the system `curl`, which negotiates TLS — no plaintext internet communication.", + "crypto_floss": "TLS is provided by the system `curl`/OS libraries (FLOSS); the project bundles no proprietary cryptography.", + "crypto_keylength": "Relies on `curl`/OS TLS defaults (modern ciphers with >=128-bit security and standard RSA/ECDSA key sizes).", + "crypto_pfs": "Modern TLS via `curl` negotiates forward secrecy (ECDHE) with current GitHub endpoints.", + "crypto_published": "Uses the widely-published TLS protocol through the standard `curl` toolchain rather than any custom or unpublished algorithm.", + "crypto_random": "The project generates no security-sensitive random values (only UUIDs for internal job IDs), so no cryptographic RNG is required for its functionality.", + "crypto_working": "TLS HTTPS calls to GitHub succeed in practice; the version-check and self-update features work as designed.", + "enhancement_responses": "Enhancement requests are tracked on the public GitHub issue tracker (with a feature-request template) and reviewed through PRs.", + "no_leaked_credentials": "Sensitive local config (config/mt5-quant.yaml, which can hold passwords/tokens) is gitignored, and no secrets are committed to the repository.", + "repo_interim": "The repository at github.com/masdevid/mt5-quant is the canonical project home, not a temporary or interim placeholder.", + "report_archive": "GitHub automatically archives the source; tagged releases additionally publish downloadable source and binary archives.", + "report_responses": "The project uses GitHub Issues (with bug/feature templates) as its public tracker, and maintainers triage and respond via issues and PR reviews.", + "static_analysis": "CI runs Rust Clippy and GitHub CodeQL (Rust) as static analysis on every push and pull request.", + "static_analysis_fixed": "Clippy runs with `-D warnings` so defects fail the build, and CodeQL findings are reviewed and remediated.", + "static_analysis_often": "Static analysis runs automatically on every push to and pull request against `master` (clippy in rust.yml, CodeQL in codeql.yml).", + "test": "Unit and integration tests exist in `tests/` (Rust integration_tests.rs, test_mcp.py, shell tests) and `cargo test` runs in CI.", + "test_most": "Unit tests cover core modules (analytics extraction/analysis, storage, report parsing); `cargo test` is part of CI.", + "test_policy": "AGENTS.md and CONTRIBUTING.md require that non-trivial changes include tests, and CI enforces `cargo test`.", + "tests_are_added": "The `tests/` directory contains Rust, Python, and shell tests, and new non-trivial functionality is expected to add corresponding tests.", + "tests_documented_added": "Testing expectations and the `cargo test` requirement are documented in AGENTS.md and CONTRIBUTING.md.", + "version_semver": "The `version` field in Cargo.toml uses Semantic Versioning (currently 1.34.3).", + "version_tags": "Releases are tagged with `vX.Y.Z` semver tags (e.g., v1.34.1), confirmed via `git tag`.", + "version_unique": "Each release carries a unique `version` in Cargo.toml and a corresponding immutable `vX.Y.Z` git tag.", + "vulnerabilities_critical_fixed": "No known unpatched critical vulnerabilities; `cargo-deny` advisories and dependency-review run in CI and dependency updates are monitored.", + "vulnerabilities_fixed_60_days": "Dependency vulnerabilities are surfaced by `cargo-deny` (advisories) and the dependency-review action in CI, and are updated promptly.", + "warnings": "CI enforces a warning-free build via `cargo clippy --all-targets -- -D warnings` plus `cargo fmt --check`.", + "warnings_fixed": "Because Clippy uses `-D warnings`, any warning fails CI and must be fixed before a change can merge." +}