From 82a87ba8e212ac5d9cc1c311a9260523f27e87a0 Mon Sep 17 00:00:00 2001 From: Neal006 Date: Sat, 4 Jul 2026 21:21:29 +0530 Subject: [PATCH 1/2] chore(lint): add ruff config and fix all lint violations Unblocks a lint gate in CI without touching test_imports.py's intentional unused imports (per-file ignore instead). --- dashboard.py | 6 +++--- memorylens/api.py | 2 +- memorylens/cli.py | 11 +++++------ memorylens/evaluation/llm_judge.py | 5 +++-- memorylens/evaluation/metrics.py | 2 +- memorylens/memory/cascading.py | 4 ++-- memorylens/memory/rag_chunked.py | 2 +- memorylens/simulator/conversation.py | 2 +- memorylens/simulator/facts.py | 2 +- pyproject.toml | 9 +++++++++ quick_demo.py | 2 +- tests/test_pipeline.py | 4 ++-- 12 files changed, 30 insertions(+), 21 deletions(-) diff --git a/dashboard.py b/dashboard.py index 47bd613..a51de9e 100644 --- a/dashboard.py +++ b/dashboard.py @@ -6,7 +6,7 @@ import json import os -from typing import Dict, List, Optional +from typing import Dict, List import pandas as pd import plotly.graph_objects as go @@ -235,8 +235,8 @@ def render_results(data: Dict, is_demo: bool = False) -> None: content_vals = data[name]["recall"] llm_vals = data[name].get("llm_recall", [None] * len(content_vals)) gaps = [ - (c - l) * 100 if l is not None else None - for c, l in zip(content_vals, llm_vals) + (c - lv) * 100 if lv is not None else None + for c, lv in zip(content_vals, llm_vals) ] if any(g is not None for g in gaps): fig_gap.add_trace(go.Bar( diff --git a/memorylens/api.py b/memorylens/api.py index 65fd78a..a844831 100644 --- a/memorylens/api.py +++ b/memorylens/api.py @@ -13,7 +13,7 @@ import threading import uuid -from typing import Dict, List, Optional +from typing import Dict, List from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field diff --git a/memorylens/cli.py b/memorylens/cli.py index 3e3484d..23b7fd3 100644 --- a/memorylens/cli.py +++ b/memorylens/cli.py @@ -26,7 +26,6 @@ memorylens --list-providers """ -import os import sys import json import argparse @@ -235,7 +234,7 @@ def _print_single_seed_results(display: dict, backends: list) -> None: col = " ".join(f"T={c:3d}" for c in checkpoints) sep = "-" * 65 - print(f"\nCONTENT Recall@T") + print("\nCONTENT Recall@T") print(f" {'Backend':<14} {col}") print(sep) for name in backends: @@ -245,7 +244,7 @@ def _print_single_seed_results(display: dict, backends: list) -> None: print(f" {name:<14} {vals}") if display.get("has_llm_eval"): - print(f"\nLLM Recall@T (answer+judge)") + print("\nLLM Recall@T (answer+judge)") print(f" {'Backend':<14} {col}") print(sep) for name in backends: @@ -258,7 +257,7 @@ def _print_single_seed_results(display: dict, backends: list) -> None: ) print(f" {name:<14} {vals}") - print(f"\n Gap = Content Recall - LLM Recall") + print("\n Gap = Content Recall - LLM Recall") print(f" {'Backend':<14} {col}") print(sep) for name in backends: @@ -267,8 +266,8 @@ def _print_single_seed_results(display: dict, backends: list) -> None: content = display[name]["recall"] llm = display[name].get("llm_recall", [None]*len(content)) vals = " ".join( - f"{(c - l)*100:+5.1f}%" if l is not None else " N/A " - for c, l in zip(content, llm) + f"{(c - lv)*100:+5.1f}%" if lv is not None else " N/A " + for c, lv in zip(content, llm) ) print(f" {name:<14} {vals}") diff --git a/memorylens/evaluation/llm_judge.py b/memorylens/evaluation/llm_judge.py index b2573a6..465f651 100644 --- a/memorylens/evaluation/llm_judge.py +++ b/memorylens/evaluation/llm_judge.py @@ -6,7 +6,7 @@ judge mode. All primary benchmark metrics remain content-based. """ -from typing import Dict, List, Optional +from typing import Dict, List from memorylens.utils.llm import chat from memorylens.memory.base import BaseMemory from memorylens.simulator.facts import Fact @@ -32,7 +32,8 @@ def judge_answer( ] raw = chat(messages, model=model, temperature=0.0, max_tokens=80) - import json, re + import json + import re try: match = re.search(r"\{.*\}", raw, re.DOTALL) parsed = json.loads(match.group()) if match else {} diff --git a/memorylens/evaluation/metrics.py b/memorylens/evaluation/metrics.py index 5c7856a..538aea6 100644 --- a/memorylens/evaluation/metrics.py +++ b/memorylens/evaluation/metrics.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Dict, List from memorylens.memory.base import BaseMemory from memorylens.simulator.facts import Fact diff --git a/memorylens/memory/cascading.py b/memorylens/memory/cascading.py index 3d13287..189fcc1 100644 --- a/memorylens/memory/cascading.py +++ b/memorylens/memory/cascading.py @@ -2,8 +2,8 @@ from typing import List, Dict, Optional, Callable, Tuple import numpy as np from .base import BaseMemory -from .decay import get_decay_fn, decay_ebbinghaus -from memorylens.utils.embeddings import embed, top_k_indices +from .decay import get_decay_fn +from memorylens.utils.embeddings import embed def _extractive_summary(messages: List[Dict], max_chars: int = 400) -> str: diff --git a/memorylens/memory/rag_chunked.py b/memorylens/memory/rag_chunked.py index a9c8095..c81ec9c 100644 --- a/memorylens/memory/rag_chunked.py +++ b/memorylens/memory/rag_chunked.py @@ -21,7 +21,7 @@ ChunkedRAGMemory โ€” chunked + evicting, bounded index (realistic lower bound) """ -from typing import List, Dict, Tuple +from typing import List, Dict import numpy as np from .base import BaseMemory from memorylens.utils.embeddings import embed, top_k_indices diff --git a/memorylens/simulator/conversation.py b/memorylens/simulator/conversation.py index 0f329a8..6f81e3c 100644 --- a/memorylens/simulator/conversation.py +++ b/memorylens/simulator/conversation.py @@ -1,5 +1,5 @@ from typing import List, Dict, Optional -from .facts import Fact, BENCHMARK_FACTS +from .facts import Fact FILLER_TURNS = [ "Can you explain the difference between REST and GraphQL APIs?", diff --git a/memorylens/simulator/facts.py b/memorylens/simulator/facts.py index c92875d..70f6db3 100644 --- a/memorylens/simulator/facts.py +++ b/memorylens/simulator/facts.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Optional, List diff --git a/pyproject.toml b/pyproject.toml index a8d35b7..ee70683 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ dev = [ "uvicorn>=0.29.0", "build>=1.0", "twine>=5.0", + "ruff>=0.5", ] [project.scripts] @@ -94,3 +95,11 @@ include = ["memorylens*"] [tool.pytest.ini_options] testpaths = ["tests"] + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint.per-file-ignores] +# import smoke test โ€” unused imports are the point +"tests/test_imports.py" = ["F401"] diff --git a/quick_demo.py b/quick_demo.py index a28dc99..968a324 100644 --- a/quick_demo.py +++ b/quick_demo.py @@ -35,7 +35,7 @@ def main() -> None: from memorylens.memory.cascading import CascadingTemporalMemory from memorylens.evaluation.metrics import ( recall_at_t, temporal_drift_score, memory_noise_ratio, - precision_at_k, cascade_efficiency, + cascade_efficiency, ) if not args.quiet: diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index e414a52..26687fe 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -10,14 +10,14 @@ os.environ["USE_TF"] = "0" sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from memorylens.simulator.facts import BENCHMARK_FACTS, Fact +from memorylens.simulator.facts import BENCHMARK_FACTS from memorylens.simulator.conversation import generate_conversation from memorylens.memory.naive import NaiveMemory from memorylens.memory.rag import RAGMemory from memorylens.memory.cascading import CascadingTemporalMemory from memorylens.memory.summary import SummaryMemory from memorylens.evaluation.metrics import ( - recall_at_t, temporal_drift_score, memory_noise_ratio, precision_at_k + recall_at_t, temporal_drift_score, memory_noise_ratio ) From 55cd08d5548a3a023351493f5a82d8f3306568ed Mon Sep 17 00:00:00 2001 From: Neal006 Date: Sat, 4 Jul 2026 21:21:35 +0530 Subject: [PATCH 2/2] ci: add maintainer automation for repo scaling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds PR title linting, auto-labeling by changed path, stale issue/PR sweeps, CodeQL scanning, first-interaction greetings, Dependabot, CODEOWNERS, and SECURITY.md โ€” the automation a repo needs before it can handle high contributor volume without a maintainer manually triaging every PR and issue. Also routes CODE_OF_CONDUCT reports to private email instead of a public issue, and documents the new gates in CONTRIBUTING.md. --- .github/CODEOWNERS | 7 +++++ .github/ISSUE_TEMPLATE/config.yml | 8 +++++ .github/dependabot.yml | 19 ++++++++++++ .github/labeler.yml | 49 +++++++++++++++++++++++++++++++ .github/pull_request_template.md | 6 ++-- .github/workflows/ci.yml | 16 ++++++++++ .github/workflows/codeql.yml | 23 +++++++++++++++ .github/workflows/greetings.yml | 29 ++++++++++++++++++ .github/workflows/labeler.yml | 16 ++++++++++ .github/workflows/pr-title.yml | 28 ++++++++++++++++++ .github/workflows/stale.yml | 32 ++++++++++++++++++++ .gitignore | 2 ++ CODE_OF_CONDUCT.md | 2 +- CONTRIBUTING.md | 11 +++++-- README.md | 2 +- SECURITY.md | 23 +++++++++++++++ 16 files changed, 265 insertions(+), 8 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/greetings.yml create mode 100644 .github/workflows/labeler.yml create mode 100644 .github/workflows/pr-title.yml create mode 100644 .github/workflows/stale.yml create mode 100644 SECURITY.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..6799e9b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# Default owner for everything โ€” update as maintainers join. +* @Neal006 + +# High-blast-radius areas: benchmark correctness and published numbers. +/memorylens/evaluation/ @Neal006 +/README.md @Neal006 +/.github/ @Neal006 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..15cd1c5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: ๐Ÿ’ฌ Questions & Discussions + url: https://github.com/Neal006/memorylens/discussions + about: Ask questions, share benchmark results, or discuss memory architectures. + - name: ๐Ÿ”’ Report a security vulnerability + url: https://github.com/Neal006/memorylens/security/advisories/new + about: Please report security issues privately, not as public issues. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b864fce --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + groups: + python-deps: + patterns: ["*"] + labels: ["area: dependencies"] + + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + groups: + actions: + patterns: ["*"] + labels: ["area: ci"] diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..9e33d4a --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,49 @@ +"area: memory-backend": + - changed-files: + - any-glob-to-any-file: "memorylens/memory/**" + +"area: evaluation": + - changed-files: + - any-glob-to-any-file: "memorylens/evaluation/**" + +"area: simulator": + - changed-files: + - any-glob-to-any-file: "memorylens/simulator/**" + +"area: providers": + - changed-files: + - any-glob-to-any-file: "memorylens/utils/providers.py" + +"area: api": + - changed-files: + - any-glob-to-any-file: "memorylens/api.py" + +"area: cli": + - changed-files: + - any-glob-to-any-file: + - "memorylens/cli.py" + - "main.py" + +"area: dashboard": + - changed-files: + - any-glob-to-any-file: "dashboard.py" + +"area: documentation": + - changed-files: + - any-glob-to-any-file: + - "**/*.md" + - "docs/**" + +"area: tests": + - changed-files: + - any-glob-to-any-file: "tests/**" + +"area: ci": + - changed-files: + - any-glob-to-any-file: ".github/**" + +"area: dependencies": + - changed-files: + - any-glob-to-any-file: + - "pyproject.toml" + - "requirements.txt" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e964f43..4b3e85e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -36,8 +36,8 @@ pytest tests/ -v - [ ] Type hints used on all new function signatures - [ ] No API key required to run any new tests - [ ] If adding a backend: registered in `VALID_BACKENDS` and `_make_memory()` in `benchmark.py` -- [ ] If adding a scenario: `--scenario` flag added to `main.py` -- [ ] If adding a CLI flag: docstring example in `main.py` updated +- [ ] If adding a scenario: registered in the `SCENARIOS` dict in `simulator/scenarios/__init__.py` +- [ ] Lint passes: `ruff check .` - [ ] README updated if there are new user-facing features or CLI flags - [ ] No hardcoded API keys or secrets in any file @@ -51,5 +51,3 @@ pytest tests/ -v | Recall@T=100 | | | | Tokens/query@T=100 | | | | Cascade Efficiency | | | - -๐Ÿค– Generated with [Claude Code](https://claude.ai/claude-code) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db216ca..cfb9d7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,23 @@ on: pull_request: branches: [main] +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pipx run ruff check . + test: strategy: fail-fast: false diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..82a970a --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,23 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "31 5 * * 1" + +permissions: + security-events: write + contents: read + +jobs: + analyze: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: github/codeql-action/init@v3 + with: + languages: python + - uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml new file mode 100644 index 0000000..c6c8d9c --- /dev/null +++ b/.github/workflows/greetings.yml @@ -0,0 +1,29 @@ +name: Welcome + +on: + issues: + types: [opened] + pull_request_target: + types: [opened] + +permissions: + issues: write + pull-requests: write + +jobs: + greet: + runs-on: ubuntu-latest + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: > + Thanks for opening your first MemoryLens issue! A maintainer will triage it soon. + If you'd like to work on it yourself, comment here to claim it โ€” + [CONTRIBUTING.md](https://github.com/Neal006/memorylens/blob/main/CONTRIBUTING.md) + has everything you need to get a dev environment running in 5 minutes. + pr-message: > + Thanks for your first contribution to MemoryLens! ๐ŸŽ‰ CI will run the test suite + on Linux/macOS/Windows across Python 3.10โ€“3.13 โ€” no API key needed. + A maintainer will review within 48 hours. If CI fails, `pytest tests/ -v` and + `ruff check .` locally reproduce almost every failure. diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..f551f64 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,16 @@ +name: Label PRs + +on: + pull_request_target: + +permissions: + contents: read + pull-requests: write + +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 + with: + sync-labels: true diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..9370f01 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,28 @@ +name: PR Title + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + pull-requests: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + # PRs are squash-merged, so the PR title becomes the commit message on main. + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + perf + refactor + test + docs + chore + ci + style diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..2c4d3d2 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,32 @@ +name: Stale + +on: + schedule: + - cron: "17 3 * * *" + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-issue-stale: 60 + days-before-issue-close: 14 + days-before-pr-stale: 30 + days-before-pr-close: 14 + stale-issue-label: "status: stale" + stale-pr-label: "status: stale" + exempt-issue-labels: "status: open-for-contribution,pinned,roadmap,help wanted,good first issue" + exempt-pr-labels: "pinned" + stale-issue-message: > + This issue has been inactive for 60 days. It will close in 14 days + unless there is new activity. Comment or add the `pinned` label to keep it open. + stale-pr-message: > + This PR has been inactive for 30 days. It will close in 14 days + unless there is new activity. Push a commit or comment to keep it open. + operations-per-run: 100 diff --git a/.gitignore b/.gitignore index af478f8..58c4a1c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ demo_venv/ benchmark_v04_200.json *.db experiment_logs/runs_summary*.csv +.pytest_cache/ +.ruff_cache/ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index eb94171..f28e634 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -23,7 +23,7 @@ We pledge to act and interact in ways that contribute to an open, welcoming, div ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening a GitHub issue or contacting the maintainer directly. All complaints will be reviewed and investigated promptly and fairly. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported privately to the maintainer at builtbyneal@gmail.com. All complaints will be reviewed and investigated promptly and fairly, and reporter confidentiality will be respected. ## Attribution diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e48b85d..3297b59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -286,10 +286,17 @@ refactor: extract _extract_entity() helper from EntityMemory 1. Fork the repo and create a branch: `git checkout -b feat/your-feature` 2. Make your changes with tests -3. Run `pytest tests/ -v` โ€” all tests must pass +3. Run `pytest tests/ -v` and `ruff check .` โ€” both must pass 4. Open a PR against `main` โ€” fill in the PR template 5. Reference the issue: `Closes #` in the PR description +**What happens automatically when you open a PR:** + +- CI runs lint + the full test suite on Linux, macOS, and Windows (Python 3.10โ€“3.13) and builds the package +- Your **PR title is checked** against Conventional Commits (`feat: ...`, `fix: ...`) โ€” PRs are squash-merged, so the title becomes the commit message +- Area labels (`area: memory-backend`, `area: documentation`, โ€ฆ) are applied from the files you changed +- PRs inactive for 30 days are marked stale and closed after 14 more โ€” push a commit or comment to keep one alive + **PR checklist:** - [ ] All existing tests pass: `pytest tests/ -v` - [ ] New tests added for new functionality @@ -311,7 +318,7 @@ refactor: extract _extract_entity() helper from EntityMemory | **No new top-level dependencies** without issue discussion | Keeps install size predictable | | **All new metrics return `float` in `[0, 1]`** | Ensures dashboard and aggregation code work without guards | | **All tests pass without an API key** | Keeps CI fast and accessible to all contributors | -| **PEP 8**, 100-char line limit | Consistency | +| **`ruff check .` passes** (PEP 8, 100-char lines) | Enforced in CI โ€” no style debates in review | --- diff --git a/README.md b/README.md index cb49037..9217300 100644 --- a/README.md +++ b/README.md @@ -276,7 +276,7 @@ pip install -e ".[server,dev]" pytest tests -q # all green before you start ``` -Start here: [`good first issue`](https://github.com/Neal006/memorylens/issues?q=label%3A%22good+first+issue%22) ยท Guide: [CONTRIBUTING.md](CONTRIBUTING.md) ยท Plans: [ROADMAP.md](ROADMAP.md) +Start here: [`good first issue`](https://github.com/Neal006/memorylens/issues?q=label%3A%22good+first+issue%22) ยท Guide: [CONTRIBUTING.md](CONTRIBUTING.md) ยท Plans: [ROADMAP.md](ROADMAP.md) ยท Questions: [Discussions](https://github.com/Neal006/memorylens/discussions) ยท Vulnerabilities: [SECURITY.md](SECURITY.md) --- diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ab883de --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +## Supported Versions + +Only the latest release on [PyPI](https://pypi.org/project/memorylens-bench/) receives security fixes. + +## Reporting a Vulnerability + +Please **do not open a public issue** for security problems. + +Report privately via [GitHub Security Advisories](https://github.com/Neal006/memorylens/security/advisories/new) +or email builtbyneal@gmail.com. + +You can expect an acknowledgement within 72 hours and a fix or mitigation plan within 14 days +for confirmed issues. + +## Scope notes + +MemoryLens is a local benchmark tool. The most security-relevant surfaces are: + +- The optional FastAPI server (`memorylens.api`) โ€” intended for local use, ships with no auth. + Do not expose it to the public internet without a reverse proxy handling auth and rate limits. +- LLM provider keys read from `.env` โ€” never committed, never logged.