diff --git a/docs/evidence/issue-1773-dev-tool-archive-bindings/README.md b/docs/evidence/issue-1773-dev-tool-archive-bindings/README.md new file mode 100644 index 00000000..1f76dfe6 --- /dev/null +++ b/docs/evidence/issue-1773-dev-tool-archive-bindings/README.md @@ -0,0 +1,15 @@ +# Reviewed dashboard validation successor for dev-tool pins + +Existing generated dependency delivery1772 atadafa19c393197ea2ea8ad9c540200063b1edbd6 changes Black26.10.0→26.10.1 and Ruff0.16.10→0.17.0. The historical review-recovery manifest pinned the entire then-current pyproject byte sequence, so these legitimate tool-only updates failed its current-input check. The authentic generated pyproject is retained with GitHub blob/head/SHA256/length provenance. Original checker:1FAIL; successor checker on the exact same bytes:1PASS. + +The original issue1750 archives and manifests remain byte-identical. Historical pyproject and validator snapshots match their original manifest hashes. The successor binds the original manifest and new reviewed validator hash separately. All four product/replay inputs retain their exact historical current-input bindings. Current project semantics must match the historical input except the version values of exactly pinned Black/Ruff requirements; names/extras/markers, every other dependency and all other project/tool settings remain checked. Non-exact/wildcard formatter pins, unrelated project/dependency changes, or changed product/validator bytes fail. + +Current main pyproject is unchanged. This does not rewrite historical experiments or claim that they used newer tool versions. The successor is a newly reviewed validation contract for the existing delivery prerequisite. + +Validation:8focused successor controlsPASS; full dashboard evidence/replay/loader suite47PASS, including actual ten intended assertion RED/restored GREEN replay pairs. Original archive hash2204105d4a9239531d5d7c65c49fba86ce551e9d714d47e4d2f97c965e434dd1 verified unchanged. Black/Ruff/diff checks pass. + +Commands: `/opt/anaconda3/bin/python3 -m pytest tests/test_dashboard_read_failure_evidence.py tests/test_dashboard_read_failure_replay.py tests/test_dashboard_loader_failures.py -q -o addopts=`. To replay the generated-input contrast, save main pyproject bytes, substitute only the retained authenticated generated pyproject, run the named recovery test, and restore the original bytes. Do not edit generated branch1772. + +Hosted current-head CI/review, guarded source delivery, actual verification and source-owned Maint52/71 regeneration remain separate gates. + +Hosted sourcePR1774/run38825093631 correctly rejected the newly imported transitive `packaging` dependency as undeclared. The correction uses only stdlib `re`/`tomllib` and the bounded plain numeric Black/Ruff release-pin syntax. Extras, markers and alternate operators are not allowed changes to the historical formatter requirement. No pyproject or lock edit is needed. Dependency verificationPASS, focused9PASS, full dashboard evidence/replay/loader48PASS. Current validator binding refreshed to the reviewed new checker bytes; original archive/manifest/snapshots unchanged. diff --git a/docs/evidence/issue-1773-dev-tool-archive-bindings/current-bindings.json b/docs/evidence/issue-1773-dev-tool-archive-bindings/current-bindings.json new file mode 100644 index 00000000..22542d9d --- /dev/null +++ b/docs/evidence/issue-1773-dev-tool-archive-bindings/current-bindings.json @@ -0,0 +1,5 @@ +{ + "schema": "dashboard-review-recovery-successor/v1", + "historical_manifest_sha256": "c08ee51933e9c53f6528a8d0731fc94fbf3e153b4f0a356cab58a2ea21d517a6", + "current_validator_sha256": "0dae7c5d64c320edd88fd5724c69becf5767f31555928409bdb2b897ce4efd34" +} diff --git a/docs/evidence/issue-1773-dev-tool-archive-bindings/fixture-derived-green.log b/docs/evidence/issue-1773-dev-tool-archive-bindings/fixture-derived-green.log new file mode 100644 index 00000000..0f15b711 --- /dev/null +++ b/docs/evidence/issue-1773-dev-tool-archive-bindings/fixture-derived-green.log @@ -0,0 +1,53 @@ +............................. [100%] +=============================== warnings summary =============================== +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/sklearn/utils/_param_validation.py:11 + /opt/anaconda3/lib/python3.12/site-packages/sklearn/utils/_param_validation.py:11: UserWarning: A NumPy version >=2.0.0 and <2.8.0 is required for this version of SciPy (detected version 1.26.4) + from scipy.sparse import csr_matrix, issparse + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/scipy/sparse/_sputils.py:17 + /opt/anaconda3/lib/python3.12/site-packages/scipy/sparse/_sputils.py:17: FutureWarning: In the future `np.long` will be defined as the corresponding NumPy scalar. + np.uintc, np.long, np.ulong, np.longlong, np.ulonglong, + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/langsmith/evaluation/_key_extraction.py:20 + /opt/anaconda3/lib/python3.12/site-packages/langsmith/evaluation/_key_extraction.py:20: DeprecationWarning: ast.Str is deprecated and will be removed in Python 3.14; use ast.Constant instead + (ast.Str, ast.Constant) if hasattr(ast, "Str") else (ast.Constant,) + +api/chat.py:1501 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1501: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("startup") + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 + /opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + return self.router.on_event(event_type) # ty: ignore[deprecated] + +api/chat.py:1801 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1801: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("shutdown") + +api/chat.py:1808 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1808: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("shutdown") + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html diff --git a/docs/evidence/issue-1773-dev-tool-archive-bindings/full-dashboard-green.log b/docs/evidence/issue-1773-dev-tool-archive-bindings/full-dashboard-green.log new file mode 100644 index 00000000..dcafc430 --- /dev/null +++ b/docs/evidence/issue-1773-dev-tool-archive-bindings/full-dashboard-green.log @@ -0,0 +1,62 @@ +............................................... [100%] +=============================== warnings summary =============================== +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/sklearn/utils/_param_validation.py:11 + /opt/anaconda3/lib/python3.12/site-packages/sklearn/utils/_param_validation.py:11: UserWarning: A NumPy version >=2.0.0 and <2.8.0 is required for this version of SciPy (detected version 1.26.4) + from scipy.sparse import csr_matrix, issparse + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/scipy/sparse/_sputils.py:17 + /opt/anaconda3/lib/python3.12/site-packages/scipy/sparse/_sputils.py:17: FutureWarning: In the future `np.long` will be defined as the corresponding NumPy scalar. + np.uintc, np.long, np.ulong, np.longlong, np.ulonglong, + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/langsmith/evaluation/_key_extraction.py:20 + /opt/anaconda3/lib/python3.12/site-packages/langsmith/evaluation/_key_extraction.py:20: DeprecationWarning: ast.Str is deprecated and will be removed in Python 3.14; use ast.Constant instead + (ast.Str, ast.Constant) if hasattr(ast, "Str") else (ast.Constant,) + +api/chat.py:1501 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1501: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("startup") + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 + /opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + return self.router.on_event(event_type) # ty: ignore[deprecated] + +api/chat.py:1801 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1801: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("shutdown") + +api/chat.py:1808 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1808: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("shutdown") + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/pandas/core/computation/expressions.py:23 + /opt/anaconda3/lib/python3.12/site-packages/pandas/core/computation/expressions.py:23: UserWarning: Pandas requires version '2.10.2' or newer of 'numexpr' (version '2.8.7' currently installed). + from pandas.core.computation.check import NUMEXPR_INSTALLED + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/pandas/core/arrays/masked.py:56 + /opt/anaconda3/lib/python3.12/site-packages/pandas/core/arrays/masked.py:56: UserWarning: Pandas requires version '1.4.2' or newer of 'bottleneck' (version '1.3.7' currently installed). + from pandas.core import ( + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +47 passed, 11 warnings in 136.58s (0:02:16) diff --git a/docs/evidence/issue-1773-dev-tool-archive-bindings/generated-input-original-red.log b/docs/evidence/issue-1773-dev-tool-archive-bindings/generated-input-original-red.log new file mode 100644 index 00000000..b07b8437 --- /dev/null +++ b/docs/evidence/issue-1773-dev-tool-archive-bindings/generated-input-original-red.log @@ -0,0 +1,91 @@ +F [100%] +=================================== FAILURES =================================== +________ test_dashboard_review_recovery_archive_and_six_current_inputs _________ + + def test_dashboard_review_recovery_archive_and_six_current_inputs(): + recovery = EVIDENCE / "review-recovery-20261012" + manifest = json.loads((recovery / "manifest.json").read_text()) + proof = recovery / "proof.tar.gz" + assert digest(proof.read_bytes()) == manifest["archive_sha256"] + with tarfile.open(proof) as bundle: + members = bundle.getmembers() + assert len(members) == len(manifest["members"]) == 88 + assert all(member.isfile() for member in members) + assert len({member.name for member in members}) == len(members) + assert {member.name for member in members} == set(manifest["members"]) + for member in members: + raw = bundle.extractfile(member).read() + assert len(raw) == manifest["members"][member.name]["bytes"], member.name + assert digest(raw) == manifest["members"][member.name]["sha256"], member.name + assert set(manifest["current_inputs"]) == { + SOURCE, + TEST, + DRIVER, + "tests/test_dashboard_read_failure_replay.py", + "tests/test_dashboard_read_failure_evidence.py", + "pyproject.toml", + } + for name, expected in manifest["current_inputs"].items(): +> assert digest((ROOT / name).read_bytes()) == expected, name +E AssertionError: pyproject.toml +E assert '90a215cb26ba...bc385e8cff2e3' == '8069f8a24b79...788cb94dd5c4f' +E +E - 8069f8a24b7995a866613454ae2f2268cf601c9b5a1fcbab3c0788cb94dd5c4f +E + 90a215cb26ba3a90a88e88c6684678ba4ab5897b666cf7935eebc385e8cff2e3 + +tests/test_dashboard_read_failure_evidence.py:118: AssertionError +=============================== warnings summary =============================== +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/sklearn/utils/_param_validation.py:11 + /opt/anaconda3/lib/python3.12/site-packages/sklearn/utils/_param_validation.py:11: UserWarning: A NumPy version >=2.0.0 and <2.8.0 is required for this version of SciPy (detected version 1.26.4) + from scipy.sparse import csr_matrix, issparse + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/scipy/sparse/_sputils.py:17 + /opt/anaconda3/lib/python3.12/site-packages/scipy/sparse/_sputils.py:17: FutureWarning: In the future `np.long` will be defined as the corresponding NumPy scalar. + np.uintc, np.long, np.ulong, np.longlong, np.ulonglong, + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/langsmith/evaluation/_key_extraction.py:20 + /opt/anaconda3/lib/python3.12/site-packages/langsmith/evaluation/_key_extraction.py:20: DeprecationWarning: ast.Str is deprecated and will be removed in Python 3.14; use ast.Constant instead + (ast.Str, ast.Constant) if hasattr(ast, "Str") else (ast.Constant,) + +api/chat.py:1501 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1501: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("startup") + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 + /opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + return self.router.on_event(event_type) # ty: ignore[deprecated] + +api/chat.py:1801 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1801: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("shutdown") + +api/chat.py:1808 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1808: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("shutdown") + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +=========================== short test summary info ============================ +FAILED tests/test_dashboard_read_failure_evidence.py::test_dashboard_review_recovery_archive_and_six_current_inputs +1 failed, 9 warnings in 0.22s diff --git a/docs/evidence/issue-1773-dev-tool-archive-bindings/generated-input-successor-green.log b/docs/evidence/issue-1773-dev-tool-archive-bindings/generated-input-successor-green.log new file mode 100644 index 00000000..318341c5 --- /dev/null +++ b/docs/evidence/issue-1773-dev-tool-archive-bindings/generated-input-successor-green.log @@ -0,0 +1,54 @@ +. [100%] +=============================== warnings summary =============================== +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/sklearn/utils/_param_validation.py:11 + /opt/anaconda3/lib/python3.12/site-packages/sklearn/utils/_param_validation.py:11: UserWarning: A NumPy version >=2.0.0 and <2.8.0 is required for this version of SciPy (detected version 1.26.4) + from scipy.sparse import csr_matrix, issparse + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/scipy/sparse/_sputils.py:17 + /opt/anaconda3/lib/python3.12/site-packages/scipy/sparse/_sputils.py:17: FutureWarning: In the future `np.long` will be defined as the corresponding NumPy scalar. + np.uintc, np.long, np.ulong, np.longlong, np.ulonglong, + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/langsmith/evaluation/_key_extraction.py:20 + /opt/anaconda3/lib/python3.12/site-packages/langsmith/evaluation/_key_extraction.py:20: DeprecationWarning: ast.Str is deprecated and will be removed in Python 3.14; use ast.Constant instead + (ast.Str, ast.Constant) if hasattr(ast, "Str") else (ast.Constant,) + +api/chat.py:1501 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1501: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("startup") + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 + /opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + return self.router.on_event(event_type) # ty: ignore[deprecated] + +api/chat.py:1801 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1801: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("shutdown") + +api/chat.py:1808 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1808: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("shutdown") + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +1 passed, 9 warnings in 0.19s diff --git a/docs/evidence/issue-1773-dev-tool-archive-bindings/generated-provenance.json b/docs/evidence/issue-1773-dev-tool-archive-bindings/generated-provenance.json new file mode 100644 index 00000000..07d58cd3 --- /dev/null +++ b/docs/evidence/issue-1773-dev-tool-archive-bindings/generated-provenance.json @@ -0,0 +1,8 @@ +{ + "repository": "stranske/Manager-Database", + "head": "adafa19c393197ea2ea8ad9c540200063b1edbd6", + "path": "pyproject.toml", + "git_blob": "26116b6e26930352f603f1131d1e75d3935653b7", + "sha256": "90a215cb26ba3a90a88e88c6684678ba4ab5897b666cf7935eebc385e8cff2e3", + "bytes": 4245 +} diff --git a/docs/evidence/issue-1773-dev-tool-archive-bindings/generated-pyproject.toml b/docs/evidence/issue-1773-dev-tool-archive-bindings/generated-pyproject.toml new file mode 100644 index 00000000..26116b6e --- /dev/null +++ b/docs/evidence/issue-1773-dev-tool-archive-bindings/generated-pyproject.toml @@ -0,0 +1,151 @@ +[project] +name = "manager-database" +version = "0.1.0" +description = "Manager Database Application" +readme = "README.md" +license = { text = "MIT" } +requires-python = ">=3.12" +authors = [{ name = "stranske" }] + +dependencies = [ + "alembic", + "edgartools>=5.44,<5.45", + "httpx", + "feedparser", + "prefect", + "jsonschema>=4.26.0,<4.27.0", + "beautifulsoup4", + "boto3", + "python-json-logger", + "psycopg[binary]", + "streamlit==1.60.0", + "pandas", + "streamlit-authenticator==0.4.2", + "altair", + # Prefect requires newer FastAPI; avoid conflicting pin. + "fastapi", + "uvicorn", + "stranske-pdf-extract[baseline] @ git+https://github.com/stranske/Workflows.git@pdf-extract-v0.1.0#subdirectory=packages/stranske_pdf_extract", + "langchain>=1.2,<2.0", + "langchain-core>=1.2,<2.0", + "langchain-openai>=0.3,<2.0", + "langchain-anthropic>=0.3,<2.0", + "langsmith>=0.7.30,<1.0", +] + +[project.scripts] +# Internal/local launch for the analyst UI shell (see ui/launch.py and `make app`). +mgrdb-app = "ui.launch:main" + +[project.optional-dependencies] +app = [] +dev = [ + "extra-streamlit-components==0.1.81", + "starlette", + "pytest==9.1.1", + "pytest-asyncio", + "pytest-cov==7.1.0", + "black==26.10.1", + "ruff==0.17.0", + "pre-commit", + "mypy==2.4.0", + "langchain>=1.2,<2.0", + "langchain-core>=1.2,<2.0", + "langchain-openai>=0.3,<2.0", + "langchain-anthropic>=0.3,<2.0", + "langsmith>=0.7.30,<1.0", + "types-PyYAML", + "coverage==7.16.2", + "defusedxml==0.7.1", + "referencing", + "PyYAML", + "fakeredis", + # Shared app-behavior baseline harness (api-snapshot kit under tests/baseline). + # Pattern A: unpinned @main, excluded from requirements.lock via the + # [tool.uv.pip] no-emit-package below. See docs/baseline-kit-dependency.md. + "app-baseline-kit @ git+https://github.com/stranske/Workflows.git#subdirectory=packages/app-baseline-kit", + # Transitive dep of app-baseline-kit; declared explicitly so the lock-presence + # test (tests/test_dependency_version_alignment.py) is satisfied. + "pytest-regressions>=2.8", + "sqlalchemy", +] + +[build-system] +requires = ["setuptools>=82.0.1"] +build-backend = "setuptools.build_meta" + +# Do not freeze a commit SHA for the monorepo app-baseline-kit package in +# requirements.lock. Pattern A: the unpinned @main pyproject URL and the lock +# must never disagree, or uv aborts on a cold-cache resolve with "conflicting +# URLs for package app-baseline-kit". See docs/baseline-kit-dependency.md. +[tool.uv.pip] +no-emit-package = ["app-baseline-kit"] + +# Explicit package discovery for flat layout with multiple top-level packages +[tool.setuptools.packages.find] +include = ["adapters*", "alerts*", "api*", "etl*", "llm*", "ui*"] + +[tool.pytest.ini_options] +addopts = "-q" +pythonpath = ["."] +markers = [ + "nightly: nightly regression tests", + "integration: integration tests", + "golden: offline credential-free golden/threshold gate runs", +] + +[tool.black] +line-length = 100 +target-version = ["py312"] +workers = 1 +force-exclude = "(^|.*/)(\\.git|\\.hg|\\.mypy_cache|\\.nox|\\.tox|\\.venv|_build|buck-out|build|dist|node_modules|\\.workflows-lib)(/|$)" + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "W", "F", "I", "B", "C4", "UP"] +ignore = ["E501"] + +[tool.mypy] +python_version = "3.12" +warn_return_any = false +warn_unused_ignores = false +ignore_missing_imports = true +check_untyped_defs = false +disallow_untyped_defs = false +exclude = "(^|.*/)(scripts|etl|adapters|api)/|(^|.*/)embeddings\\.py$" + +[tool.coverage.run] +source = ["adapters", "api", "etl", "ui"] +branch = true +omit = ["*/tests/*", "*/__pycache__/*"] + +[tool.coverage.report] +fail_under = 75 +show_missing = true + +[[tool.mypy.overrides]] +module = ["adapters.*", "api.*", "etl.*", "embeddings"] +ignore_errors = true + +[[tool.mypy.overrides]] +module = ["scripts.langchain.*"] +ignore_errors = true + +[[tool.mypy.overrides]] +module = ["tests.*"] +disable_error_code = ["attr-defined"] + +[[tool.mypy.overrides]] +module = [ + "semantic_matcher", + "label_matcher", + "issue_dedup", + "integration_layer", + "issue_formatter", + "task_validator", + "injection_guard", +] +ignore_errors = true diff --git a/docs/evidence/issue-1773-dev-tool-archive-bindings/historical-pyproject.toml b/docs/evidence/issue-1773-dev-tool-archive-bindings/historical-pyproject.toml new file mode 100644 index 00000000..57847461 --- /dev/null +++ b/docs/evidence/issue-1773-dev-tool-archive-bindings/historical-pyproject.toml @@ -0,0 +1,151 @@ +[project] +name = "manager-database" +version = "0.1.0" +description = "Manager Database Application" +readme = "README.md" +license = { text = "MIT" } +requires-python = ">=3.12" +authors = [{ name = "stranske" }] + +dependencies = [ + "alembic", + "edgartools>=5.44,<5.45", + "httpx", + "feedparser", + "prefect", + "jsonschema>=4.26.0,<4.27.0", + "beautifulsoup4", + "boto3", + "python-json-logger", + "psycopg[binary]", + "streamlit==1.60.0", + "pandas", + "streamlit-authenticator==0.4.2", + "altair", + # Prefect requires newer FastAPI; avoid conflicting pin. + "fastapi", + "uvicorn", + "stranske-pdf-extract[baseline] @ git+https://github.com/stranske/Workflows.git@pdf-extract-v0.1.0#subdirectory=packages/stranske_pdf_extract", + "langchain>=1.2,<2.0", + "langchain-core>=1.2,<2.0", + "langchain-openai>=0.3,<2.0", + "langchain-anthropic>=0.3,<2.0", + "langsmith>=0.7.30,<1.0", +] + +[project.scripts] +# Internal/local launch for the analyst UI shell (see ui/launch.py and `make app`). +mgrdb-app = "ui.launch:main" + +[project.optional-dependencies] +app = [] +dev = [ + "extra-streamlit-components==0.1.81", + "starlette", + "pytest==9.1.1", + "pytest-asyncio", + "pytest-cov==7.1.0", + "black==26.10.0", + "ruff==0.16.10", + "pre-commit", + "mypy==2.4.0", + "langchain>=1.2,<2.0", + "langchain-core>=1.2,<2.0", + "langchain-openai>=0.3,<2.0", + "langchain-anthropic>=0.3,<2.0", + "langsmith>=0.7.30,<1.0", + "types-PyYAML", + "coverage==7.16.2", + "defusedxml==0.7.1", + "referencing", + "PyYAML", + "fakeredis", + # Shared app-behavior baseline harness (api-snapshot kit under tests/baseline). + # Pattern A: unpinned @main, excluded from requirements.lock via the + # [tool.uv.pip] no-emit-package below. See docs/baseline-kit-dependency.md. + "app-baseline-kit @ git+https://github.com/stranske/Workflows.git#subdirectory=packages/app-baseline-kit", + # Transitive dep of app-baseline-kit; declared explicitly so the lock-presence + # test (tests/test_dependency_version_alignment.py) is satisfied. + "pytest-regressions>=2.8", + "sqlalchemy", +] + +[build-system] +requires = ["setuptools>=82.0.1"] +build-backend = "setuptools.build_meta" + +# Do not freeze a commit SHA for the monorepo app-baseline-kit package in +# requirements.lock. Pattern A: the unpinned @main pyproject URL and the lock +# must never disagree, or uv aborts on a cold-cache resolve with "conflicting +# URLs for package app-baseline-kit". See docs/baseline-kit-dependency.md. +[tool.uv.pip] +no-emit-package = ["app-baseline-kit"] + +# Explicit package discovery for flat layout with multiple top-level packages +[tool.setuptools.packages.find] +include = ["adapters*", "alerts*", "api*", "etl*", "llm*", "ui*"] + +[tool.pytest.ini_options] +addopts = "-q" +pythonpath = ["."] +markers = [ + "nightly: nightly regression tests", + "integration: integration tests", + "golden: offline credential-free golden/threshold gate runs", +] + +[tool.black] +line-length = 100 +target-version = ["py312"] +workers = 1 +force-exclude = "(^|.*/)(\\.git|\\.hg|\\.mypy_cache|\\.nox|\\.tox|\\.venv|_build|buck-out|build|dist|node_modules|\\.workflows-lib)(/|$)" + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "W", "F", "I", "B", "C4", "UP"] +ignore = ["E501"] + +[tool.mypy] +python_version = "3.12" +warn_return_any = false +warn_unused_ignores = false +ignore_missing_imports = true +check_untyped_defs = false +disallow_untyped_defs = false +exclude = "(^|.*/)(scripts|etl|adapters|api)/|(^|.*/)embeddings\\.py$" + +[tool.coverage.run] +source = ["adapters", "api", "etl", "ui"] +branch = true +omit = ["*/tests/*", "*/__pycache__/*"] + +[tool.coverage.report] +fail_under = 75 +show_missing = true + +[[tool.mypy.overrides]] +module = ["adapters.*", "api.*", "etl.*", "embeddings"] +ignore_errors = true + +[[tool.mypy.overrides]] +module = ["scripts.langchain.*"] +ignore_errors = true + +[[tool.mypy.overrides]] +module = ["tests.*"] +disable_error_code = ["attr-defined"] + +[[tool.mypy.overrides]] +module = [ + "semantic_matcher", + "label_matcher", + "issue_dedup", + "integration_layer", + "issue_formatter", + "task_validator", + "injection_guard", +] +ignore_errors = true diff --git a/docs/evidence/issue-1773-dev-tool-archive-bindings/historical-validator.py b/docs/evidence/issue-1773-dev-tool-archive-bindings/historical-validator.py new file mode 100644 index 00000000..9798d04a --- /dev/null +++ b/docs/evidence/issue-1773-dev-tool-archive-bindings/historical-validator.py @@ -0,0 +1,299 @@ +"""Check dashboard acceptance claims against immutable raw experiment receipts.""" + +import ast +import hashlib +import json +import subprocess +import sys +import tarfile +from collections import Counter +from pathlib import Path + +import pytest +from defusedxml import ElementTree as ET + +ROOT = Path(__file__).resolve().parents[1] +EVIDENCE = ROOT / "docs/evidence/issue-1750-dashboard-read-failures" +SOURCE = "ui/dashboard.py" +TEST = "tests/test_dashboard_loader_failures.py" +DRIVER = "docs/evidence/issue-1750-dashboard-read-failures/dashboard_read_failure_replay.py" +SQL = ("load_filing_timeline", "load_latest_holdings_snapshot", "load_top_deltas") +DOMAIN = ( + "load_manager_activism_filings", + "load_manager_activism_events", + "load_manager_activism_timeline", + "load_active_campaigns_summary", + "load_manager_conviction_scores", + "load_manager_crowded_trades", + "load_manager_contrarian_signals", +) + + +def digest(raw): + return hashlib.sha256(raw).hexdigest() + + +def case_name(loader): + test = ( + "test_sql_loader_returns_typed_empty_frame_and_closes_missing_schema" + if loader in SQL + else "test_domain_loader_returns_empty_and_closes_after_query_error" + ) + return f"{test}[{loader}]" + + +def outcomes(raw): + result = {} + for case in ET.fromstring(raw).iter("testcase"): + node = f"{case.get('classname')}::{case.get('name')}" + assert node not in result, f"Duplicate test node: {node}" + assert case.find("error") is None, f"Test error: {node}" + failure, skip = case.find("failure"), case.find("skipped") + assert failure is None or skip is None, f"Ambiguous outcome: {node}" + result[node] = ( + "failure" if failure is not None else "skipped" if skip is not None else "passed" + ) + return result + + +@pytest.fixture(scope="module") +def archive(): + with tarfile.open(EVIDENCE / "validation.tar.gz") as bundle: + members = bundle.getmembers() + assert len(members) == 96 + assert all(member.isfile() for member in members) + assert len({member.name for member in members}) == len(members) + return {member.name: bundle.extractfile(member).read() for member in members} + + +def test_dashboard_archive_and_current_source_bindings(archive): + manifest = json.loads((EVIDENCE / "manifest.json").read_text()) + assert digest((EVIDENCE / "validation.tar.gz").read_bytes()) == manifest["archive_sha256"] + assert set(archive) == set(manifest["members"]) + for name, raw in archive.items(): + assert digest(raw) == manifest["members"][name], name + assert set(manifest["current_bindings"]) == {SOURCE, TEST, DRIVER} + for name, expected in manifest["current_bindings"].items(): + assert digest((ROOT / name).read_bytes()) == expected, name + for name, expected in manifest["historical_bindings"].items(): + assert digest(archive[f"source/{name}"]) == expected, name + if name in (SOURCE, TEST): + assert archive[f"source/{name}"] == (ROOT / name).read_bytes(), name + proof = json.loads(archive["mutations/controls.json"]) + assert proof["caller_identity"] == { + name: manifest["current_bindings"][name] for name in (SOURCE, TEST) + } + assert ( + proof["replay_sha256"] + == manifest["historical_bindings"][ + "docs/evidence/issue-1750-dashboard-read-failures/replay.py" + ] + ) + + +def test_dashboard_review_recovery_archive_and_six_current_inputs(): + recovery = EVIDENCE / "review-recovery-20261012" + manifest = json.loads((recovery / "manifest.json").read_text()) + proof = recovery / "proof.tar.gz" + assert digest(proof.read_bytes()) == manifest["archive_sha256"] + with tarfile.open(proof) as bundle: + members = bundle.getmembers() + assert len(members) == len(manifest["members"]) == 88 + assert all(member.isfile() for member in members) + assert len({member.name for member in members}) == len(members) + assert {member.name for member in members} == set(manifest["members"]) + for member in members: + raw = bundle.extractfile(member).read() + assert len(raw) == manifest["members"][member.name]["bytes"], member.name + assert digest(raw) == manifest["members"][member.name]["sha256"], member.name + assert set(manifest["current_inputs"]) == { + SOURCE, + TEST, + DRIVER, + "tests/test_dashboard_read_failure_replay.py", + "tests/test_dashboard_read_failure_evidence.py", + "pyproject.toml", + } + for name, expected in manifest["current_inputs"].items(): + assert digest((ROOT / name).read_bytes()) == expected, name + + +@pytest.mark.parametrize("loader", SQL + DOMAIN) +def test_dashboard_ten_intended_red_and_restored_green_pairs(archive, loader): + controls = json.loads(archive["mutations/controls.json"])["controls"] + expected_nodes = {f"{TEST}::{case_name(name)}" for name in SQL + DOMAIN} + assert len(controls) == 10 + assert {control["node"] for control in controls} == expected_nodes + control = next( + control for control in controls if control["node"] == f"{TEST}::{case_name(loader)}" + ) + original = archive[f"mutations/{loader}-original.py"] + mutant = archive[f"mutations/{loader}-mutant.py"] + assert original == archive[f"source/{SOURCE}"] + assert digest(original) == control["original_sha256"] == control["restored_sha256"] + assert digest(mutant) == control["mutant_sha256"] != digest(original) + + # Independently limit each mutation to the selected production error fallback. + function = next( + node + for node in ast.parse(original).body + if isinstance(node, ast.FunctionDef) and node.name == loader + ) + handlers = [node for node in ast.walk(function) if isinstance(node, ast.ExceptHandler)] + assert len(handlers) == 1 + fallback = handlers[0].body[0] + assert isinstance(fallback, ast.Assign) + replacement = ( + 'df = pd.DataFrame({"unexpected": [1]})' + if loader in SQL + else 'return pd.DataFrame({"unexpected": [1]})' + ) + lines = original.decode().splitlines(keepends=True) + expected_mutant = ( + "".join(lines[: fallback.lineno - 1]) + + " " * fallback.col_offset + + replacement + + "\n" + + "".join(lines[fallback.end_lineno :]) + ).encode() + assert mutant == expected_mutant + assert control["red"]["cwd"] == control["green"]["cwd"] + for phase, expected_exit, status in (("red", 1, "failure"), ("green", 0, "passed")): + prefix = f"mutations/{loader}-{phase}" + receipt = json.loads(archive[f"{prefix}.json"]) + assert receipt == control[phase] + assert receipt["exit"] == expected_exit + assert receipt["argv"][1:4] == ["-m", "pytest", control["node"]] + xml_arg = next(arg for arg in receipt["argv"] if arg.startswith("--junitxml=")) + assert Path(xml_arg.split("=", 1)[1]).name == f"{loader}-{phase}.xml" + xml = archive[f"{prefix}.xml"] + assert outcomes(xml) == { + f"tests.test_dashboard_loader_failures::{case_name(loader)}": status + } + suite = ET.fromstring(xml).find("testsuite") + assert {key: suite.get(key) for key in ("tests", "errors", "failures", "skipped")} == { + "tests": "1", + "errors": "0", + "failures": str(expected_exit), + "skipped": "0", + } + if phase == "red": + failure = suite.find("testcase/failure") + assert failure.get("message", "").startswith("assert False") + assert "> assert result.empty" in failure.text + assert "AssertionError" in failure.text + assert archive[f"{prefix}.log"], "Missing raw pytest output" + + +def test_dashboard_current_replay_proves_ten_pairs_and_refuses_output_reuse(tmp_path): + """Exercise the real driver and production mutants, independently of saved receipts.""" + bound_files = (SOURCE, TEST, DRIVER) + before = {name: (ROOT / name).read_bytes() for name in bound_files} + output = tmp_path / "fresh-proof" + argv = [sys.executable, str(ROOT / DRIVER), "--output", str(output)] + run = subprocess.run(argv, cwd=ROOT, capture_output=True, text=True, timeout=300, check=False) + assert run.returncode == 0, run.stdout + run.stderr + assert "ten intended assertion RED1/restored GREEN0 pairs" in run.stdout + receipts = {path.name: path.read_bytes() for path in output.iterdir()} + fresh = {f"mutations/{name}": raw for name, raw in receipts.items()} + fresh[f"source/{SOURCE}"] = before[SOURCE] + controls = json.loads(receipts["controls.json"]) + assert controls["caller_identity"] == {name: digest(before[name]) for name in (SOURCE, TEST)} + assert controls["replay_sha256"] == digest(before[DRIVER]) + for loader in SQL + DOMAIN: + test_dashboard_ten_intended_red_and_restored_green_pairs(fresh, loader) + + reuse = subprocess.run(argv, cwd=ROOT, capture_output=True, text=True, timeout=30, check=False) + assert reuse.returncode == 1 + assert "FileExistsError" in reuse.stderr + assert "PASS:" not in reuse.stdout + assert {path.name: path.read_bytes() for path in output.iterdir()} == receipts + assert {name: (ROOT / name).read_bytes() for name in bound_files} == before + + +def test_dashboard_archived_full_suites_preserve_all_old_outcomes(archive): + comparison = json.loads((EVIDENCE / "comparison.json").read_text()) + baseline = outcomes(archive["md-baseline-junit.xml"]) + candidate = outcomes(archive["md-candidate-junit.xml"]) + assert set(baseline) <= set(candidate) + assert {node: candidate[node] for node in baseline} == baseline + added = {node: status for node, status in candidate.items() if node not in baseline} + assert ( + added + == comparison["new_nodes"] + == { + f"tests.test_dashboard_loader_failures::{case_name(loader)}": "passed" + for loader in SQL + DOMAIN + } + ) + assert comparison["old_outcome_changes"] == {} + for phase, results, passed in (("baseline", baseline, 1959), ("candidate", candidate, 1969)): + assert ( + Counter(results.values()) + == comparison[f"{phase}_outcomes"] + == { + "passed": passed, + "failure": 24, + "skipped": 21, + } + ) + command = json.loads(archive[f"md-{phase}-command.json"]) + assert command["exit"] == 1 + assert command["argv"][1:5] == ["-m", "pytest", "tests", "-q"] + assert "--cov" in command["argv"] + assert command["checkout_base"] == comparison["base_head"] + assert all( + node.startswith("tests.test_ui_auth::") + for node, status in baseline.items() + if status == "failure" + ) + + +def test_dashboard_archived_statement_coverage_preserves_every_old_line(archive): + comparison = json.loads((EVIDENCE / "comparison.json").read_text()) + baseline = json.loads(archive["md-baseline-coverage.json"]) + candidate = json.loads(archive["md-candidate-coverage.json"]) + assert set(baseline["files"]) == set(candidate["files"]) + assert len(candidate["files"]) == comparison["source_file_count"] == 117 + assert comparison["identical_source_universe"] is True + assert comparison["same_exclusions"] is True + assert comparison["covered_line_regressions"] == {} + assert comparison["coverage_branch_measured"] is False + gained = {} + for name, old in baseline["files"].items(): + new = candidate["files"][name] + assert set(old["executed_lines"]) <= set(new["executed_lines"]), name + assert old["excluded_lines"] == new["excluded_lines"], name + assert set(old["executed_lines"] + old["missing_lines"]) == set( + new["executed_lines"] + new["missing_lines"] + ), name + added = set(new["executed_lines"]) - set(old["executed_lines"]) + if added: + gained[name] = added + assert set(gained) == {SOURCE} + assert len(gained[SOURCE]) == 20 + for phase, data, covered, percent in ( + ("baseline", baseline, 12447, 86.98721085), + ("candidate", candidate, 12467, 87.12698302), + ): + assert data["meta"]["branch_coverage"] is False + totals = data["totals"] + assert totals == comparison[phase] + assert totals["covered_lines"] == covered + assert totals["num_statements"] == 14309 + assert totals["excluded_lines"] == 82 + assert round(totals["percent_covered"], 8) == percent + assert totals["percent_covered"] == pytest.approx(100 * covered / 14309) + assert sum(len(file["executed_lines"]) for file in data["files"].values()) == covered + assert sum(len(file["missing_lines"]) for file in data["files"].values()) == 14309 - covered + assert sum(len(file["excluded_lines"]) for file in data["files"].values()) == 82 + + +def test_dashboard_archived_reuse_refusal_and_black_receipt(archive): + refusal = json.loads(archive["md-reuse.json"]) + assert refusal["exit"] == 1 + assert "FileExistsError" in refusal["stderr"] + assert "output.mkdir(parents=True, exist_ok=False)" in refusal["stderr"] + assert refusal["original_evidence_hashes_unchanged"] is True + assert refusal["production_bytes_unchanged"] is True + assert "409 files would be left unchanged." in archive["md-black.log"].decode() diff --git a/docs/evidence/issue-1773-dev-tool-archive-bindings/stdlib-full-dashboard-green.log b/docs/evidence/issue-1773-dev-tool-archive-bindings/stdlib-full-dashboard-green.log new file mode 100644 index 00000000..d68300db --- /dev/null +++ b/docs/evidence/issue-1773-dev-tool-archive-bindings/stdlib-full-dashboard-green.log @@ -0,0 +1,62 @@ +................................................ [100%] +=============================== warnings summary =============================== +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/sklearn/utils/_param_validation.py:11 + /opt/anaconda3/lib/python3.12/site-packages/sklearn/utils/_param_validation.py:11: UserWarning: A NumPy version >=2.0.0 and <2.8.0 is required for this version of SciPy (detected version 1.26.4) + from scipy.sparse import csr_matrix, issparse + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/scipy/sparse/_sputils.py:17 + /opt/anaconda3/lib/python3.12/site-packages/scipy/sparse/_sputils.py:17: FutureWarning: In the future `np.long` will be defined as the corresponding NumPy scalar. + np.uintc, np.long, np.ulong, np.longlong, np.ulonglong, + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/langsmith/evaluation/_key_extraction.py:20 + /opt/anaconda3/lib/python3.12/site-packages/langsmith/evaluation/_key_extraction.py:20: DeprecationWarning: ast.Str is deprecated and will be removed in Python 3.14; use ast.Constant instead + (ast.Str, ast.Constant) if hasattr(ast, "Str") else (ast.Constant,) + +api/chat.py:1501 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1501: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("startup") + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675 + /opt/anaconda3/lib/python3.12/site-packages/fastapi/applications.py:4675: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + return self.router.on_event(event_type) # ty: ignore[deprecated] + +api/chat.py:1801 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1801: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("shutdown") + +api/chat.py:1808 + /Users/teacher/.codex/automations/sync-dependency-pr-closer/worktrees/manager-database-1773/api/chat.py:1808: DeprecationWarning: + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + + @app.on_event("shutdown") + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/pandas/core/computation/expressions.py:23 + /opt/anaconda3/lib/python3.12/site-packages/pandas/core/computation/expressions.py:23: UserWarning: Pandas requires version '2.10.2' or newer of 'numexpr' (version '2.8.7' currently installed). + from pandas.core.computation.check import NUMEXPR_INSTALLED + +../../../../../../../opt/anaconda3/lib/python3.12/site-packages/pandas/core/arrays/masked.py:56 + /opt/anaconda3/lib/python3.12/site-packages/pandas/core/arrays/masked.py:56: UserWarning: Pandas requires version '1.4.2' or newer of 'bottleneck' (version '1.3.7' currently installed). + from pandas.core import ( + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +48 passed, 11 warnings in 127.45s (0:02:07) diff --git a/tests/test_dashboard_read_failure_evidence.py b/tests/test_dashboard_read_failure_evidence.py index 9798d04a..98d37836 100644 --- a/tests/test_dashboard_read_failure_evidence.py +++ b/tests/test_dashboard_read_failure_evidence.py @@ -3,9 +3,11 @@ import ast import hashlib import json +import re import subprocess import sys import tarfile +import tomllib from collections import Counter from pathlib import Path @@ -114,8 +116,7 @@ def test_dashboard_review_recovery_archive_and_six_current_inputs(): "tests/test_dashboard_read_failure_evidence.py", "pyproject.toml", } - for name, expected in manifest["current_inputs"].items(): - assert digest((ROOT / name).read_bytes()) == expected, name + validate_review_recovery_current_inputs(ROOT, manifest) @pytest.mark.parametrize("loader", SQL + DOMAIN) @@ -297,3 +298,110 @@ def test_dashboard_archived_reuse_refusal_and_black_receipt(archive): assert refusal["original_evidence_hashes_unchanged"] is True assert refusal["production_bytes_unchanged"] is True assert "409 files would be left unchanged." in archive["md-black.log"].decode() + + +def normalized_dashboard_project(raw): + """Only exact Black/Ruff version values are mutable infrastructure inputs.""" + project = tomllib.loads(raw.decode()) + dependencies = project["project"]["optional-dependencies"]["dev"] + seen = set() + normalized = [] + for value in dependencies: + tool = re.match(r"(black|ruff)(?=[^A-Za-z0-9_-]|$)", value, re.IGNORECASE) + if tool: + name = tool.group(1).lower() + assert name not in seen, f"Duplicate formatter requirement: {name}" + seen.add(name) + # The retained/current contract uses plain numeric release pins. + # Other operators, URLs, extras, markers or wildcard pins are not + # an allowed version-only change to that historical input. + assert re.fullmatch( + r"(black|ruff)==[0-9]+(?:\.[0-9]+){1,3}", value, re.IGNORECASE + ), name + normalized.append(name + "==0") + else: + normalized.append(value) + assert seen == {"black", "ruff"} + project["project"]["optional-dependencies"]["dev"] = normalized + return project + + +def validate_review_recovery_current_inputs(root, manifest): + successor = ROOT / "docs/evidence/issue-1773-dev-tool-archive-bindings" + binding = json.loads((successor / "current-bindings.json").read_text()) + assert binding["schema"] == "dashboard-review-recovery-successor/v1" + recovery_manifest = EVIDENCE / "review-recovery-20261012/manifest.json" + assert digest(recovery_manifest.read_bytes()) == binding["historical_manifest_sha256"] + mutable = {"pyproject.toml", "tests/test_dashboard_read_failure_evidence.py"} + for name, expected in manifest["current_inputs"].items(): + if name not in mutable: + assert digest((root / name).read_bytes()) == expected, name + for name, snapshot in [ + ("pyproject.toml", "historical-pyproject.toml"), + ("tests/test_dashboard_read_failure_evidence.py", "historical-validator.py"), + ]: + assert digest((successor / snapshot).read_bytes()) == manifest["current_inputs"][name] + assert ( + digest((root / "tests/test_dashboard_read_failure_evidence.py").read_bytes()) + == binding["current_validator_sha256"] + ) + assert normalized_dashboard_project( + (root / "pyproject.toml").read_bytes() + ) == normalized_dashboard_project((successor / "historical-pyproject.toml").read_bytes()) + + +def test_review_recovery_accepts_generated_formatter_pins_without_rewriting_history(): + folder = ROOT / "docs/evidence/issue-1773-dev-tool-archive-bindings" + raw = (folder / "generated-pyproject.toml").read_bytes() + provenance = json.loads((folder / "generated-provenance.json").read_text()) + assert len(raw) == provenance["bytes"] + assert digest(raw) == provenance["sha256"] + assert normalized_dashboard_project(raw) == normalized_dashboard_project( + (folder / "historical-pyproject.toml").read_bytes() + ) + + +@pytest.mark.parametrize( + "package,mutation", + [ + ("project-name", "rename"), + ("pytest", "version"), + ("black", "range"), + ("ruff", "wildcard"), + ("black", "extras"), + ], +) +def test_review_recovery_rejects_non_formatter_or_non_exact_changes(package, mutation): + folder = ROOT / "docs/evidence/issue-1773-dev-tool-archive-bindings" + original = (folder / "generated-pyproject.toml").read_text() + if package == "project-name": + old, new = 'name = "manager-database"', 'name = "different-product"' + else: + dependencies = tomllib.loads(original)["project"]["optional-dependencies"]["dev"] + pin = next(value for value in dependencies if value.startswith(package + "==")) + version = pin.split("==", 1)[1] + replacements = { + "version": package + "==" + version + "0", + "range": package + ">=" + version, + "wildcard": package + "==" + version.rsplit(".", 1)[0] + ".*", + "extras": package + "[extra]==" + version, + } + old, new = json.dumps(pin), json.dumps(replacements[mutation]) + assert old in original + with pytest.raises(AssertionError): + assert normalized_dashboard_project( + original.replace(old, new).encode() + ) == normalized_dashboard_project((folder / "historical-pyproject.toml").read_bytes()) + + +@pytest.mark.parametrize("changed", [SOURCE, "tests/test_dashboard_read_failure_evidence.py"]) +def test_review_recovery_retains_live_product_and_validator_bindings(tmp_path, changed): + manifest = json.loads((EVIDENCE / "review-recovery-20261012/manifest.json").read_text()) + for name in manifest["current_inputs"]: + target = tmp_path / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes((ROOT / name).read_bytes()) + target = tmp_path / changed + target.write_bytes(target.read_bytes() + b"\n# unreviewed change\n") + with pytest.raises(AssertionError): + validate_review_recovery_current_inputs(tmp_path, manifest)