From 7574da04b530235d34ea255b97c1da56c20edda8 Mon Sep 17 00:00:00 2001 From: Ciprian-LocalPulse Date: Wed, 23 Sep 2026 05:41:13 +0300 Subject: [PATCH 1/8] fix: tighten analysis integrity contracts --- CHANGELOG.md | 2 +- README.md | 2 +- ROADMAP.md | 2 +- docs/academic/06-multi-omics-integration.md | 8 +-- .../adr/0010-multi-omics-interfaces.md | 6 +- docs/audits/v0.3.0-baseline.md | 8 +++ src/openlongevity/analysis/omics.py | 19 +++++- src/openlongevity/analysis/pathway.py | 45 +++++++++---- tests/test_analysis.py | 65 ++++++++++++++++++- 9 files changed, 133 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 985df31..fe429a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Documentation tooling now counts prose separately from fenced code and diagrams, The abbreviated license file has been replaced with the official Apache License 2.0 text. Project attribution is recorded separately in NOTICE, and citation metadata identifies the author as an independent Romanian researcher. The existing release tag is preserved. The citation's release version is not silently advanced to an unpublished development version merely because the Python package declares that version. -Known implementation issues remain visible: incomplete pathway adjustment, duplicate multi-omics handling, and the absence of an operational scientific review service. The frontend now exposes key provenance boundaries and has build-plus-browser smoke coverage, but it still needs broader accessibility, visual regression, and production deployment evidence. Publication origin classification is improved, but provider origin remains a retrieval-boundary label rather than scientific validation. No clinical validation or independent benchmark is asserted by this documentation entry. +Two analysis-integrity issues have targeted corrections after the documentation baseline. Pathway enrichment now applies monotonic Benjamini–Hochberg adjustment over the full valid pathway family, including valid pathways with no observed overlap in the correction denominator. Multi-omics grouping now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting earlier values. The frontend now exposes key provenance boundaries and has build-plus-browser smoke coverage, but it still needs broader accessibility, visual regression, and production deployment evidence. Publication origin classification is improved, but provider origin remains a retrieval-boundary label rather than scientific validation. No clinical validation, independent benchmark, or operational scientific review service is asserted by this documentation entry. ## [0.2.0] - 2026-09-14 diff --git a/README.md b/README.md index e3ac460..222a742 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ The A–G taxonomy assigns systematic reviews to A, randomized trials to B, clin The numerical navigation score uses hand-selected design weights, confidence, replication metadata, sample size, and publication age. It is not a probability of truth, a treatment effect, or a validated evidence-certainty scale. Date-sensitive outputs can change with execution time. The [whitepaper](WHITEPAPER.md) now describes the actual arithmetic and its limitations. A research report should show the inputs and discuss whether conclusions depend on the ranking choices. -Experimental analysis utilities need similarly narrow interpretation. The biological-age routine is an ordinary-least-squares baseline. The survival utility produces Kaplan–Meier points without a complete inference framework. Multi-omics integration groups sample keys but does not establish participant consistency or statistical batch correction. The pathway routine has an identified multiple-testing adjustment limitation. These are reasons to inspect and improve the methods before drawing scientific conclusions, not details to hide behind a general research-use label. +Experimental analysis utilities need similarly narrow interpretation. The biological-age routine is an ordinary-least-squares baseline. The survival utility produces Kaplan–Meier points without a complete inference framework. Multi-omics integration now rejects duplicate sample-layer pairs and inconsistent participant mappings, but it still does not perform feature harmonization, statistical batch correction, or biological interpretation. The pathway routine now applies a monotonic Benjamini–Hochberg correction across the valid pathway hypothesis family, while results still depend on the declared universe, pathway annotations, and upstream gene selection. These are reasons to inspect methods before drawing scientific conclusions, not details to hide behind a general research-use label. ## Documentation, evidence, and contribution standards diff --git a/ROADMAP.md b/ROADMAP.md index 6ad37c1..8dd01cf 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -38,7 +38,7 @@ Publication-origin classification has moved from a fixed false synthetic flag to Complete the documentation expansion with distinct, source-backed explanations rather than repeated filler. Every tracked Markdown file has a minimum prose target, but accuracy and implementation alignment remain separate requirements. The automated inventory should continue to expose short documents, missing attribution, and structural problems. Code examples need execution checks, and diagrams need to identify proposed components clearly. A passing word-count gate is not a substitute for editorial review. -Resolve known analysis limitations through focused changes and reference cases. The pathway adjustment needs a clearly defined hypothesis family and correct adjustment behavior. Multi-omics integration needs an explicit duplicate and participant-consistency policy. Date-sensitive scoring needs a controllable or clearly recorded time basis. Each change should state its effect on existing outputs and should not be promoted as clinical validation merely because a regression test passes. +Continue resolving analysis limitations through focused changes and reference cases. Pathway enrichment now defines the tested family as pathways with at least one member in the supplied universe and applies monotonic Benjamini–Hochberg adjustment while returning only observed overlaps. Multi-omics integration now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting values. Remaining analysis priorities include a controllable or clearly recorded time basis for date-sensitive scoring, explicit manifests for preprocessing metadata, broader edge-case fixtures, and downstream methods that keep their scientific assumptions visible. Each change should state its effect on existing outputs and should not be promoted as clinical validation merely because a regression test passes. ## Publication-path acceptance diff --git a/docs/academic/06-multi-omics-integration.md b/docs/academic/06-multi-omics-integration.md index 348bba1..d9e8f0e 100644 --- a/docs/academic/06-multi-omics-integration.md +++ b/docs/academic/06-multi-omics-integration.md @@ -10,7 +10,7 @@ At baseline `9fddcbb`, `MultiOmicsSample` includes sample identifier, participan A sample identifier should refer to a defined physical or analytical sample, while a participant identifier links samples from the same person or experimental subject. These identifiers are not interchangeable. Multiple samples can arise from one participant at different times or from different tissues. A join using participant identity alone could combine measurements that were never intended to represent the same sampling occasion. Conversely, globally reusing a short sample label can merge unrelated studies accidentally. -The current implementation does not verify that repeated sample identifiers have consistent participant identifiers. It also overwrites an earlier feature dictionary when a later record has the same sample and layer. Those behaviors are concrete limitations, not theoretical concerns to be hidden in a generic warning. Before using the helper for cohort analysis, callers need to validate identity consistency and define a duplicate policy. A future implementation should reject ambiguous input or require an explicit, recorded resolution. +The current implementation now verifies two identity conditions at the grouping boundary. A sample identifier must map to one participant identifier, and a sample-layer pair may appear only once. If either condition is violated, the helper raises an error instead of returning a polished table that hides the ambiguity. This is a narrow but important improvement: it prevents participant mix-ups and repeated exports from becoming silent overwrites. It does not yet solve the harder scientific tasks of feature harmonization, batch correction, longitudinal alignment, or cohort-level missingness modeling. An integration manifest should record source dataset, participant namespace, sample namespace, collection time, tissue, assay layer, and the identifier mapping used. If privacy requirements prevent public release of participant identifiers, the reproducibility package still needs a controlled mapping strategy appropriate to the authorized setting. Public code does not justify exposing linkage keys. Synthetic identifiers should be used in repository examples and clearly distinguished from real cohort information. @@ -48,15 +48,15 @@ assert "transcriptomics" not in joined["S1"] ## Evaluation and failure cases -The acceptance suite should include an empty identifier, an inconsistent participant mapping, a repeated sample-layer pair, an explicitly missing value, an absent layer, and incompatible feature definitions. Separate tests of current behavior from tests of proposed safeguards. For example, a test documenting overwrite behavior should not be described as proving duplicate safety. A safeguard is complete only when the implementation rejects or resolves the ambiguity as specified. +The acceptance suite should include an empty identifier, an inconsistent participant mapping, a repeated sample-layer pair, an explicitly missing value, an absent layer, and incompatible feature definitions. The implemented safeguard now rejects inconsistent participant mappings and repeated sample-layer pairs. That should be described precisely: the helper prevents two high-risk identity failures, but it does not certify that all features are comparable or that a downstream model is scientifically appropriate. -Order sensitivity is particularly informative. Permuting distinct sample-layer inputs should preserve the same logical output, while permutations containing duplicates currently can change the result. Demonstrating that difference makes the limitation concrete. Once a duplicate policy is implemented, the test should show either a stable declared resolution or a consistent error. The project should preserve the original inputs so that an analyst can inspect which records participated in the join. +Order sensitivity is particularly informative. Permuting distinct sample-layer inputs should preserve the same logical output, while permutations containing duplicates now produce a consistent error instead of a different overwrite result. This is the correct behavior for the present interface because it forces a curator or upstream importer to decide whether the duplicate is an accidental repeat, a conflicting measurement, or a distinct sample that needs a different identifier. The project should preserve the original inputs so that an analyst can inspect which records participated in the join. Before downstream modeling, publish a summary of input samples, retained samples, missing layers, excluded records, and unresolved identifier issues. These counts describe data handling, not biological performance. A model report should link back to the exact integration manifest and transformation versions. The source reference for the present behavior is [`omics.py`](../../src/openlongevity/analysis/omics.py); [A09](09-model-evaluation.md) develops the separate evaluation boundary. **Question.** How can layers be joined without silently imputing absent measurements? -The integration interface uses a sample key and a typed layer enum. Explicit missing feature values remain `None`; absent layers remain absent. An integration manifest and duplicate safeguards are proposed additions, not current automatic output. +The integration interface uses a sample key and a typed layer enum. Explicit missing feature values remain `None`; absent layers remain absent. Duplicate and participant-consistency safeguards are now implemented at the grouping boundary. A richer integration manifest, feature namespaces, assay units, and preprocessing lineage remain proposed additions rather than current automatic output. ```mermaid flowchart TB diff --git a/docs/architecture/adr/0010-multi-omics-interfaces.md b/docs/architecture/adr/0010-multi-omics-interfaces.md index 424bd42..77357ba 100644 --- a/docs/architecture/adr/0010-multi-omics-interfaces.md +++ b/docs/architecture/adr/0010-multi-omics-interfaces.md @@ -39,7 +39,7 @@ The cost is that integration is postponed. The interface does not by itself solv ## Metadata Requirements -Each omics record should preserve sample identifier, participant identifier when available, layer, features, units or feature namespace where possible, batch, normalization method, source dataset, retrieval or creation date, and synthetic status where applicable. If the same sample appears in multiple layers, the grouping function should maintain layer separation. Duplicate sample-layer pairs should have a documented conflict policy. +Each omics record should preserve sample identifier, participant identifier when available, layer, features, units or feature namespace where possible, batch, normalization method, source dataset, retrieval or creation date, and synthetic status where applicable. If the same sample appears in multiple layers, the grouping function should maintain layer separation. The current conflict policy rejects duplicate sample-layer pairs and rejects sample identifiers that map to conflicting participant identifiers. Batch and normalization metadata are essential. Many omics signals are sensitive to platform, reagent lot, sequencing depth, laboratory site, and preprocessing pipeline. A value without preprocessing context can be difficult to compare. OpenLongevity should not overstate cross-study comparability when normalization methods differ. @@ -57,7 +57,7 @@ The platform's open-science ambition does not override participant protection. A ## Verification -Tests should confirm grouping behavior, preservation of layer identity, explicit missing-layer representation, and avoidance of silent imputation. Fixtures should include complete and incomplete samples, multiple layers, duplicate attempts, and different normalization labels. Documentation examples should state whether data are synthetic or real and should avoid clinical interpretation from raw multi-omics values. +Tests should confirm grouping behavior, preservation of layer identity, explicit missing-layer representation, and avoidance of silent imputation. Fixtures should include complete and incomplete samples, multiple layers, duplicate attempts, inconsistent participant linkage, and different normalization labels. The current suite verifies missing values, multiple layers for one sample, duplicate rejection, and participant-consistency rejection. Documentation examples should state whether data are synthetic or real and should avoid clinical interpretation from raw multi-omics values. This ADR remains accepted because it gives OpenLongevity a disciplined foundation for future multi-omics work. The project can connect layers without erasing their differences, and it can delay complex integration until methods and governance are ready. @@ -71,7 +71,7 @@ The acceptance criterion is that any multi-omics output can answer three questio ## Operational Risks -The operational risks are duplicate sample identifiers, inconsistent participant linkage, incompatible feature names, undocumented batch correction, and accidental exposure of sensitive individual-level records. The interface should force those issues into metadata rather than hiding them. A future ingestion workflow should reject ambiguous duplicates, record normalization state, and require explicit governance approval before handling real individual-level omics data. +The operational risks are duplicate sample identifiers, inconsistent participant linkage, incompatible feature names, undocumented batch correction, and accidental exposure of sensitive individual-level records. The interface now rejects two of those risks at grouping time: duplicate sample-layer pairs and conflicting participant linkage. The remaining risks still need metadata, ingestion manifests, feature namespaces, normalization records, and explicit governance approval before handling real individual-level omics data. The interface should also support negative capability statements. If a dataset lacks proteomics, longitudinal follow-up, or participant linkage, the grouped view should say so directly. Clear absence is better than a smooth table that suggests completeness. diff --git a/docs/audits/v0.3.0-baseline.md b/docs/audits/v0.3.0-baseline.md index 91f2a7a..5d27fd4 100644 --- a/docs/audits/v0.3.0-baseline.md +++ b/docs/audits/v0.3.0-baseline.md @@ -87,6 +87,14 @@ The verification added for this correction covers origin normalization rules, Pu This correction improves source interpretation, but it does not complete the scientific evidence workflow. `origin: provider` means the record crossed an implemented retrieval boundary; it does not mean the paper is correct, complete, unretracted, clinically relevant, or human reviewed. The product remains an alpha research prototype until frontend rendering, provider-backed evidence extraction, review operations, export manifests, and release gates are demonstrated together. +## Analysis-integrity follow-up: 23 September 2026 + +Two analysis utilities now have narrower and more defensible contracts than they had at the documentation baseline. Pathway enrichment defines its multiple-testing family as every pathway that has at least one member inside the supplied universe. Pathways with no observed overlap contribute to the Benjamini–Hochberg denominator, and the adjusted values are computed with the monotonic step-up rule rather than a simple rank multiplier. The result list remains focused on pathways with observed overlap, so callers do not receive a long list of empty hits, but the correction is no longer determined only by the pathways that happened to overlap. + +The multi-omics grouping helper now rejects duplicate sample-layer pairs and rejects a sample identifier that maps to more than one participant identifier. This removes the previous silent-overwrite behavior. The utility still returns a compact layer map for each sample and preserves explicit missing feature values as `None`. It does not perform batch correction, feature harmonization, unit conversion, privacy review, or biological integration. Those remain downstream scientific tasks. + +Verification for this follow-up added targeted regression cases for the pathway hypothesis family, monotonic adjusted p-values, genes outside the universe, missing omics values, duplicate omics layers, and inconsistent participant mappings. The local focused check reported `ruff check` passing and the analysis test module passing with seven tests. This is implementation evidence for the stated software contracts, not evidence that any pathway result or multi-omics association is biologically valid. + This audit records project responsibility under Ciprian Ștefan Pleșca, an independent Romanian researcher. It does not claim an external audit institution, university affiliation, scientific peer review, or security certification. Its contribution is a traceable account of what was inspected, what changed, what was actually checked, and what remains unresolved. Future updates should preserve that separation so that readers can evaluate progress without having to infer completion from presentation quality. --- diff --git a/src/openlongevity/analysis/omics.py b/src/openlongevity/analysis/omics.py index 27d3710..c405842 100644 --- a/src/openlongevity/analysis/omics.py +++ b/src/openlongevity/analysis/omics.py @@ -26,10 +26,25 @@ class MultiOmicsSample: def integrate_samples( samples: list[MultiOmicsSample], ) -> dict[str, dict[str, dict[str, float | None]]]: - """Group layers by common sample ID; missing values stay explicit as ``None``.""" + """Group layers by common sample ID; missing values stay explicit as ``None``. + + A sample identifier must map to a single participant, and each sample/layer pair + may appear only once. Silent overwrites are rejected because they can hide + participant mix-ups, repeated exports, or inconsistent upstream normalization. + """ result: dict[str, dict[str, dict[str, float | None]]] = {} + participants_by_sample: dict[str, str] = {} for sample in samples: if not sample.sample_id or not sample.participant_id: raise ValueError("sample_id and participant_id are required") - result.setdefault(sample.sample_id, {})[sample.layer.value] = dict(sample.features) + previous_participant = participants_by_sample.setdefault( + sample.sample_id, sample.participant_id + ) + if previous_participant != sample.participant_id: + raise ValueError("sample_id must map to exactly one participant_id") + + layers = result.setdefault(sample.sample_id, {}) + if sample.layer.value in layers: + raise ValueError("duplicate sample_id and layer combination") + layers[sample.layer.value] = dict(sample.features) return result diff --git a/src/openlongevity/analysis/pathway.py b/src/openlongevity/analysis/pathway.py index 44d2c8a..cd3b6a2 100644 --- a/src/openlongevity/analysis/pathway.py +++ b/src/openlongevity/analysis/pathway.py @@ -16,6 +16,14 @@ class EnrichmentResult: def enrich_gene_set( genes: set[str], pathways: dict[str, set[str]], universe: set[str] ) -> list[EnrichmentResult]: + """Compute one-sided pathway enrichment with monotonic Benjamini-Hochberg q-values. + + The multiple-testing family is every pathway that has at least one member inside the + supplied universe. Pathways with no observed overlap still contribute to the + correction factor, because excluding them would make adjusted p-values depend on + the observed result rather than the declared hypothesis family. The returned list + remains focused on pathways with at least one overlapping gene. + """ if not genes or not pathways or not universe or not genes <= universe: raise ValueError("genes must be non-empty and contained in universe") tests: list[tuple[str, int, int, float]] = [] @@ -24,17 +32,32 @@ def enrich_gene_set( for name, members in pathways.items(): overlap = len(genes & members) size = len(members & universe) - if not size or not overlap: + if not size: continue - tail = sum( - comb(size, k) * comb(population_size - size, sample_size - k) - for k in range(overlap, min(size, sample_size) + 1) - if sample_size - k <= population_size - size - ) - denominator = comb(population_size, sample_size) - tests.append((name, overlap, size, min(1.0, tail / denominator))) - tests.sort(key=lambda item: item[3]) + if overlap: + tail = sum( + comb(size, k) * comb(population_size - size, sample_size - k) + for k in range(overlap, min(size, sample_size) + 1) + if sample_size - k <= population_size - size + ) + denominator = comb(population_size, sample_size) + p_value = min(1.0, tail / denominator) + else: + p_value = 1.0 + tests.append((name, overlap, size, p_value)) + tests.sort(key=lambda item: (item[3], item[0])) + + adjusted_by_pathway: dict[str, float] = {} + running_adjusted = 1.0 + family_size = len(tests) + for rank, (name, _overlap, _size, p_value) in reversed( + list(enumerate(tests, start=1)) + ): + running_adjusted = min(running_adjusted, min(1.0, p_value * family_size / rank)) + adjusted_by_pathway[name] = running_adjusted + return [ - EnrichmentResult(name, overlap, size, p, min(1.0, p * len(tests) / rank)) - for rank, (name, overlap, size, p) in enumerate(tests, 1) + EnrichmentResult(name, overlap, size, p, adjusted_by_pathway[name]) + for name, overlap, size, p in tests + if overlap ] diff --git a/tests/test_analysis.py b/tests/test_analysis.py index b51915c..aa1e0f1 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -19,5 +19,68 @@ def test_kaplan_meier_handles_censoring() -> None: def test_pathway_enrichment_and_omics_join() -> None: result = enrich_gene_set({"A", "B"}, {"pathway": {"A", "B", "C"}}, {"A", "B", "C", "D"}) assert result[0].overlap == 2 - joined = integrate_samples([MultiOmicsSample("S1", "P1", OmicsLayer.GENOMICS, {"A": 1.0})]) + joined = integrate_samples( + [ + MultiOmicsSample("S1", "P1", OmicsLayer.GENOMICS, {"A": 1.0}), + MultiOmicsSample("S1", "P1", OmicsLayer.TRANSCRIPTOMICS, {"B": None}), + ] + ) assert joined["S1"]["genomics"]["A"] == 1.0 + assert joined["S1"]["transcriptomics"]["B"] is None + + +def test_pathway_enrichment_uses_full_valid_family_for_bh_adjustment() -> None: + results = enrich_gene_set( + {"A", "B"}, + { + "direct": {"A", "B"}, + "partial": {"A", "C"}, + "no_observed_overlap": {"E", "F"}, + "outside_universe": {"X", "Y"}, + }, + {"A", "B", "C", "D", "E", "F"}, + ) + + assert [result.pathway for result in results] == ["direct", "partial"] + assert results[0].p_value == 1 / 15 + assert results[0].adjusted_p_value == 0.2 + assert [result.adjusted_p_value for result in results] == sorted( + result.adjusted_p_value for result in results + ) + + +def test_pathway_enrichment_rejects_genes_outside_universe() -> None: + try: + enrich_gene_set({"A", "X"}, {"pathway": {"A"}}, {"A", "B"}) + except ValueError as exc: + assert "genes must be non-empty" in str(exc) + else: + raise AssertionError("Expected genes outside universe to be rejected") + + +def test_multi_omics_rejects_duplicate_sample_layer_pairs() -> None: + samples = [ + MultiOmicsSample("S1", "P1", OmicsLayer.GENOMICS, {"A": 1.0}), + MultiOmicsSample("S1", "P1", OmicsLayer.GENOMICS, {"A": 2.0}), + ] + + try: + integrate_samples(samples) + except ValueError as exc: + assert "duplicate sample_id and layer" in str(exc) + else: + raise AssertionError("Expected duplicate sample/layer pairs to be rejected") + + +def test_multi_omics_rejects_inconsistent_participant_mapping() -> None: + samples = [ + MultiOmicsSample("S1", "P1", OmicsLayer.GENOMICS, {"A": 1.0}), + MultiOmicsSample("S1", "P2", OmicsLayer.TRANSCRIPTOMICS, {"B": 2.0}), + ] + + try: + integrate_samples(samples) + except ValueError as exc: + assert "one participant_id" in str(exc) + else: + raise AssertionError("Expected inconsistent sample participant mapping to fail") From f7bb2a0270f89f9babcddde70dcd71ee2273cb7d Mon Sep 17 00:00:00 2001 From: Ciprian-LocalPulse Date: Wed, 23 Sep 2026 14:41:02 +0300 Subject: [PATCH 2/8] fix: make evidence scoring reproducible --- CHANGELOG.md | 2 +- README.md | 2 +- ROADMAP.md | 2 +- docs/API.md | 2 +- docs/audits/v0.3.0-baseline.md | 2 +- src/openlongevity/evidence.py | 35 ++++++++++++++++++++++++++-------- tests/test_core.py | 22 +++++++++++++++++++++ 7 files changed, 54 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe429a8..0f40105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Documentation tooling now counts prose separately from fenced code and diagrams, The abbreviated license file has been replaced with the official Apache License 2.0 text. Project attribution is recorded separately in NOTICE, and citation metadata identifies the author as an independent Romanian researcher. The existing release tag is preserved. The citation's release version is not silently advanced to an unpublished development version merely because the Python package declares that version. -Two analysis-integrity issues have targeted corrections after the documentation baseline. Pathway enrichment now applies monotonic Benjamini–Hochberg adjustment over the full valid pathway family, including valid pathways with no observed overlap in the correction denominator. Multi-omics grouping now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting earlier values. The frontend now exposes key provenance boundaries and has build-plus-browser smoke coverage, but it still needs broader accessibility, visual regression, and production deployment evidence. Publication origin classification is improved, but provider origin remains a retrieval-boundary label rather than scientific validation. No clinical validation, independent benchmark, or operational scientific review service is asserted by this documentation entry. +Analysis-integrity work now includes reproducible time handling for navigation scores, plus the two targeted corrections recorded after the documentation baseline. Pathway enrichment now applies monotonic Benjamini–Hochberg adjustment over the full valid pathway family, including valid pathways with no observed overlap in the correction denominator. Multi-omics grouping now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting earlier values. The frontend now exposes key provenance boundaries and has build-plus-browser smoke coverage, but it still needs broader accessibility, visual regression, and production deployment evidence. Publication origin classification is improved, but provider origin remains a retrieval-boundary label rather than scientific validation. No clinical validation, independent benchmark, or operational scientific review service is asserted by this documentation entry. ## [0.2.0] - 2026-09-14 diff --git a/README.md b/README.md index 222a742..a477ce7 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Only then consider a structured evidence observation. Define the study design, s The A–G taxonomy assigns systematic reviews to A, randomized trials to B, clinical studies to C, observational studies to D, animal studies to E, in-vitro studies to F, and computational work to G. This is the project's navigation convention. It is not a claim that every review is stronger than every experiment or a substitute for evaluating bias and relevance. The implementation also maps retracted records to G, so clients must retain original study design alongside publication status. -The numerical navigation score uses hand-selected design weights, confidence, replication metadata, sample size, and publication age. It is not a probability of truth, a treatment effect, or a validated evidence-certainty scale. Date-sensitive outputs can change with execution time. The [whitepaper](WHITEPAPER.md) now describes the actual arithmetic and its limitations. A research report should show the inputs and discuss whether conclusions depend on the ranking choices. +The numerical navigation score uses hand-selected design weights, confidence, replication metadata, sample size, and publication age. It is not a probability of truth, a treatment effect, or a validated evidence-certainty scale. Date-sensitive outputs can now be frozen by passing an explicit scoring time to the evidence engine; API summaries also expose `scoring_as_of` so exported interpretations can record the temporal basis. The [whitepaper](WHITEPAPER.md) describes the actual arithmetic and its limitations. A research report should show the inputs and discuss whether conclusions depend on the ranking choices. Experimental analysis utilities need similarly narrow interpretation. The biological-age routine is an ordinary-least-squares baseline. The survival utility produces Kaplan–Meier points without a complete inference framework. Multi-omics integration now rejects duplicate sample-layer pairs and inconsistent participant mappings, but it still does not perform feature harmonization, statistical batch correction, or biological interpretation. The pathway routine now applies a monotonic Benjamini–Hochberg correction across the valid pathway hypothesis family, while results still depend on the declared universe, pathway annotations, and upstream gene selection. These are reasons to inspect methods before drawing scientific conclusions, not details to hide behind a general research-use label. diff --git a/ROADMAP.md b/ROADMAP.md index 8dd01cf..c7e18da 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -38,7 +38,7 @@ Publication-origin classification has moved from a fixed false synthetic flag to Complete the documentation expansion with distinct, source-backed explanations rather than repeated filler. Every tracked Markdown file has a minimum prose target, but accuracy and implementation alignment remain separate requirements. The automated inventory should continue to expose short documents, missing attribution, and structural problems. Code examples need execution checks, and diagrams need to identify proposed components clearly. A passing word-count gate is not a substitute for editorial review. -Continue resolving analysis limitations through focused changes and reference cases. Pathway enrichment now defines the tested family as pathways with at least one member in the supplied universe and applies monotonic Benjamini–Hochberg adjustment while returning only observed overlaps. Multi-omics integration now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting values. Remaining analysis priorities include a controllable or clearly recorded time basis for date-sensitive scoring, explicit manifests for preprocessing metadata, broader edge-case fixtures, and downstream methods that keep their scientific assumptions visible. Each change should state its effect on existing outputs and should not be promoted as clinical validation merely because a regression test passes. +Continue resolving analysis limitations through focused changes and reference cases. Pathway enrichment now defines the tested family as pathways with at least one member in the supplied universe and applies monotonic Benjamini–Hochberg adjustment while returning only observed overlaps. Multi-omics integration now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting values. Evidence scoring now accepts an explicit `as_of` time and exposes the summary timestamp, reducing one reproducibility risk in date-sensitive navigation scores. Remaining analysis priorities include explicit manifests for preprocessing metadata, broader edge-case fixtures, and downstream methods that keep their scientific assumptions visible. Each change should state its effect on existing outputs and should not be promoted as clinical validation merely because a regression test passes. ## Publication-path acceptance diff --git a/docs/API.md b/docs/API.md index 0ceeb91..5feb1c5 100644 --- a/docs/API.md +++ b/docs/API.md @@ -85,7 +85,7 @@ The citation export route should not be described as a complete publication expo `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. The score also depends on execution time when a publication date is present. 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; evidence summaries now expose `scoring_as_of` so clients can record that temporal basis. 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/docs/audits/v0.3.0-baseline.md b/docs/audits/v0.3.0-baseline.md index 5d27fd4..76337c8 100644 --- a/docs/audits/v0.3.0-baseline.md +++ b/docs/audits/v0.3.0-baseline.md @@ -93,7 +93,7 @@ Two analysis utilities now have narrower and more defensible contracts than they The multi-omics grouping helper now rejects duplicate sample-layer pairs and rejects a sample identifier that maps to more than one participant identifier. This removes the previous silent-overwrite behavior. The utility still returns a compact layer map for each sample and preserves explicit missing feature values as `None`. It does not perform batch correction, feature harmonization, unit conversion, privacy review, or biological integration. Those remain downstream scientific tasks. -Verification for this follow-up added targeted regression cases for the pathway hypothesis family, monotonic adjusted p-values, genes outside the universe, missing omics values, duplicate omics layers, and inconsistent participant mappings. The local focused check reported `ruff check` passing and the analysis test module passing with seven tests. This is implementation evidence for the stated software contracts, not evidence that any pathway result or multi-omics association is biologically valid. +Verification for this follow-up added targeted regression cases for the pathway hypothesis family, monotonic adjusted p-values, genes outside the universe, missing omics values, duplicate omics layers, and inconsistent participant mappings. The local focused check reported `ruff check` passing and the analysis test module passing with seven tests. This is implementation evidence for the stated software contracts, not evidence that any pathway result or multi-omics association is biologically valid. A subsequent scoring reproducibility check adds an explicit `as_of` parameter for navigation-score age calculations and records `scoring_as_of` in evidence summaries, reducing drift caused by executing the same report on different dates. This audit records project responsibility under Ciprian Ștefan Pleșca, an independent Romanian researcher. It does not claim an external audit institution, university affiliation, scientific peer review, or security certification. Its contribution is a traceable account of what was inspected, what changed, what was actually checked, and what remains unresolved. Future updates should preserve that separation so that readers can evaluate progress without having to infer completion from presentation quality. diff --git a/src/openlongevity/evidence.py b/src/openlongevity/evidence.py index 55b9ad5..0ff4fc2 100644 --- a/src/openlongevity/evidence.py +++ b/src/openlongevity/evidence.py @@ -45,6 +45,17 @@ class EvidenceLevel(StrEnum): } +def _utc_datetime(value: datetime) -> datetime: + if value.tzinfo is None: + return value.replace(tzinfo=UTC) + return value.astimezone(UTC) + + +def _parse_publication_datetime(value: str) -> datetime: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + return _utc_datetime(parsed) + + class EvidenceEngine: """Grade and summarize records without implying clinical effectiveness.""" @@ -53,8 +64,13 @@ def grade(self, record: EvidenceRecord) -> EvidenceLevel: return EvidenceLevel.G return _LEVEL_BY_TYPE[record.study_type] - def score(self, record: EvidenceRecord) -> float: - """Return a transparent navigation score, not a validated effect estimate.""" + def score(self, record: EvidenceRecord, *, as_of: datetime | None = None) -> float: + """Return a transparent navigation score, not a validated effect estimate. + + ``as_of`` freezes the publication-age component for reproducible reports. When + omitted, the current UTC time preserves the historical runtime behavior. + """ + scoring_time = _utc_datetime(as_of) if as_of else datetime.now(UTC) score = _BASE_SCORE[record.study_type] * record.confidence if record.replication_status.casefold() in {"replicated", "independent"}: score *= 1.15 @@ -68,10 +84,7 @@ def score(self, record: EvidenceRecord) -> float: try: age_years = max( 0.0, - ( - datetime.now(UTC) - - datetime.fromisoformat(record.publication_date).replace(tzinfo=UTC) - ).days + (scoring_time - _parse_publication_datetime(record.publication_date)).days / 365.25, ) score *= max(0.75, 1.0 - age_years * 0.01) @@ -79,9 +92,12 @@ def score(self, record: EvidenceRecord) -> float: pass return round(min(1.0, max(0.0, score)), 4) - def summarize(self, records: Iterable[EvidenceRecord]) -> dict[str, object]: + def summarize( + self, records: Iterable[EvidenceRecord], *, as_of: datetime | None = None + ) -> dict[str, object]: records = tuple(records) active = tuple(r for r in records if r.retraction_status is not RetractionStatus.RETRACTED) + scoring_time = _utc_datetime(as_of) if as_of else datetime.now(UTC) grades = Counter(self.grade(r).value for r in active) mean = sum(r.confidence for r in active) / len(active) if active else 0.0 return { @@ -89,9 +105,12 @@ def summarize(self, records: Iterable[EvidenceRecord]) -> dict[str, object]: "active_records": len(active), "evidence_distribution": dict(sorted(grades.items())), "mean_confidence": round(mean, 4), - "mean_navigation_score": round(sum(self.score(r) for r in active) / len(active), 4) + "mean_navigation_score": round( + sum(self.score(r, as_of=scoring_time) for r in active) / len(active), 4 + ) if active else 0.0, + "scoring_as_of": scoring_time.isoformat(), "disclaimer": "Research use only. Not medical advice.", } diff --git a/tests/test_core.py b/tests/test_core.py index 96687ed..69ce346 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,3 +1,5 @@ +from datetime import UTC, datetime + from openlongevity.evidence import EvidenceEngine, EvidenceLevel from openlongevity.gaps import ResearchGapDetector from openlongevity.graph import EvidenceGraph @@ -28,6 +30,26 @@ def test_grading_and_retraction() -> None: ) +def test_navigation_score_accepts_explicit_scoring_time() -> None: + engine = EvidenceEngine() + dated = record( + "dated", + StudyType.RCT, + publication_date="2020-01-01", + replication_status="replicated", + sample_size=200, + ) + early = datetime(2021, 1, 1, tzinfo=UTC) + later = datetime(2031, 1, 1, tzinfo=UTC) + + assert engine.score(dated, as_of=early) == engine.score(dated, as_of=early) + assert engine.score(dated, as_of=early) > engine.score(dated, as_of=later) + + summary = engine.summarize([dated], as_of=early) + assert summary["scoring_as_of"] == "2021-01-01T00:00:00+00:00" + assert summary["mean_navigation_score"] == engine.score(dated, as_of=early) + + def test_gap_detector_flags_translation_gap() -> None: gaps = ResearchGapDetector().detect("senescence", [record("a", StudyType.ANIMAL)]) assert gaps[0].kind == "translational_gap" From 160cb4b286dcd5bdf93f8f6cb8c2d961f3bec88a Mon Sep 17 00:00:00 2001 From: Ciprian-LocalPulse Date: Wed, 23 Sep 2026 14:49:22 +0300 Subject: [PATCH 3/8] feat: expose reproducible evidence scoring time --- CHANGELOG.md | 2 +- docs/API.md | 2 +- src/openlongevity/api.py | 27 +++++++++++++++++++++++++-- tests/test_api.py | 22 ++++++++++++++++++++++ 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f40105..994fd8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Documentation tooling now counts prose separately from fenced code and diagrams, The abbreviated license file has been replaced with the official Apache License 2.0 text. Project attribution is recorded separately in NOTICE, and citation metadata identifies the author as an independent Romanian researcher. The existing release tag is preserved. The citation's release version is not silently advanced to an unpublished development version merely because the Python package declares that version. -Analysis-integrity work now includes reproducible time handling for navigation scores, plus the two targeted corrections recorded after the documentation baseline. Pathway enrichment now applies monotonic Benjamini–Hochberg adjustment over the full valid pathway family, including valid pathways with no observed overlap in the correction denominator. Multi-omics grouping now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting earlier values. The frontend now exposes key provenance boundaries and has build-plus-browser smoke coverage, but it still needs broader accessibility, visual regression, and production deployment evidence. Publication origin classification is improved, but provider origin remains a retrieval-boundary label rather than scientific validation. No clinical validation, independent benchmark, or operational scientific review service is asserted by this documentation entry. +Analysis-integrity work now includes reproducible time handling for navigation scores through both the evidence engine and the evidence API, plus the two targeted corrections recorded after the documentation baseline. Pathway enrichment now applies monotonic Benjamini–Hochberg adjustment over the full valid pathway family, including valid pathways with no observed overlap in the correction denominator. Multi-omics grouping now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting earlier values. The frontend now exposes key provenance boundaries and has build-plus-browser smoke coverage, but it still needs broader accessibility, visual regression, and production deployment evidence. Publication origin classification is improved, but provider origin remains a retrieval-boundary label rather than scientific validation. No clinical validation, independent benchmark, or operational scientific review service is asserted by this documentation entry. ## [0.2.0] - 2026-09-14 diff --git a/docs/API.md b/docs/API.md index 5feb1c5..c7db39c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -85,7 +85,7 @@ The citation export route should not be described as a complete publication expo `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; evidence summaries now expose `scoring_as_of` so clients can record that temporal basis. 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` 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. 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 3019f8a..ff37ee3 100644 --- a/src/openlongevity/api.py +++ b/src/openlongevity/api.py @@ -3,6 +3,7 @@ from collections.abc import AsyncIterator from contextlib import asynccontextmanager from dataclasses import asdict +from datetime import UTC, datetime from os import getenv from typing import Any, Literal @@ -168,6 +169,23 @@ def require_review_repository() -> EvidenceReviewRepository: "message": "Configure and migrate PostgreSQL first"}) return review_repository + def parse_scoring_as_of(value: str | None) -> datetime | None: + if value is None: + return None + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as exc: + raise HTTPException( + 422, + { + "code": "INVALID_SCORING_AS_OF", + "message": "scoring_as_of must be an ISO 8601 datetime", + }, + ) from exc + if parsed.tzinfo is None: + return parsed.replace(tzinfo=UTC) + return parsed.astimezone(UTC) + @app.get("/api/v1/health") async def health() -> dict[str, str]: return {"status": "ok", "version": __version__, @@ -251,13 +269,18 @@ async def current_records(records: list[EvidenceRecord]) -> list[EvidenceRecord] ) if record.identifier in events else record for record in records] @app.get("/api/v1/evidence") - async def evidence(topic: str = Query(default="", max_length=120)) -> dict[str, Any]: + async def evidence( + 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()] ) return {"items": [evidence_record_payload(r, synthetic=True, level=engine.grade(r).value) for r in records], "mode": "fixture-only", - "summary": engine.summarize(records), "disclaimer": DISCLAIMER} + "summary": engine.summarize(records, as_of=scoring_time), + "disclaimer": DISCLAIMER} @app.get("/api/v1/evidence/export/citation") async def citation_export(topic: str = Query(default="", max_length=120)) -> dict[str, Any]: diff --git a/tests/test_api.py b/tests/test_api.py index 86969ab..e0944b2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -71,6 +71,28 @@ def test_search_contract() -> None: assert search.json()["total"] >= 1 +def test_evidence_summary_accepts_explicit_scoring_time() -> None: + client = TestClient(create_app()) + response = client.get( + "/api/v1/evidence", + params={"topic": "senescence", "scoring_as_of": "2021-01-01T00:00:00Z"}, + ) + + assert response.status_code == 200 + assert response.json()["summary"]["scoring_as_of"] == "2021-01-01T00:00:00+00:00" + + +def test_evidence_summary_rejects_invalid_scoring_time() -> None: + client = TestClient(create_app()) + response = client.get( + "/api/v1/evidence", + 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_missing_evidence_is_structured() -> None: client = TestClient(create_app()) payload = client.get("/api/v1/evidence/unknown").json() From 64e88a79ba35e63b476bfd9b5c12398a40ba5cde Mon Sep 17 00:00:00 2001 From: Ciprian-LocalPulse Date: Wed, 23 Sep 2026 14:56:19 +0300 Subject: [PATCH 4/8] feat: expose record-level evidence score metadata --- CHANGELOG.md | 2 +- docs/API.md | 2 +- src/openlongevity/api.py | 43 +++++++++++++++++++++++++++--------- src/openlongevity/exports.py | 16 ++++++++++++-- tests/test_api.py | 26 ++++++++++++++++++++++ 5 files changed, 74 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 994fd8b..f2a7a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Documentation tooling now counts prose separately from fenced code and diagrams, The abbreviated license file has been replaced with the official Apache License 2.0 text. Project attribution is recorded separately in NOTICE, and citation metadata identifies the author as an independent Romanian researcher. The existing release tag is preserved. The citation's release version is not silently advanced to an unpublished development version merely because the Python package declares that version. -Analysis-integrity work now includes reproducible time handling for navigation scores through both the evidence engine and the evidence API, plus the two targeted corrections recorded after the documentation baseline. Pathway enrichment now applies monotonic Benjamini–Hochberg adjustment over the full valid pathway family, including valid pathways with no observed overlap in the correction denominator. Multi-omics grouping now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting earlier values. The frontend now exposes key provenance boundaries and has build-plus-browser smoke coverage, but it still needs broader accessibility, visual regression, and production deployment evidence. Publication origin classification is improved, but provider origin remains a retrieval-boundary label rather than scientific validation. No clinical validation, independent benchmark, or operational scientific review service is asserted by this documentation entry. +Analysis-integrity work now includes reproducible time handling and record-level score metadata for navigation scores through both the evidence engine and the evidence API, plus the two targeted corrections recorded after the documentation baseline. Pathway enrichment now applies monotonic Benjamini–Hochberg adjustment over the full valid pathway family, including valid pathways with no observed overlap in the correction denominator. Multi-omics grouping now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting earlier values. The frontend now exposes key provenance boundaries and has build-plus-browser smoke coverage, but it still needs broader accessibility, visual regression, and production deployment evidence. Publication origin classification is improved, but provider origin remains a retrieval-boundary label rather than scientific validation. No clinical validation, independent benchmark, or operational scientific review service is asserted by this documentation entry. ## [0.2.0] - 2026-09-14 diff --git a/docs/API.md b/docs/API.md index c7db39c..5d409e3 100644 --- a/docs/API.md +++ b/docs/API.md @@ -85,7 +85,7 @@ The citation export route should not be described as a complete publication expo `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. 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` 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`, and record-level `scoring_as_of` 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 ff37ee3..48da23c 100644 --- a/src/openlongevity/api.py +++ b/src/openlongevity/api.py @@ -19,7 +19,11 @@ from .constants import DISCLAIMER from .db import Database from .evidence import EvidenceEngine -from .exports import build_citation_export, evidence_record_payload +from .exports import ( + EVIDENCE_SCORE_METHOD_VERSION, + build_citation_export, + evidence_record_payload, +) from .gaps import ResearchGapDetector from .models import EvidenceRecord, ReviewStatus, StudyType from .origins import PublicationOrigin @@ -277,26 +281,43 @@ async def evidence( records = await current_records( [r for r in fixtures if topic.casefold() in r.title.casefold()] ) - return {"items": [evidence_record_payload(r, synthetic=True, level=engine.grade(r).value) + summary = engine.summarize(records, as_of=scoring_time) + scoring_time = datetime.fromisoformat(summary["scoring_as_of"]) + return {"items": [evidence_payload(r, synthetic=True, scoring_time=scoring_time) for r in records], "mode": "fixture-only", - "summary": engine.summarize(records, as_of=scoring_time), - "disclaimer": DISCLAIMER} + "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]: records = await current_records( [r for r in fixtures if topic.casefold() in r.title.casefold()] ) - payloads = [evidence_record_payload(r, synthetic=True, level=engine.grade(r).value) + summary = engine.summarize(records) + scoring_time = datetime.fromisoformat(summary["scoring_as_of"]) + payloads = [evidence_payload(r, synthetic=True, scoring_time=scoring_time) for r in records] return {**build_citation_export(payloads), "source_mode": "fixture-only"} @app.get("/api/v1/evidence/{identifier}") async def evidence_record(identifier: str) -> dict[str, Any]: record, = await current_records([fixture_by_identifier(identifier)]) - return {"item": evidence_record_payload( - record, synthetic=True, level=engine.grade(record).value, - ), "mode": "fixture-only", "disclaimer": DISCLAIMER} + summary = engine.summarize([record]) + scoring_time = datetime.fromisoformat(summary["scoring_as_of"]) + return {"item": evidence_payload(record, synthetic=True, scoring_time=scoring_time), + "mode": "fixture-only", "summary": summary, "disclaimer": DISCLAIMER} + + + def evidence_payload( + record: EvidenceRecord, *, synthetic: bool, scoring_time: datetime + ) -> dict[str, Any]: + return evidence_record_payload( + record, + synthetic=synthetic, + level=engine.grade(record).value, + navigation_score=engine.score(record, as_of=scoring_time), + score_method=EVIDENCE_SCORE_METHOD_VERSION, + scoring_as_of=scoring_time.isoformat(), + ) def fixture_by_identifier(identifier: str) -> EvidenceRecord: for record in fixtures: @@ -327,9 +348,9 @@ async def review_evidence_record( ) except ValueError as exc: raise HTTPException(422, {"code": "INVALID_REVIEW", "message": str(exc)}) from exc - payload = evidence_record_payload( - reviewed, synthetic=True, level=engine.grade(reviewed).value - ) + summary = engine.summarize([reviewed]) + scoring_time = datetime.fromisoformat(summary["scoring_as_of"]) + payload = evidence_payload(reviewed, synthetic=True, scoring_time=scoring_time) event = await require_review_repository().record_event( record_identifier=reviewed.identifier, status=reviewed.review_status.value, diff --git a/src/openlongevity/exports.py b/src/openlongevity/exports.py index e4b925a..197dfb9 100644 --- a/src/openlongevity/exports.py +++ b/src/openlongevity/exports.py @@ -8,13 +8,25 @@ from .models import EvidenceRecord, ReviewStatus CITATION_EXPORT_SCHEMA_VERSION = "citation-export-v1" +EVIDENCE_SCORE_METHOD_VERSION = "navigation-score-v1" def evidence_record_payload( - record: EvidenceRecord, *, synthetic: bool, level: str + record: EvidenceRecord, + *, + synthetic: bool, + level: str, + navigation_score: float | None = None, + score_method: str | None = None, + scoring_as_of: str | None = None, ) -> dict[str, Any]: """Serialize an evidence record with export-relevant boundary fields.""" - return {**asdict(record), "synthetic": synthetic, "level": level} + payload = {**asdict(record), "synthetic": synthetic, "level": level} + if navigation_score is not None: + payload["navigation_score"] = navigation_score + payload["score_method"] = score_method or EVIDENCE_SCORE_METHOD_VERSION + payload["scoring_as_of"] = scoring_as_of + return payload def is_synthetic_record(record: Mapping[str, Any]) -> bool: diff --git a/tests/test_api.py b/tests/test_api.py index e0944b2..f6de0fd 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -19,6 +19,9 @@ def test_evidence_without_database_retains_unreviewed_fixture( with TestClient(create_app()) as client: detail = client.get("/api/v1/evidence/SYN-001").json()["item"] listed = client.get("/api/v1/evidence").json()["items"][0] + for volatile_field in ("navigation_score", "score_method", "scoring_as_of"): + detail.pop(volatile_field, None) + listed.pop(volatile_field, None) assert detail == listed assert detail["review_status"] == "unreviewed" assert detail["synthetic"] is True @@ -99,6 +102,29 @@ def test_missing_evidence_is_structured() -> None: assert payload["error"]["code"] == "NOT_FOUND" +def test_evidence_items_include_navigation_score_metadata() -> None: + client = TestClient(create_app()) + payload = client.get( + "/api/v1/evidence", + params={"topic": "senescence", "scoring_as_of": "2021-01-01T00:00:00Z"}, + ).json() + + item = payload["items"][0] + assert item["navigation_score"] == payload["summary"]["mean_navigation_score"] + assert item["score_method"] == "navigation-score-v1" + assert item["scoring_as_of"] == "2021-01-01T00:00:00+00:00" + + +def test_evidence_detail_includes_navigation_score_metadata() -> None: + client = TestClient(create_app()) + response = client.get("/api/v1/evidence/SYN-001") + + assert response.status_code == 200 + payload = response.json() + assert payload["item"]["score_method"] == "navigation-score-v1" + assert payload["item"]["scoring_as_of"] == payload["summary"]["scoring_as_of"] + + def test_citation_export_excludes_synthetic_fixtures() -> None: client = TestClient(create_app()) response = client.get("/api/v1/evidence/export/citation", params={"topic": "senescence"}) From 9eb9b77c02c8a792bba5a6dada6ffd0d505f4b16 Mon Sep 17 00:00:00 2001 From: Ciprian-LocalPulse Date: Wed, 23 Sep 2026 15:00:07 +0300 Subject: [PATCH 5/8] feat: expose evidence score components --- CHANGELOG.md | 2 +- docs/API.md | 2 +- src/openlongevity/api.py | 1 + src/openlongevity/evidence.py | 74 ++++++++++++++++++++++++++--------- src/openlongevity/exports.py | 3 ++ tests/test_api.py | 2 + tests/test_core.py | 19 +++++++++ 7 files changed, 83 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a7a2f..730190a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Documentation tooling now counts prose separately from fenced code and diagrams, The abbreviated license file has been replaced with the official Apache License 2.0 text. Project attribution is recorded separately in NOTICE, and citation metadata identifies the author as an independent Romanian researcher. The existing release tag is preserved. The citation's release version is not silently advanced to an unpublished development version merely because the Python package declares that version. -Analysis-integrity work now includes reproducible time handling and record-level score metadata for navigation scores through both the evidence engine and the evidence API, plus the two targeted corrections recorded after the documentation baseline. Pathway enrichment now applies monotonic Benjamini–Hochberg adjustment over the full valid pathway family, including valid pathways with no observed overlap in the correction denominator. Multi-omics grouping now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting earlier values. The frontend now exposes key provenance boundaries and has build-plus-browser smoke coverage, but it still needs broader accessibility, visual regression, and production deployment evidence. Publication origin classification is improved, but provider origin remains a retrieval-boundary label rather than scientific validation. No clinical validation, independent benchmark, or operational scientific review service is asserted by this documentation entry. +Analysis-integrity work now includes reproducible time handling, record-level score metadata, and auditable score components for navigation scores through both the evidence engine and the evidence API, plus the two targeted corrections recorded after the documentation baseline. Pathway enrichment now applies monotonic Benjamini–Hochberg adjustment over the full valid pathway family, including valid pathways with no observed overlap in the correction denominator. Multi-omics grouping now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting earlier values. The frontend now exposes key provenance boundaries and has build-plus-browser smoke coverage, but it still needs broader accessibility, visual regression, and production deployment evidence. Publication origin classification is improved, but provider origin remains a retrieval-boundary label rather than scientific validation. No clinical validation, independent benchmark, or operational scientific review service is asserted by this documentation entry. ## [0.2.0] - 2026-09-14 diff --git a/docs/API.md b/docs/API.md index 5d409e3..8874231 100644 --- a/docs/API.md +++ b/docs/API.md @@ -85,7 +85,7 @@ The citation export route should not be described as a complete publication expo `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`, and record-level `scoring_as_of` 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` 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. 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 48da23c..ae380ff 100644 --- a/src/openlongevity/api.py +++ b/src/openlongevity/api.py @@ -317,6 +317,7 @@ def evidence_payload( navigation_score=engine.score(record, as_of=scoring_time), score_method=EVIDENCE_SCORE_METHOD_VERSION, scoring_as_of=scoring_time.isoformat(), + score_components=engine.score_components(record, as_of=scoring_time), ) def fixture_by_identifier(identifier: str) -> EvidenceRecord: diff --git a/src/openlongevity/evidence.py b/src/openlongevity/evidence.py index 0ff4fc2..aa701a4 100644 --- a/src/openlongevity/evidence.py +++ b/src/openlongevity/evidence.py @@ -64,33 +64,71 @@ def grade(self, record: EvidenceRecord) -> EvidenceLevel: return EvidenceLevel.G return _LEVEL_BY_TYPE[record.study_type] - def score(self, record: EvidenceRecord, *, as_of: datetime | None = None) -> float: - """Return a transparent navigation score, not a validated effect estimate. - - ``as_of`` freezes the publication-age component for reproducible reports. When - omitted, the current UTC time preserves the historical runtime behavior. - """ + def score_components( + self, record: EvidenceRecord, *, as_of: datetime | None = None + ) -> dict[str, float | str | None]: + """Return auditable navigation-score components for one evidence record.""" scoring_time = _utc_datetime(as_of) if as_of else datetime.now(UTC) - score = _BASE_SCORE[record.study_type] * record.confidence - if record.replication_status.casefold() in {"replicated", "independent"}: - score *= 1.15 - elif record.replication_status.casefold() in {"unreplicated", "unknown"}: - score *= 0.85 + base_score = _BASE_SCORE[record.study_type] + replication_status = record.replication_status.casefold() + replication_multiplier = 1.0 + if replication_status in {"replicated", "independent"}: + replication_multiplier = 1.15 + elif replication_status in {"unreplicated", "unknown"}: + replication_multiplier = 0.85 + + sample_size_multiplier = 1.0 if record.sample_size is not None: - score *= min(1.15, 0.85 + (record.sample_size / (record.sample_size + 200))) - if record.retraction_status is RetractionStatus.RETRACTED: - return 0.0 + sample_size_multiplier = min( + 1.15, 0.85 + (record.sample_size / (record.sample_size + 200)) + ) + + publication_age_years: float | None = None + publication_age_multiplier = 1.0 if record.publication_date: try: - age_years = max( + publication_age_years = max( 0.0, (scoring_time - _parse_publication_datetime(record.publication_date)).days / 365.25, ) - score *= max(0.75, 1.0 - age_years * 0.01) + publication_age_multiplier = max(0.75, 1.0 - publication_age_years * 0.01) except ValueError: - pass - return round(min(1.0, max(0.0, score)), 4) + publication_age_years = None + + retraction_multiplier = ( + 0.0 if record.retraction_status is RetractionStatus.RETRACTED else 1.0 + ) + raw_score = ( + base_score + * record.confidence + * replication_multiplier + * sample_size_multiplier + * publication_age_multiplier + * retraction_multiplier + ) + bounded_score = round(min(1.0, max(0.0, raw_score)), 4) + return { + "method": "navigation-score-v1", + "base_score": base_score, + "confidence": record.confidence, + "replication_multiplier": replication_multiplier, + "sample_size_multiplier": round(sample_size_multiplier, 6), + "publication_age_years": round(publication_age_years, 6) + if publication_age_years is not None + else None, + "publication_age_multiplier": round(publication_age_multiplier, 6), + "retraction_multiplier": retraction_multiplier, + "bounded_score": bounded_score, + } + + def score(self, record: EvidenceRecord, *, as_of: datetime | None = None) -> float: + """Return a transparent navigation score, not a validated effect estimate. + + ``as_of`` freezes the publication-age component for reproducible reports. When + omitted, the current UTC time preserves the historical runtime behavior. + """ + return float(self.score_components(record, as_of=as_of)["bounded_score"]) def summarize( self, records: Iterable[EvidenceRecord], *, as_of: datetime | None = None diff --git a/src/openlongevity/exports.py b/src/openlongevity/exports.py index 197dfb9..2ab86d2 100644 --- a/src/openlongevity/exports.py +++ b/src/openlongevity/exports.py @@ -19,6 +19,7 @@ def evidence_record_payload( navigation_score: float | None = None, score_method: str | None = None, scoring_as_of: str | None = None, + score_components: Mapping[str, Any] | None = None, ) -> dict[str, Any]: """Serialize an evidence record with export-relevant boundary fields.""" payload = {**asdict(record), "synthetic": synthetic, "level": level} @@ -26,6 +27,8 @@ def evidence_record_payload( payload["navigation_score"] = navigation_score payload["score_method"] = score_method or EVIDENCE_SCORE_METHOD_VERSION payload["scoring_as_of"] = scoring_as_of + if score_components is not None: + payload["score_components"] = dict(score_components) return payload diff --git a/tests/test_api.py b/tests/test_api.py index f6de0fd..2e38da3 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -113,6 +113,8 @@ def test_evidence_items_include_navigation_score_metadata() -> None: assert item["navigation_score"] == payload["summary"]["mean_navigation_score"] assert item["score_method"] == "navigation-score-v1" assert item["scoring_as_of"] == "2021-01-01T00:00:00+00:00" + assert item["score_components"]["bounded_score"] == item["navigation_score"] + assert item["score_components"]["method"] == "navigation-score-v1" def test_evidence_detail_includes_navigation_score_metadata() -> None: diff --git a/tests/test_core.py b/tests/test_core.py index 69ce346..8bfda85 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -50,6 +50,25 @@ def test_navigation_score_accepts_explicit_scoring_time() -> None: assert summary["mean_navigation_score"] == engine.score(dated, as_of=early) +def test_navigation_score_components_reconstruct_score() -> None: + engine = EvidenceEngine() + dated = record( + "dated-components", + StudyType.RCT, + publication_date="2020-01-01", + replication_status="replicated", + sample_size=200, + ) + as_of = datetime(2021, 1, 1, tzinfo=UTC) + + components = engine.score_components(dated, as_of=as_of) + + assert components["method"] == "navigation-score-v1" + assert components["base_score"] == 0.85 + assert components["replication_multiplier"] == 1.15 + assert components["bounded_score"] == engine.score(dated, as_of=as_of) + + def test_gap_detector_flags_translation_gap() -> None: gaps = ResearchGapDetector().detect("senescence", [record("a", StudyType.ANIMAL)]) assert gaps[0].kind == "translational_gap" From cf61b3d05b0585a2e92e5e75f694512433256ef3 Mon Sep 17 00:00:00 2001 From: Ciprian-LocalPulse Date: Wed, 23 Sep 2026 15:04:12 +0300 Subject: [PATCH 6/8] feat: add citation export manifest --- CHANGELOG.md | 2 +- src/openlongevity/api.py | 3 ++- src/openlongevity/exports.py | 24 +++++++++++++++++++++++- tests/test_api.py | 4 ++++ tests/test_exports.py | 24 ++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 730190a..6c37941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Documentation tooling now counts prose separately from fenced code and diagrams, The abbreviated license file has been replaced with the official Apache License 2.0 text. Project attribution is recorded separately in NOTICE, and citation metadata identifies the author as an independent Romanian researcher. The existing release tag is preserved. The citation's release version is not silently advanced to an unpublished development version merely because the Python package declares that version. -Analysis-integrity work now includes reproducible time handling, record-level score metadata, and auditable score components for navigation scores through both the evidence engine and the evidence API, plus the two targeted corrections recorded after the documentation baseline. Pathway enrichment now applies monotonic Benjamini–Hochberg adjustment over the full valid pathway family, including valid pathways with no observed overlap in the correction denominator. Multi-omics grouping now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting earlier values. The frontend now exposes key provenance boundaries and has build-plus-browser smoke coverage, but it still needs broader accessibility, visual regression, and production deployment evidence. Publication origin classification is improved, but provider origin remains a retrieval-boundary label rather than scientific validation. No clinical validation, independent benchmark, or operational scientific review service is asserted by this documentation entry. +Analysis-integrity work now includes reproducible time handling, record-level score metadata, auditable score components, and deterministic citation-export manifests for navigation scores through both the evidence engine and the evidence API, plus the two targeted corrections recorded after the documentation baseline. Pathway enrichment now applies monotonic Benjamini–Hochberg adjustment over the full valid pathway family, including valid pathways with no observed overlap in the correction denominator. Multi-omics grouping now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting earlier values. The frontend now exposes key provenance boundaries and has build-plus-browser smoke coverage, but it still needs broader accessibility, visual regression, and production deployment evidence. Publication origin classification is improved, but provider origin remains a retrieval-boundary label rather than scientific validation. No clinical validation, independent benchmark, or operational scientific review service is asserted by this documentation entry. ## [0.2.0] - 2026-09-14 diff --git a/src/openlongevity/api.py b/src/openlongevity/api.py index ae380ff..4cad042 100644 --- a/src/openlongevity/api.py +++ b/src/openlongevity/api.py @@ -296,7 +296,8 @@ async def citation_export(topic: str = Query(default="", max_length=120)) -> dic scoring_time = datetime.fromisoformat(summary["scoring_as_of"]) payloads = [evidence_payload(r, synthetic=True, scoring_time=scoring_time) for r in records] - return {**build_citation_export(payloads), "source_mode": "fixture-only"} + return {**build_citation_export(payloads, source_mode="fixture-only"), + "source_mode": "fixture-only"} @app.get("/api/v1/evidence/{identifier}") async def evidence_record(identifier: str) -> dict[str, Any]: diff --git a/src/openlongevity/exports.py b/src/openlongevity/exports.py index 2ab86d2..1d8d82c 100644 --- a/src/openlongevity/exports.py +++ b/src/openlongevity/exports.py @@ -1,5 +1,6 @@ """Export helpers that preserve the fixture/observation boundary.""" +from collections import Counter from collections.abc import Mapping from dataclasses import asdict from typing import Any @@ -56,7 +57,9 @@ def is_human_verified_record(record: Mapping[str, Any]) -> bool: ) -def build_citation_export(records: list[Mapping[str, Any]]) -> dict[str, Any]: +def build_citation_export( + records: list[Mapping[str, Any]], *, source_mode: str = "unspecified" +) -> dict[str, Any]: """Build a citation-eligible export that excludes synthetic fixtures by default.""" included: list[Mapping[str, Any]] = [] excluded: list[dict[str, str]] = [] @@ -78,9 +81,28 @@ def build_citation_export(records: list[Mapping[str, Any]]) -> dict[str, Any]: }) continue included.append(record) + + exclusion_reasons = Counter(item["reason"] for item in excluded) + score_methods = sorted( + {str(record["score_method"]) for record in records if record.get("score_method")} + ) + scoring_times = sorted( + {str(record["scoring_as_of"]) for record in records if record.get("scoring_as_of")} + ) + manifest = { + "schema_version": CITATION_EXPORT_SCHEMA_VERSION, + "source_mode": source_mode, + "input_records": len(records), + "included_records": len(included), + "excluded_records": len(excluded), + "exclusion_reasons": dict(sorted(exclusion_reasons.items())), + "score_methods": score_methods, + "scoring_as_of": scoring_times, + } return { "schema_version": CITATION_EXPORT_SCHEMA_VERSION, "mode": "citation-eligible", + "manifest": manifest, "items": included, "excluded": excluded, "total": len(included), diff --git a/tests/test_api.py b/tests/test_api.py index 2e38da3..e979733 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -134,6 +134,10 @@ def test_citation_export_excludes_synthetic_fixtures() -> None: payload = response.json() assert payload["mode"] == "citation-eligible" assert payload["source_mode"] == "fixture-only" + assert payload["manifest"]["source_mode"] == "fixture-only" + assert payload["manifest"]["input_records"] == 1 + assert payload["manifest"]["exclusion_reasons"] == {"synthetic_fixture": 1} + assert payload["manifest"]["score_methods"] == ["navigation-score-v1"] assert payload["items"] == [] assert payload["total"] == 0 assert payload["excluded_total"] == 1 diff --git a/tests/test_exports.py b/tests/test_exports.py index 398b0c8..3ce736e 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -43,3 +43,27 @@ def test_citation_export_excludes_synthetic_before_review_check() -> None: assert payload["items"] == [] assert payload["excluded_total"] == 1 assert payload["excluded"][0]["reason"] == "synthetic_fixture" + + +def test_citation_export_manifest_summarizes_boundary_and_scoring() -> None: + synthetic = evidence_record_payload( + record("SYN-TEST"), + synthetic=True, + level="F", + navigation_score=0.12, + score_method="navigation-score-v1", + scoring_as_of="2021-01-01T00:00:00+00:00", + ) + + payload = build_citation_export([synthetic], source_mode="fixture-only") + + assert payload["manifest"] == { + "schema_version": "citation-export-v1", + "source_mode": "fixture-only", + "input_records": 1, + "included_records": 0, + "excluded_records": 1, + "exclusion_reasons": {"synthetic_fixture": 1}, + "score_methods": ["navigation-score-v1"], + "scoring_as_of": ["2021-01-01T00:00:00+00:00"], + } From 811da95e6b0ca2493e29cf79888cde9986263348 Mon Sep 17 00:00:00 2001 From: Ciprian-LocalPulse Date: Wed, 23 Sep 2026 15:09:12 +0300 Subject: [PATCH 7/8] feat: add citation export fingerprint --- CHANGELOG.md | 2 +- docs/API.md | 2 +- src/openlongevity/exports.py | 11 +++++++++++ tests/test_api.py | 2 ++ tests/test_exports.py | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c37941..a36a09a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Documentation tooling now counts prose separately from fenced code and diagrams, The abbreviated license file has been replaced with the official Apache License 2.0 text. Project attribution is recorded separately in NOTICE, and citation metadata identifies the author as an independent Romanian researcher. The existing release tag is preserved. The citation's release version is not silently advanced to an unpublished development version merely because the Python package declares that version. -Analysis-integrity work now includes reproducible time handling, record-level score metadata, auditable score components, and deterministic citation-export manifests for navigation scores through both the evidence engine and the evidence API, plus the two targeted corrections recorded after the documentation baseline. Pathway enrichment now applies monotonic Benjamini–Hochberg adjustment over the full valid pathway family, including valid pathways with no observed overlap in the correction denominator. Multi-omics grouping now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting earlier values. The frontend now exposes key provenance boundaries and has build-plus-browser smoke coverage, but it still needs broader accessibility, visual regression, and production deployment evidence. Publication origin classification is improved, but provider origin remains a retrieval-boundary label rather than scientific validation. No clinical validation, independent benchmark, or operational scientific review service is asserted by this documentation entry. +Analysis-integrity work now includes reproducible time handling, record-level score metadata, auditable score components, deterministic citation-export manifests, and SHA-256 export fingerprints for navigation scores through both the evidence engine and the evidence API, plus the two targeted corrections recorded after the documentation baseline. Pathway enrichment now applies monotonic Benjamini–Hochberg adjustment over the full valid pathway family, including valid pathways with no observed overlap in the correction denominator. Multi-omics grouping now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting earlier values. The frontend now exposes key provenance boundaries and has build-plus-browser smoke coverage, but it still needs broader accessibility, visual regression, and production deployment evidence. Publication origin classification is improved, but provider origin remains a retrieval-boundary label rather than scientific validation. No clinical validation, independent benchmark, or operational scientific review service is asserted by this documentation entry. ## [0.2.0] - 2026-09-14 diff --git a/docs/API.md b/docs/API.md index 8874231..b43eaa4 100644 --- a/docs/API.md +++ b/docs/API.md @@ -79,7 +79,7 @@ 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, persistent publication manifests, 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. Future work can extend the same contract to persisted publication records once review status, source authenticity, and export manifests 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. 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. diff --git a/src/openlongevity/exports.py b/src/openlongevity/exports.py index 1d8d82c..cf7060b 100644 --- a/src/openlongevity/exports.py +++ b/src/openlongevity/exports.py @@ -1,5 +1,7 @@ """Export helpers that preserve the fixture/observation boundary.""" +import hashlib +import json from collections import Counter from collections.abc import Mapping from dataclasses import asdict @@ -12,6 +14,12 @@ EVIDENCE_SCORE_METHOD_VERSION = "navigation-score-v1" +def _stable_fingerprint(payload: Mapping[str, Any]) -> str: + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + + def evidence_record_payload( record: EvidenceRecord, *, @@ -95,10 +103,13 @@ def build_citation_export( "input_records": len(records), "included_records": len(included), "excluded_records": len(excluded), + "included_identifiers": [str(record.get("identifier", "")) for record in included], + "excluded_identifiers": [item["identifier"] for item in excluded], "exclusion_reasons": dict(sorted(exclusion_reasons.items())), "score_methods": score_methods, "scoring_as_of": scoring_times, } + manifest["export_fingerprint"] = _stable_fingerprint(manifest) return { "schema_version": CITATION_EXPORT_SCHEMA_VERSION, "mode": "citation-eligible", diff --git a/tests/test_api.py b/tests/test_api.py index e979733..68e4463 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -138,6 +138,8 @@ def test_citation_export_excludes_synthetic_fixtures() -> None: assert payload["manifest"]["input_records"] == 1 assert payload["manifest"]["exclusion_reasons"] == {"synthetic_fixture": 1} assert payload["manifest"]["score_methods"] == ["navigation-score-v1"] + assert payload["manifest"]["excluded_identifiers"] == ["SYN-001"] + assert len(payload["manifest"]["export_fingerprint"]) == 64 assert payload["items"] == [] assert payload["total"] == 0 assert payload["excluded_total"] == 1 diff --git a/tests/test_exports.py b/tests/test_exports.py index 3ce736e..065ecd4 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -63,7 +63,39 @@ def test_citation_export_manifest_summarizes_boundary_and_scoring() -> None: "input_records": 1, "included_records": 0, "excluded_records": 1, + "included_identifiers": [], + "excluded_identifiers": ["SYN-TEST"], "exclusion_reasons": {"synthetic_fixture": 1}, "score_methods": ["navigation-score-v1"], "scoring_as_of": ["2021-01-01T00:00:00+00:00"], + "export_fingerprint": payload["manifest"]["export_fingerprint"], + } + assert len(payload["manifest"]["export_fingerprint"]) == 64 + + +def test_citation_export_fingerprint_changes_with_export_boundary() -> None: + unverified = evidence_record_payload( + record("REAL-UNVERIFIED"), + synthetic=False, + level="D", + navigation_score=0.4, + score_method="navigation-score-v1", + scoring_as_of="2021-01-01T00:00:00+00:00", + ) + synthetic = evidence_record_payload( + record("SYN-TEST"), + synthetic=True, + level="F", + navigation_score=0.12, + score_method="navigation-score-v1", + scoring_as_of="2021-01-01T00:00:00+00:00", + ) + + first = build_citation_export([synthetic], source_mode="fixture-only") + second = build_citation_export([synthetic, unverified], source_mode="fixture-only") + + assert first["manifest"]["export_fingerprint"] != second["manifest"]["export_fingerprint"] + assert second["manifest"]["exclusion_reasons"] == { + "not_human_verified": 1, + "synthetic_fixture": 1, } From 95a2234aa88a98c54f74c6c8e6753fdbf1e2d85b Mon Sep 17 00:00:00 2001 From: Ciprian-LocalPulse Date: Wed, 23 Sep 2026 15:15:46 +0300 Subject: [PATCH 8/8] docs: add OpenLongevity impact article --- docs/research/openlongevity-impact-article.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/research/openlongevity-impact-article.md diff --git a/docs/research/openlongevity-impact-article.md b/docs/research/openlongevity-impact-article.md new file mode 100644 index 0000000..715d328 --- /dev/null +++ b/docs/research/openlongevity-impact-article.md @@ -0,0 +1,60 @@ +# OpenLongevityLab: An Open-Source Computational Repository for Transparent Aging Research, Evidence Mapping, and Reproducible Longevity Science + +**Principal Author:** Ciprian Ștefan Pleșca — Independent Romanian Researcher +**Repository:** [OpenLongevityLab](https://github.com/Ciprian-LocalPulse/OpenLongevityLab) +**Collaboration and Research Support:** [PayPal — agentflowenterprise](https://www.paypal.com/paypalme/agentflowenterprise) + +## Abstract + +OpenLongevityLab is an open-source computational research repository dedicated to the transparent organization, evaluation, and future synthesis of scientific evidence in aging and longevity research. The repository is not a medical product, a clinical decision system, or a claim that any intervention extends human lifespan. Its purpose is more foundational: to create a reproducible digital infrastructure through which scientific publications, evidence records, review status, provenance metadata, biological mechanisms, computational scores, and research gaps can be represented in a form that is inspectable by researchers and contributors. At its current stage, OpenLongevityLab is an advanced research prototype with implemented evidence models, provider-oriented publication infrastructure, synthetic fixture boundaries, API endpoints, documentation audits, scoring transparency, pathway and multi-omics utilities, browser smoke coverage, and academic documentation. Its final intended stage is a governed open-science platform capable of supporting reproducible evidence curation, benchmarked extraction, transparent review workflows, interoperable knowledge graphs, and responsible collaboration across computational biology, geroscience, data engineering, and scientific publishing. This article explains what the repository is for, where it stands now, what it is designed to become, and how collaborators or donors can support the work. + +## 1. Purpose of the Repository + +Aging research is one of the most complex scientific domains of the twenty-first century. It spans molecular biology, genetics, epigenetics, proteomics, metabolomics, inflammation, cellular senescence, mitochondrial dysfunction, clinical biomarkers, cohort studies, animal experiments, randomized trials, computational models, and philosophical questions about healthspan, lifespan, risk, and human responsibility. The volume of literature is too large for informal reading alone, yet the field is too nuanced for simplistic automation. A platform that merely scrapes papers and ranks them with a black-box score would not solve the problem. It could make the problem worse by giving false authority to incomplete summaries. + +OpenLongevityLab exists to address that gap. The repository is designed as transparent infrastructure for aging research, not as a shortcut around scientific judgment. Its central idea is that evidence should be represented with provenance, uncertainty, limitations, review status, and reproducible computational context. A paper is not only a title and abstract. It has a source, retrieval boundary, publication date, possible corrections, study design, species, endpoint, population, confidence, limitations, and interpretation boundary. A computational score is not proof. It is a navigation aid whose components should be visible. A fixture used for software testing is not real scientific evidence. It must be labeled and excluded from citation-eligible export by default. + +The repository therefore supports a disciplined open-science workflow. It helps researchers and developers ask: Where did this record come from? Is it synthetic or provider-derived? Is it reviewed? What scoring method was used? What time basis affected a date-sensitive score? What was excluded from export and why? What assumptions are implemented in code, and which ones remain future research protocols? These questions are essential for a field where overstatement can mislead both scientists and the public. + +## 2. Current Stage of Development + +OpenLongevityLab is currently in a research-prototype stage. This is an important distinction. It is no longer an empty concept or a static README, but it is also not a finished scientific platform. The repository contains real implementation work: Python domain models, evidence grading, navigation scoring, contradiction surfacing, fixture evidence, citation-export boundaries, provider-related publication infrastructure, PostgreSQL-oriented persistence, API routes, documentation tooling, academic notes, frontend smoke tests, and analysis utilities. It also includes explicit disclaimers and methodological boundaries so the repository does not pretend to be a clinical validation system. + +Recent development has strengthened the scientific integrity of the prototype. Pathway enrichment now uses a clearer hypothesis-family contract and monotonic Benjamini-Hochberg adjustment. Multi-omics integration now rejects duplicate sample-layer pairs and inconsistent participant mappings instead of silently overwriting records. Evidence scoring can now be frozen with an explicit `as_of` time, which makes date-sensitive outputs reproducible. Evidence records can carry `navigation_score`, `score_method`, `scoring_as_of`, and auditable `score_components`, allowing a reviewer to reconstruct the score rather than trust a mysterious number. Citation exports now include deterministic manifests and are being extended toward reproducible fingerprints, included and excluded identifiers, exclusion reasons, scoring methods, and temporal scoring context. + +The documentation layer is also unusually important in this repository. OpenLongevityLab includes academic notes, architecture decision records, limitations, governance documents, data-source notes, threat modeling, reproducibility guidance, and audit reports. This is not decorative writing. In a scientific infrastructure project, documentation is part of the evidence contract. If the software changes but the documentation does not, users cannot interpret outputs correctly. If the documentation claims maturity that the code does not support, the repository becomes misleading. The project therefore treats documentation as an active scientific artifact. + +At this stage, however, important limitations remain. The system does not yet provide validated biomedical extraction from full text. It does not establish clinical efficacy. It does not prove that a biomarker, pathway, supplement, drug, behavioral intervention, or molecular mechanism extends human life. It does not replace peer review, systematic review, medical judgment, or regulatory evaluation. Its current value is infrastructural: it builds the computational and conceptual scaffolding needed for more reliable open research. + +## 3. Intended Final Stage + +The final intended stage of OpenLongevityLab is a governed, reproducible, open-source platform for computational longevity science. In that mature form, the repository should support a pipeline from source retrieval to normalized publication records, from evidence extraction to human review, from reviewed records to citation-eligible exports, from individual findings to knowledge graphs, and from knowledge graphs to research-gap detection and hypothesis generation. Every stage should preserve provenance and uncertainty. + +A mature OpenLongevityLab should include benchmarked extraction pipelines. That means curated evaluation corpora, permitted-use datasets, annotation guidelines, inter-reviewer disagreement records, false-positive analysis, and versioned benchmark reports. Claims of extraction quality should come from measured performance, not from optimism. The system should be able to say which task it performs well, which task remains weak, and which inputs are outside scope. + +The platform should also develop stronger review workflows. Machine extraction may propose a candidate evidence record, but verification should remain a human action with reviewer identity, timestamp, notes, and status. A future reviewer interface should allow records to be marked as unreviewed, machine-extracted, human-reviewed, verified, disputed, or rejected according to clear rules. No score should automatically become verification. No attractive dashboard should hide uncertainty. + +Another final-stage goal is an interoperable longevity knowledge graph. Such a graph should connect genes, pathways, hallmarks, biomarkers, interventions, studies, model organisms, outcomes, and limitations. It should not imply causation merely because an edge exists. It should encode relationship types, source records, review status, and confidence boundaries. In its strongest form, the graph could help researchers identify underdeveloped mechanisms, contradictory evidence clusters, translation gaps between animal and human findings, and areas where new experiments or reviews are needed. + +The final stage also requires production-quality operations: deployment evidence, access control, monitoring, backup and recovery procedures, security review, dependency governance, data licensing review, and contributor processes. Open science does not mean uncontrolled science. Responsible openness requires clear boundaries, reproducible releases, and transparent governance. + +## 4. Why This Project Matters + +Longevity research attracts both serious science and exaggerated claims. Public interest is high, commercial incentives are strong, and the literature is technically difficult. Without transparent tools, non-specialists may confuse animal evidence with human evidence, preliminary biomarkers with clinical outcomes, correlation with causation, and software-generated summaries with verified conclusions. Even researchers can struggle to track conflicting findings across disciplines. + +OpenLongevityLab matters because it tries to make the evidence environment more honest. It does not promise immortality. It does not sell a treatment. It does not present a score as truth. Instead, it builds the infrastructure through which claims can be inspected, challenged, corrected, and improved. That is a more modest goal than hype, but it is also more scientifically durable. + +The repository may become useful to computational biologists who need structured evidence representations, software engineers who want to build transparent scientific tools, students learning evidence boundaries, independent researchers building open datasets, and collaborators interested in aging mechanisms. It may also support future educational material, reproducible examples, and public-interest research tools. + +## 5. Collaboration and Support + +OpenLongevityLab is led by **Ciprian Ștefan Pleșca**, an independent Romanian researcher and the principal author of the project. The repository is public at [https://github.com/Ciprian-LocalPulse/OpenLongevityLab](https://github.com/Ciprian-LocalPulse/OpenLongevityLab). Researchers, developers, reviewers, data curators, designers, and open-science supporters are welcome to inspect the repository, propose issues, contribute improvements, review documentation, test workflows, or help develop future evidence pipelines. + +Those who wish to support the research financially may donate through PayPal at [https://www.paypal.com/paypalme/agentflowenterprise](https://www.paypal.com/paypalme/agentflowenterprise). Donations can help sustain development time, infrastructure, documentation, testing, data curation, and future research tooling. Contributions should be understood as support for open research infrastructure, not as purchase of medical advice, clinical services, or guaranteed scientific outcomes. + +## Conclusion + +OpenLongevityLab is an ambitious open-source repository for transparent computational aging research. Its current stage is a serious research prototype with growing technical and academic foundations. Its final goal is a governed, reproducible, evidence-aware platform that helps the longevity field separate source evidence from interpretation, synthetic fixtures from observations, scores from truth, and hypotheses from validated conclusions. If developed responsibly, it can become a valuable open infrastructure layer for scientific collaboration in aging and longevity research. +--- + +**Project author: CIPRIAN ȘTEFAN PLEȘCA — cercetător român independent.**