Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .bestpractices.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"build_floss_tools_justification": "pip, uv, hatchling, pytest, ruff and pyright are all FLOSS, and the build runs on ubuntu-latest and windows-latest GitHub runners.",

"test_status": "Met",
"test_justification": "An automated test suite of 6550 tests lives under tests_py/ (measured 2026-07-30 with 'pytest --collect-only -q'), covering core, handlers, infrastructure, integration, invariants and architecture: https://github.com/cdeust/Cortex/tree/main/tests_py",
"test_justification": "An automated test suite of 6565 tests lives under tests_py/ (measured 2026-07-30 with 'pytest --collect-only -q'), covering core, handlers, infrastructure, integration, invariants and architecture: https://github.com/cdeust/Cortex/tree/main/tests_py",

"test_invocation_status": "Met",
"test_invocation_justification": "The whole suite runs with a single 'pytest' command, documented in CONTRIBUTING.md under Testing along with per-layer subsets: https://github.com/cdeust/Cortex/blob/main/CONTRIBUTING.md",
Expand All @@ -114,7 +114,7 @@
"test_policy_justification": "CONTRIBUTING.md carries an explicit, mandatory testing policy: new functionality ships with tests in the automated suite, a bug fix carries a regression test that fails on the pre-fix code, and each failure path asserts its observable effect including the signal it emits \u2014 https://github.com/cdeust/Cortex/blob/main/CONTRIBUTING.md#the-testing-policy-mandatory. The per-tool checklist ('Add a unit test', 'Add an integration test if the tool touches the database') and the five-element mechanism checklist restate it per change type.",

"tests_are_added_status": "Met",
"tests_are_added_justification": "New functionality ships with its tests; the suite has grown to 6550 tests alongside the v4.x feature series, and CI runs it on every pull request.",
"tests_are_added_justification": "New functionality ships with its tests; the suite has grown to 6565 tests alongside the v4.x feature series, and CI runs it on every pull request.",

"tests_documented_added_status": "Met",
"tests_documented_added_justification": "The requirement is written into the documented instructions for change proposals: https://github.com/cdeust/Cortex/blob/main/CONTRIBUTING.md#the-testing-policy-mandatory states that every change adding or altering observable behaviour must arrive with tests in the same PR, the per-tool and per-mechanism checklists repeat it as a concrete step, and .github/PULL_REQUEST_TEMPLATE.md requires a Test plan section in every pull request.",
Expand Down Expand Up @@ -189,7 +189,7 @@
"static_analysis_often_justification": "CodeQL default setup analyses each push and pull request and additionally runs on a weekly schedule, so analysis happens per change rather than per release.",

"dynamic_analysis_status": "Unmet",
"dynamic_analysis_justification": "No dynamic analysis tool in the badge's sense (fuzzer, sanitizer, or scanner) is applied. The project runs a 6550-test suite with coverage on every change, but a test suite is not a dynamic analysis tool and is not claimed as one here.",
"dynamic_analysis_justification": "No dynamic analysis tool in the badge's sense (fuzzer, sanitizer, or scanner) is applied. The project runs a 6565-test suite with coverage on every change, but a test suite is not a dynamic analysis tool and is not claimed as one here.",

"dynamic_analysis_unsafe_status": "N/A",
"dynamic_analysis_unsafe_justification": "Cortex is written in Python, a memory-safe language, so the memory-safety tooling this criterion asks about (ASan, Valgrind) does not apply.",
Expand Down Expand Up @@ -297,7 +297,7 @@
"interfaces_current_justification": "The stack targets currently supported runtimes and APIs: Python 3.10 through 3.13, all four in the CI matrix, with pgvector 0.3+/PostgreSQL 17 and current major versions of FastMCP, Pydantic v2, numpy and sentence-transformers. Deprecated interfaces are removed rather than wrapped \u2014 the standing rule is one-shot migrations with no back-compat shims (CLAUDE.md), and CI runs on the newest released Python so a deprecation surfaces as a warning in the build rather than as a surprise at end of life.",

"automated_integration_testing_status": "Met",
"automated_integration_testing_justification": "The full suite runs on every push and pull request to main via .github/workflows/ci.yml and reports success or failure per job: 6550 tests on Python 3.10-3.13 against PostgreSQL + pgvector, the same suite against the SQLite backend, the suite on Windows, and a Docker smoke job that boots the bare container and exercises the DB-less contract. tests_py/integration/ holds the database-backed integration tests specifically.",
"automated_integration_testing_justification": "The full suite runs on every push and pull request to main via .github/workflows/ci.yml and reports success or failure per job: 6565 tests on Python 3.10-3.13 against PostgreSQL + pgvector, the same suite against the SQLite backend, the suite on Windows, and a Docker smoke job that boots the bare container and exercises the DB-less contract. tests_py/integration/ holds the database-backed integration tests specifically.",

