diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..fac235c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +# Funding links for OpenLongevityLab. +# Financial support is optional. It does not purchase scientific conclusions, +# review status, medical advice, priority evidence grading, or private data access. +custom: + - https://www.paypal.com/paypalme/agentflowenterprise diff --git a/CHANGELOG.md b/CHANGELOG.md index a36a09a..1f48c06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased — documentation and integrity audit +Citation export now accepts `scoring_as_of` as an ISO 8601 query parameter, matching the evidence-list scoring contract. The normalized UTC time flows into the citation manifest's observed scoring-time set, allowing clients to recreate the same temporal scoring basis and compare SHA-256 export fingerprints without depending on the day the request is executed. Invalid scoring timestamps return the existing structured `INVALID_SCORING_AS_OF` error. + Persisted publication responses now expose an explicit `origin` contract with `unknown`, `manual`, `provider`, and `synthetic` states plus a tri-state `synthetic` interpretation. Synthetic seeds and `SYN-*` or `SEED-*` identifiers remain synthetic even when their payload has provider-shaped metadata. Legacy rows without a documented origin are normalized conservatively as `unknown` rather than being presented as real provider retrievals. Revision history carries the normalized origin fields in each payload. PubMed parsing remains conservative: direct XML parsing and injected transports do not certify provider origin, while the ordinary built-in PubMed search path marks records as provider-derived before persistence. Tests now cover origin normalization, PubMed path classification, provider-shaped synthetic payloads, revision changes from unknown to manual origin, API list/detail/history responses, and the CI seed record's synthetic label. diff --git a/README.md b/README.md index 98d9200..ac5faf2 100644 --- a/README.md +++ b/README.md @@ -102,9 +102,15 @@ Academic depth means that a reader can identify a question, assess assumptions, Useful contributions include a focused bug reproduction, a parser fixture with clear redistribution rights, a correction to an overstated capability, or a reference calculation exposing an edge case. Identify the commit and environment and explain the expected behavior. Separate a software defect from a scientific disagreement: both deserve examination, but they require different evidence. Preserve the author attribution and credit the original studies and external software independently of project authorship. +## Collaboration and support + +OpenLongevity welcomes focused collaboration from researchers, engineers, reviewers, students, and independent contributors who want transparent computational infrastructure for aging research. Useful contributions include reproducible bug reports, provider-adapter fixtures with clear redistribution rights, documentation corrections, API examples, database migration checks, benchmark proposals, and scientific-review notes that preserve uncertainty instead of replacing it with promotional certainty. Contributors should begin with [CONTRIBUTING.md](CONTRIBUTING.md), the [pull request template](.github/PULL_REQUEST_TEMPLATE.md), and the [OpenLongevityLab impact article](docs/research/openlongevity-impact-article.md) to understand the present research-prototype stage and the final platform target. + +Financial support is optional and helps sustain maintenance, documentation, infrastructure, and research-tool development. Support can be offered through [PayPal](https://www.paypal.com/paypalme/agentflowenterprise), with additional context in [DONATE.md](DONATE.md). A donation does not purchase a scientific conclusion, evidence grade, private dataset, review status, clinical recommendation, or claim of longevity benefit. Funding and evidence remain separate: every result must still be traceable to source records, explicit methods, stated limitations, and human review where required. + ## Author -**Ciprian Ștefan Pleșca** — Founder, Project Creator, Lead Maintainer, and Principal Author. See [AUTHORS.md](AUTHORS.md) and [CITATION.cff](CITATION.cff). +**Ciprian Ștefan Pleșca** — Founder, Project Creator, Lead Maintainer, Principal Author, and independent Romanian researcher. See [AUTHORS.md](AUTHORS.md), [CITATION.cff](CITATION.cff), and [DONATE.md](DONATE.md). ## Governance and contribution diff --git a/docs/API.md b/docs/API.md index b43eaa4..b61d93a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -14,7 +14,7 @@ The service separates persisted publications from synthetic evidence demonstrati ```bash curl http://localhost:8000/api/v1/health curl 'http://localhost:8000/api/v1/evidence?topic=senescence' -curl 'http://localhost:8000/api/v1/evidence/export/citation?topic=senescence' +curl 'http://localhost:8000/api/v1/evidence/export/citation?topic=senescence&scoring_as_of=2021-01-01T00:00:00Z' curl 'http://localhost:8000/api/v1/search?query=senescence&page=1&page_size=10' curl 'http://localhost:8000/api/v1/research-gaps?topic=senescence' curl http://localhost:8000/api/v1/graph @@ -79,13 +79,13 @@ Evidence, evidence detail, and research-gap routes operate on synthetic fixtures `GET /api/v1/evidence/export/citation` is the first executable export boundary for the fixture corpus. It returns `mode: citation-eligible`, an `items` list, an `excluded` list, totals for both lists, a schema version, and the research disclaimer. Under the current fixture-only evidence mode, `SYN-*` records are excluded with reason `synthetic_fixture`, so the citation-eligible item list is empty for the bundled cellular-senescence demonstration. Non-synthetic records must also carry `review_status: verified` plus reviewer identity, review timestamp, and review notes before they can enter the citation-eligible item list. This is intentional: the route proves that the platform can reject demonstration data and unverified evidence rather than allowing attractive records to leak into citation workflows. -The citation export route should not be described as a complete publication export system. It does not yet produce bibliographic formats, human-review certificates, or provider-backed evidence bundles. It establishes a narrow behavior that was previously documented only as a policy: synthetic fixtures are not observations and are excluded by default from citation-eligible evidence export. The response now includes a deterministic manifest with included and excluded identifiers, exclusion reasons, scoring metadata, and a SHA-256 `export_fingerprint` so repeated exports can be compared. Future work can extend the same contract to persisted publication records once review status and source authenticity are implemented for that path. +The citation export route should not be described as a complete publication export system. It does not yet produce bibliographic formats, human-review certificates, or provider-backed evidence bundles. It establishes a narrow behavior that was previously documented only as a policy: synthetic fixtures are not observations and are excluded by default from citation-eligible evidence export. The response now includes a deterministic manifest with included and excluded identifiers, exclusion reasons, scoring metadata, and a SHA-256 `export_fingerprint` so repeated exports can be compared. The route accepts the same optional `scoring_as_of` ISO 8601 query parameter used by the evidence list, normalizes it to UTC, and carries the observed value set into `manifest.scoring_as_of`. Supplying the timestamp is the recommended way to produce a reproducible export fingerprint across different days. The `excluded` list remains a compact audit summary with identifier, title, and exclusion reason; the manifest is the comparison surface for temporal scoring metadata. Future work can extend the same contract to persisted publication records once review status and source authenticity are implemented for that path. `POST /api/v1/evidence/{record_id}/review` records a persistent review event when PostgreSQL is configured and the caller supplies `X-Review-Key` matching `OPENLONGEVITY_REVIEW_KEY`. The request body includes `status`, `reviewer`, `reviewed_at`, and `notes`. The server rejects machine-only statuses as human review actions and requires the same metadata that citation export later expects from verified records. Without a configured review key, the route returns `REVIEW_DISABLED`; without a configured and migrated database, it returns `DATABASE_NOT_CONFIGURED`. This keeps the preview from pretending that review events are persistent when the audit table is not available. `GET /api/v1/evidence/{record_id}/review-events` lists stored review events for a fixture evidence record when the review repository is configured. The route returns audit events, not a full reviewer user interface. It is the persistence boundary for the human-review workflow: reviewer actions can be stored, inspected, and connected to citation-export eligibility, while user management and role delegation remain future work. -Evidence grades and scores require their methodological labels. The A–G mapping is a project taxonomy, and the numerical navigation score uses heuristic constants. Neither is a calibrated scientific certainty estimate. When a publication date is present, the score depends on the scoring time; `GET /api/v1/evidence` accepts an optional `scoring_as_of` ISO 8601 datetime query parameter and evidence summaries expose the normalized `scoring_as_of` value so clients can record that temporal basis. Evidence items and detail responses include `navigation_score`, `score_method`, record-level `scoring_as_of`, and `score_components` metadata so exports remain traceable to the scoring contract. The API's ability to serialize a number does not justify describing it as a treatment effect, probability of truth, or measure of human longevity benefit. +Evidence grades and scores require their methodological labels. The A–G mapping is a project taxonomy, and the numerical navigation score uses heuristic constants. Neither is a calibrated scientific certainty estimate. When a publication date is present, the score depends on the scoring time; `GET /api/v1/evidence` and `GET /api/v1/evidence/export/citation` accept an optional `scoring_as_of` ISO 8601 datetime query parameter and expose the normalized value so clients can record that temporal basis. Evidence items and detail responses include `navigation_score`, `score_method`, record-level `scoring_as_of`, and `score_components` metadata so exports remain traceable to the scoring contract. The API's ability to serialize a number does not justify describing it as a treatment effect, probability of truth, or measure of human longevity benefit. Publication origin classification is now explicit for persisted publications, including synthetic seed rows and records saved through the PubMed ingestion path. Clients and operators must still avoid treating `synthetic: false` as proof of scientific reliability. It means the record was not classified as synthetic by the storage contract and entered through a provider-boundary path; it does not mean the publication is complete, unretracted, clinically relevant, or human reviewed. diff --git a/src/openlongevity/api.py b/src/openlongevity/api.py index 4cad042..5ce73c2 100644 --- a/src/openlongevity/api.py +++ b/src/openlongevity/api.py @@ -288,11 +288,15 @@ async def evidence( "summary": summary, "disclaimer": DISCLAIMER} @app.get("/api/v1/evidence/export/citation") - async def citation_export(topic: str = Query(default="", max_length=120)) -> dict[str, Any]: + async def citation_export( + topic: str = Query(default="", max_length=120), + scoring_as_of: str | None = Query(default=None, max_length=40), + ) -> dict[str, Any]: + scoring_time = parse_scoring_as_of(scoring_as_of) records = await current_records( [r for r in fixtures if topic.casefold() in r.title.casefold()] ) - summary = engine.summarize(records) + summary = engine.summarize(records, as_of=scoring_time) scoring_time = datetime.fromisoformat(summary["scoring_as_of"]) payloads = [evidence_payload(r, synthetic=True, scoring_time=scoring_time) for r in records] diff --git a/tests/test_api.py b/tests/test_api.py index 68e4463..fc91330 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -147,6 +147,51 @@ def test_citation_export_excludes_synthetic_fixtures() -> None: assert payload["excluded"][0]["reason"] == "synthetic_fixture" +def test_citation_export_accepts_reproducible_scoring_time( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("DATABASE_URL", raising=False) + monkeypatch.delenv("TEST_DATABASE_URL", raising=False) + client = TestClient(create_app()) + + response = client.get( + "/api/v1/evidence/export/citation", + params={"topic": "senescence", "scoring_as_of": "2021-01-01T00:00:00Z"}, + ) + repeated = client.get( + "/api/v1/evidence/export/citation", + params={"topic": "senescence", "scoring_as_of": "2021-01-01T00:00:00Z"}, + ) + different_time = client.get( + "/api/v1/evidence/export/citation", + params={"topic": "senescence", "scoring_as_of": "2022-01-01T00:00:00Z"}, + ) + + assert response.status_code == 200 + assert repeated.status_code == 200 + assert different_time.status_code == 200 + payload = response.json() + assert payload["manifest"]["scoring_as_of"] == ["2021-01-01T00:00:00+00:00"] + assert payload["manifest"]["export_fingerprint"] == ( + repeated.json()["manifest"]["export_fingerprint"] + ) + assert payload["manifest"]["export_fingerprint"] != ( + different_time.json()["manifest"]["export_fingerprint"] + ) + + +def test_citation_export_rejects_invalid_scoring_time() -> None: + client = TestClient(create_app()) + + response = client.get( + "/api/v1/evidence/export/citation", + params={"topic": "senescence", "scoring_as_of": "not-a-date"}, + ) + + assert response.status_code == 422 + assert response.json()["error"]["code"] == "INVALID_SCORING_AS_OF" + + def test_review_endpoint_is_disabled_without_review_key() -> None: client = TestClient(create_app()) response = client.post(