"regression_tests_added50_status": "Met",
"regression_tests_added50_justification": "Measured on 2026-07-27 over the merged pull requests titled as fixes in the preceding six months (2026-01-27 onward): 23 of 30 \u2014 76.7% \u2014 changed the test tree in the same PR, against the 50% this criterion asks for. (The measure counts a PR as carrying tests when it touches tests_py/ or tests_js/, which is a proxy for 'added a regression test'; the policy behind it is written down at https://github.com/cdeust/Cortex/blob/main/CONTRIBUTING.md#the-testing-policy-mandatory \u2014 a bug fix carries a regression test that fails on the pre-fix code.)",
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ adheres to [Semantic Versioning](https://semver.org/).
- **Coverage-guided fuzzing** (closes the Scorecard Fuzzing alert). Two harnesses in `fuzz/` over pure parsers that read untrusted text (§13.1 D2 — LLM-generated content is untrusted): the hand-rolled YAML frontmatter parser and the wiki source-path canonicaliser. Wired to **ClusterFuzzLite** (`.clusterfuzzlite/`, `.github/workflows/fuzz.yml`) — a 120s batch on PRs that blocks, and a longer scheduled run that does not, because a fuzzer left running will eventually find something and holding the merge queue hostage to an unrelated input makes the check ignored within a week. Writing the path harness **found a live bug**: `normalize_source_path` stripped `./` in a loop and then `/` exactly once, so removing the slashes could expose a `./` the loop had already walked past — `.//./x` came out as `./x`, still carrying the prefix the function exists to remove, and not idempotent. `extract_document_paths` dedupes on that result, so one document reachable by two spellings counted as two. Fixed by iterating to a fixed point; the four reproducers are committed as corpus inputs and **fail on the pre-fix code**. `fuzz/replay_corpus.py` runs every corpus input through its harness with no atheris, so the properties execute in the ordinary `pytest` suite on every platform — atheris publishes manylinux x86_64 wheels for cpython 3.12–3.14 and nothing else, and a property only one CI job can run is one that rots.

### Fixed
- **`sqlite_compat.py` relied on sqlite3's *implicit* default `datetime` adapter, deprecated as of Python 3.12** (#260), firing on 3 tests (`test_consolidate.py::test_with_memories`, `::test_protected_memories_skip_compression`, `test_memory_lifecycle.py::test_store_consolidate_recall`). Root cause: `cascade.py::_update_stage_entered` binds a raw `datetime.datetime` object as a SQL parameter instead of an ISO string — confirmed the **sole** such call site in this codebase by instrumenting all three `execute`/`executemany` paths in `sqlite_compat.py` and running the full suite against it. Fix: an explicit `sqlite3.register_adapter(datetime, _adapt_datetime_iso)` (the sanctioned Python-docs recipe the deprecation warning itself points to), writing the same "T"-separated `.isoformat()` spelling every other datetime write path here already produces (`sqlite_store._now_iso()`, etc.) — one canonical wire format instead of two. Old rows on disk (the deprecated adapter's space-separated spelling) keep reading correctly: `datetime.fromisoformat()` — the read path every consumer here uses — parses both spellings to an identical value (verified empirically and pinned by a test), so **no migration is required**. A `pyproject.toml` `filterwarnings` entry turns this specific DeprecationWarning into a hard failure going forward — a regression tripwire, not a silence. Boy-scout: `sqlite_compat.py` was already 335 lines — over this repo's 300-line file cap — before this change touched it; split the pure SQL-dialect translation logic (`_translate_sql`/`_returning_was_stripped`/`_SUPPORTS_RETURNING`) into a new `sqlite_sql_translate.py` module (behaviour-preserving, byte-identical logic), retargeting the two existing tests that monkeypatched `_SUPPORTS_RETURNING` to the module that actually defines it. Scoped mutation testing (mutmut) against the touched `sqlite_compat.py` surfaced 13 pre-existing gaps in `_CompatCursor`/`_CompatExecutingCursor`/`PsycopgCompatConnection` field wiring (not the datetime fix itself) — added targeted tests for all of them; one mutant (`executemany`'s `self.lastrowid = None`) is a documented equivalent (`sqlite3.Cursor.lastrowid` is only meaningful after a single-row `execute()` INSERT, so it is always `None` after `executemany()` regardless of which literal is written). A separate mutation gap in `_translate_sql` itself (20 survivors, verbatim regex-translation code moved unchanged from `sqlite_compat.py`, same tests before/after) predates this change and is filed as #265 rather than folded in, per §14.3.
- **A `created_at` that states a timezone was stored as the wrong instant** (#252). `normalize_date_to_iso` had no timezone policy on any of its paths, so three defects stacked: (1) the "already ISO" guard was the substring test `"T" in raw`, and every US zone abbreviation contains a T — `8 May 2023 13:56 EST` was returned unparsed; (2) the built-in fast path matches the date at the START of the string and discards the rest, so `8 May 2023 13:56 +02:00` became midnight, dropping both the time and the offset; (3) on the dateutil path an abbreviation it cannot resolve is dropped with a warning nobody sees, leaving a naive datetime that PostgreSQL's `timestamptz` cast and `compute_recency_boost` both read as UTC. The instant was up to a day off and nothing was emitted. A stated zone is now honoured or the value is refused: `mcp_server/core/temporal_timezones.py` supplies dateutil a `tzinfos` resolver over the **RFC 5322 §4.3 obs-zone table** (the normative answer to "which EST?" — cross-checked against CPython's `email._parseaddr._timezones`), and any abbreviation outside it is refused with a warning naming the input, the abbreviation and the fix, rather than defaulted. Parsing no longer depends on the host's local zone name, and no `warnings.catch_warnings()` — process-global and not thread-safe — is taken on a store write path. `normalize_date_to_iso` moves out of `core/temporal.py` into `core/temporal_normalize.py`: storage normalization must not lose precision, retrieval scoring may, and they are now separate modules (both stores import from the new path). The refusal also covers the degraded path — a string that states a zone is never salvaged to a naive date, whatever made the parse fail.
- **`python-dateutil` is now a declared dependency** (#252). `normalize_date_to_iso` has always parsed free-form dates with a time of day through it — the LoCoMo shape `1:56 pm on 8 May, 2023` its own docstring cites — but it was never declared and arrived only by transitive luck. It was absent from every `requirements/ci-*.txt`, so **every CI test job ran with that parser missing**: the fallback was dead code in CI, and any install resolving without it kept dates it could not read. Declared, locked and hash-pinned into the 9 exported requirement sets, so the write path behaves identically on every install and both backends.
- **Both stores skipped the normalization entirely for the dates that needed it most** (#252, the same substring defect one layer up). `PgMemoryStore._build_insert_params` and `SqliteMemoryStore._insert_memory_rows` each guarded the call with `"T" not in raw_created` as a cheap "is it already ISO?" test — so `8 May 2023 13:56 EST` (and every `PST`/`CST`/`MST` string) went to the database untouched, whichever way `normalize_date_to_iso` behaved. The guard is gone from both: deciding what is already ISO belongs to the function that owns it, which returns a real ISO datetime unchanged. Asserted on the stored row, not just on the parser, and asserted equal across the two backends.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ coding write gates, causal graphs, and intent-aware retrieval.
resolving from the `pyproject.toml` ranges instead gives you versions CI never
had (issue #253).
- Environment preflight: `python -m mcp_server.doctor` (backend-aware check list, fix message per check)
- Tests: `pytest` (full suite, 6550 tests) · `pytest tests_py/core/` (one layer) · `pytest --cov=mcp_server --cov-report=term-missing`
- Tests: `pytest` (full suite, 6565 tests) · `pytest tests_py/core/` (one layer) · `pytest --cov=mcp_server --cov-report=term-missing`
- Lint BEFORE every commit: `ruff check && ruff format --check` — the CI enforces **both**; passing only `ruff check` is not enough.
- Type gate (pyright, zero-diagnostic): resolve its environment from `uv.lock`
(`uv sync --no-default-groups --extra … --group typecheck`), never from the
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ bash scripts/setup.sh # macOS / Linux
# Verify everything is wired
uvx --python 3.13 --from "hypermnesia-mcp[postgresql]" cortex-doctor

# Run tests (6550 tests under tests_py/)
# Run tests (6565 tests under tests_py/)
pytest

# Run a benchmark
Expand Down Expand Up @@ -163,7 +163,7 @@ The full standard lives in
## Testing

```bash
pytest # full suite (6550 tests)
pytest # full suite (6565 tests)
pytest tests_py/core # core (pure business logic) only
pytest tests_py/integration # PostgreSQL-backed integration
pytest tests_py/benchmarks -k locomo # subset
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<a href="https://github.com/cdeust/Cortex/actions/workflows/ci.yml"><img src="https://github.com/cdeust/Cortex/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<a href="LICENSE"><img src="assets/badge-license.svg" alt="License: MIT"></a>
<img src="assets/badge-python.svg" alt="Python 3.10+">
<img src="assets/badge-tests.svg" alt="6550 tests passing">
<img src="assets/badge-tests.svg" alt="6565 tests passing">
<img src="assets/badge-references.svg" alt="97 referenced papers">
<img src="assets/badge-version.svg" alt="Version 4.16.0">
<a href="https://www.bestpractices.dev/projects/13836"><img src="https://www.bestpractices.dev/projects/13836/badge" alt="OpenSSF Best Practices"></a>
Expand Down Expand Up @@ -526,7 +526,7 @@ Cortex is **local-first**: your memories, conversations, and profiles stay on yo
## Development

```bash
pytest # 6550 tests
pytest # 6565 tests
ruff check . # Lint
ruff format --check . # Format
python scripts/check_doc_claims.py # advertised counts must match the repo
Expand Down
8 changes: 4 additions & 4 deletions assets/badge-tests.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/ASSURANCE-CASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ decoratively:

Standing analysis: CodeQL default setup (Python, JavaScript/TypeScript,
Actions) on every push and pull request plus weekly, currently 0 open alerts;
OpenSSF Scorecard via `.github/workflows/scorecard.yml`; and 6550 tests run on
OpenSSF Scorecard via `.github/workflows/scorecard.yml`; and 6565 tests run on
four Python versions, two backends and Windows.

## 6. What this assurance case does NOT claim
Expand Down
Loading