diff --git a/Makefile b/Makefile index c330cef..e98c30d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build validate audit eval site graph track synthesize recap-dry clockbench fair repos check help +.PHONY: build validate audit eval site graph track synthesize recap-dry clockbench fair repos hub check help help: @echo "longevity-loop — an AI-native compounding loop for aging science" @@ -10,7 +10,8 @@ help: @echo " make clockbench Cross-clock disagreement benchmark (measures gaps-analysis G1)" @echo " make fair Score open aging datasets for FAIR/reproducibility → docs/FAIR.md (G2)" @echo " make repos Per-repo KG + agentic-tooling generation recipes → docs/REPOS.md (hub)" - @echo " make check validate + audit + eval + clockbench + fair + repos + build drift-gate (CI finish line)" + @echo " make hub Validate the JIT deep artifacts under hub/ (add --refresh for staleness)" + @echo " make check validate + audit + eval + clockbench + fair + repos + hub + build drift-gate (CI finish line)" build: python3 scripts/build.py @@ -59,8 +60,13 @@ fair: repos: python3 scripts/repos.py -# Finish line: data well-formed, still an AI-native loop, executions honestly eval'd -# (no fake ✅), the clock-disagreement + FAIR + repo-recipe artifacts self-verify, and docs match. +# Validate the hub's JIT deep artifacts (hub//: KG + skill + SHA-stamped manifest). +# `make hub` = offline integrity gate; `python3 scripts/hub_gen.py --refresh` = network staleness. +hub: + python3 scripts/hub_gen.py --check + +# Finish line: data well-formed, still an AI-native loop, executions honestly eval'd (no fake ✅), +# the clock-disagreement + FAIR + repo-recipe + hub artifacts self-verify, and docs match. check: validate audit eval python3 scripts/clockbench.py --selftest python3 scripts/fair.py --selftest @@ -68,4 +74,6 @@ check: validate audit eval python3 scripts/fair.py --check python3 scripts/repos.py --selftest python3 scripts/repos.py --check + python3 scripts/hub_gen.py --selftest + python3 scripts/hub_gen.py --check python3 scripts/build.py --check diff --git a/README.md b/README.md index 88af394..0627341 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ Sequenced bridge: convert the computational finding + your credential network (G

✅ Roadmap Execution

-**12/23 done (52%).** Each execution is a checkbox with a before→after eval; a box only ticks with a real result (`done` requires a non-pending before AND after — no evidence ⇒ not done, gated in CI). +**13/24 done (54%).** Each execution is a checkbox with a before→after eval; a box only ticks with a real result (`done` requires a non-pending before AND after — no evidence ⇒ not done, gated in CI). - [x] **E1** (P1 · Foundation) — Stand up the AI-native loop (repo + self-audit + gated `make check`) - [x] **E2** (P1 · Foundation) — Map the field into a verified knowledge base @@ -182,6 +182,7 @@ Sequenced bridge: convert the computational finding + your credential network (G - [x] **E21** (P1 · Foundation) — Problem map: top aging problems as a relationship graph + the one target + low-hanging fruit - [x] **E22** (P1 · Foundation) — Publish the tracker.json data contract for the interactive portfolio instance (data-first slice) - [x] **E23** (P1 · Foundation) — Hub architecture (knowledge·tooling·experts per cited repo) + the per-repo generation-recipe layer +- [x] **E24** (P1 · Foundation) — Hub Phase-2: JIT deep artifacts for one repo (pyaging) — knowledge graph + gated skill, SHA-stamped ### Eval reports — before → after @@ -210,6 +211,7 @@ Sequenced bridge: convert the computational finding + your credential network (G | ✅ E21 | a clear, visual answer to 'what are we shooting, and what's the quick win' | gaps ranked (gaps-analysis.md) but relationships + the single target + quick wins were implicit | docs/PROBLEMS.md — Mermaid relationship graph (G1 measurement = root bottleneck we target) + top-problems table + how/when/why + 3 low-hanging fruit with done-when; README pointer | | ✅ E22 | a stable, derived JSON contract the agentic-portfolio instance can consume (no drift) | no machine-readable mission/progress feed for an external interactive surface | site/tracker.json — mission + 48% progress + 21 milestones + roadmap + low-hanging + frontier + 12 hubs + links, generated by build_site.py, deployed on Pages | | ✅ E23 | a fresh, cheap, future-proof way to offer a KG + agentic tooling for each cited repo | cited repos listed in stack.yml but no KG/tooling per repo; no design for how to do it well | docs/HUB_ARCHITECTURE.md (index+JIT design; reuse graphify/understand/reverse; MCP; SHA-cached) + scripts/repos.py → docs/REPOS.md (22 repos, per-repo KG + reverse recipes), gated in make check | +| ✅ E24 | a real, grounded, provenance-stamped KG + skill generated from a cited repo, with a freshness gate | hub had the recipe layer only; no deep artifact proving the JIT layer works | hub/pyaging/ — knowledge-graph.json (20 nodes/edges) + compute-aging-clocks SKILL.md + manifest (SHA c2b3000e3c2c); scripts/hub_gen.py gates integrity (make check) + checks live staleness (FRESH); grounded in pyaging's real API | --- @@ -232,7 +234,7 @@ Sequenced bridge: convert the computational finding + your credential network (G

🧩 Knowledge · Tooling · Experts hub

-This repo is a **hub**: for each cited repo it offers a knowledge graph (knowledge) + agentic tooling (skills/plugins/workflows), and links them to the experts who build them. Design (high-quality · fast · cheap · fresh · future-proof): **[docs/HUB_ARCHITECTURE.md](docs/HUB_ARCHITECTURE.md)** · per-repo generation recipes: **[docs/REPOS.md](docs/REPOS.md)**. Fresh index, just-in-time depth — deep artifacts are generated on demand and cached by commit SHA, never committed stale. +This repo is a **hub**: for each cited repo it offers a knowledge graph (knowledge) + agentic tooling (skills/plugins/workflows), and links them to the experts who build them. Design (high-quality · fast · cheap · fresh · future-proof): **[docs/HUB_ARCHITECTURE.md](docs/HUB_ARCHITECTURE.md)** · per-repo generation recipes: **[docs/REPOS.md](docs/REPOS.md)**. Fresh index, just-in-time depth — deep artifacts are generated on demand and cached by commit SHA, never committed stale. First deep exemplar (Phase 2): **[hub/pyaging](hub/pyaging)** — a grounded knowledge graph + a gated skill, SHA-stamped, with a live staleness check (`make hub` / `scripts/hub_gen.py --refresh`). --- diff --git a/data/executions.yml b/data/executions.yml index afabc6a..ae46664 100644 --- a/data/executions.yml +++ b/data/executions.yml @@ -168,3 +168,12 @@ before: "cited repos listed in stack.yml but no KG/tooling per repo; no design for how to do it well" after: "docs/HUB_ARCHITECTURE.md (index+JIT design; reuse graphify/understand/reverse; MCP; SHA-cached) + scripts/repos.py → docs/REPOS.md (22 repos, per-repo KG + reverse recipes), gated in make check" proof: "docs/HUB_ARCHITECTURE.md · scripts/repos.py · docs/REPOS.md · make check" +- id: E24 + phase: "P1 · Foundation" + item: "Hub Phase-2: JIT deep artifacts for one repo (pyaging) — knowledge graph + gated skill, SHA-stamped" + done: true + eval: + metric: "a real, grounded, provenance-stamped KG + skill generated from a cited repo, with a freshness gate" + before: "hub had the recipe layer only; no deep artifact proving the JIT layer works" + after: "hub/pyaging/ — knowledge-graph.json (20 nodes/edges) + compute-aging-clocks SKILL.md + manifest (SHA c2b3000e3c2c); scripts/hub_gen.py gates integrity (make check) + checks live staleness (FRESH); grounded in pyaging's real API" + proof: "hub/pyaging/* · scripts/hub_gen.py · docs/HUB_ARCHITECTURE.md" diff --git a/hub/pyaging/SKILL.md b/hub/pyaging/SKILL.md new file mode 100644 index 0000000..265e05e --- /dev/null +++ b/hub/pyaging/SKILL.md @@ -0,0 +1,56 @@ +--- +name: compute-aging-clocks +description: Compute a panel of biological-age (epigenetic) clocks from methylation/omics data with pyaging, then hand the panel to clockbench to quantify cross-clock disagreement. Trigger when you need biological-age estimates, a clock panel, or the disagreement signal for the Biomarkers-of-Aging / Turn-01 work. +source: rsinghlab/pyaging @ c2b3000e3c2c +license: BSD-3-Clause (pyaging) +generated_by: longevity-loop hub (scripts/hub_gen.py) — grounded in the pyaging repo + docs, not invented +--- + +# compute-aging-clocks (via pyaging) + +Generated by the longevity-loop **hub** (docs/HUB_ARCHITECTURE.md) as the Phase-2 exemplar of +just-in-time deep tooling. Grounded in pyaging's real API at the pinned `source` SHA — regenerate +when that SHA changes (`scripts/hub_gen.py --refresh` flags staleness). + +## When to use +You have methylation (or RNA-seq / ATAC / blood-chemistry) data and need biological-age estimates +from a **panel** of clocks — e.g. to test whether cross-clock *disagreement* adds signal +(gaps-analysis G1, Turn 01), or to score biological age for the Biolearn leaderboard. + +## Install +```bash +pip install pyaging # PyTorch-based; GPU optional but faster +``` + +## The workflow (pyaging's real API) +```python +import pyaging as pya +import pandas as pd + +# 1. Load your data as a DataFrame (rows = samples, cols = features/CpGs), then -> AnnData. +# (EPICv2 arrays: aggregate probes first.) +# df = pya.pp.epicv2_probe_aggregation(df) # only for EPICv2 +adata = pya.pp.df_to_adata(df, metadata_cols=["gender", "tissue_type", "dataset"], + imputer_strategy="knn") + +# 2. Predict a PANEL of clocks in one call (170+ available; these are real clock names). +pya.pred.predict_age(adata, ["Horvath2013", "AltumAge", "PCGrimAge", "GrimAge2", "DunedinPACE"]) + +# 3. Read results — one column per clock in adata.obs; clock metadata in adata.uns. +print(adata.obs.head()) # predicted ages per sample, per clock +# adata.uns["Horvath2013_metadata"], adata.uns["_missing_features"] +``` + +## Hand off to the loop (why this skill exists) +Export the per-clock predicted ages and feed them to this repo's benchmark to get the +**cross-clock disagreement** number (the measurable core of G1): +```bash +# adata.obs[clock_cols] -> a CSV (rows = samples, header = clock names), then: +python3 scripts/clockbench.py --input clocks.csv +``` +That closes the loop: pyaging (compute the panel) → clockbench (measure disagreement) → Turn 01. + +## Discipline +Predicted ages are **computational estimates on your data** — label them as such and keep them +separate from any wet-lab/clinical claim. Verify the clock names against the current pyaging +ModelGallery (they evolve); `no evidence ⇒ no claim`. diff --git a/hub/pyaging/knowledge-graph.json b/hub/pyaging/knowledge-graph.json new file mode 100644 index 0000000..87c20c0 --- /dev/null +++ b/hub/pyaging/knowledge-graph.json @@ -0,0 +1,54 @@ +{ + "schema": "longevity-loop hub knowledge graph — portable nodes/edges JSON", + "source": { + "repo": "rsinghlab/pyaging", + "url": "https://github.com/rsinghlab/pyaging", + "sha": "c2b3000e3c2c", + "grounded_in": "repo README + pyaging.readthedocs.io tutorials (verbatim API)" + }, + "nodes": [ + {"id": "repo:pyaging", "label": "pyaging", "type": "repo", "note": "170+ GPU-optimized aging clocks; BSD-3"}, + {"id": "mod:pp", "label": "pya.pp", "type": "module", "note": "preprocessing / data prep"}, + {"id": "mod:pred", "label": "pya.pred", "type": "module", "note": "clock prediction"}, + {"id": "mod:data", "label": "pya.data", "type": "module", "note": "example data / loaders"}, + {"id": "fn:df_to_adata", "label": "pya.pp.df_to_adata", "type": "function", "note": "DataFrame -> AnnData (metadata_cols, imputer_strategy='knn')"}, + {"id": "fn:epicv2", "label": "pya.pp.epicv2_probe_aggregation", "type": "function", "note": "EPICv2 probe aggregation"}, + {"id": "fn:predict_age", "label": "pya.pred.predict_age", "type": "function", "note": "predict_age(adata, [clock_names]) -> results in adata.obs"}, + {"id": "concept:anndata", "label": "AnnData", "type": "concept", "note": "the core data container"}, + {"id": "concept:clock", "label": "aging clock (170+)", "type": "concept"}, + {"id": "clock:Horvath2013", "label": "Horvath2013", "type": "clock"}, + {"id": "clock:AltumAge", "label": "AltumAge", "type": "clock"}, + {"id": "clock:PCGrimAge", "label": "PCGrimAge", "type": "clock"}, + {"id": "clock:GrimAge2", "label": "GrimAge2", "type": "clock"}, + {"id": "clock:DunedinPACE", "label": "DunedinPACE", "type": "clock"}, + {"id": "modality:dnam", "label": "DNA methylation", "type": "modality"}, + {"id": "modality:rna", "label": "RNA-seq", "type": "modality"}, + {"id": "modality:atac", "label": "ATAC-seq", "type": "modality"}, + {"id": "modality:histone", "label": "histone marks", "type": "modality"}, + {"id": "modality:chem", "label": "blood chemistry", "type": "modality"}, + {"id": "ext:clockbench", "label": "longevity-loop clockbench (G1)", "type": "downstream", "note": "consumes the clock panel to measure cross-clock disagreement"} + ], + "edges": [ + {"source": "repo:pyaging", "target": "mod:pp", "rel": "contains"}, + {"source": "repo:pyaging", "target": "mod:pred", "rel": "contains"}, + {"source": "repo:pyaging", "target": "mod:data", "rel": "contains"}, + {"source": "mod:pp", "target": "fn:df_to_adata", "rel": "defines"}, + {"source": "mod:pp", "target": "fn:epicv2", "rel": "defines"}, + {"source": "mod:pred", "target": "fn:predict_age", "rel": "defines"}, + {"source": "fn:df_to_adata", "target": "concept:anndata", "rel": "produces"}, + {"source": "fn:predict_age", "target": "concept:anndata", "rel": "consumes"}, + {"source": "fn:predict_age", "target": "concept:clock", "rel": "predicts"}, + {"source": "clock:Horvath2013", "target": "concept:clock", "rel": "is-a"}, + {"source": "clock:AltumAge", "target": "concept:clock", "rel": "is-a"}, + {"source": "clock:PCGrimAge", "target": "concept:clock", "rel": "is-a"}, + {"source": "clock:GrimAge2", "target": "concept:clock", "rel": "is-a"}, + {"source": "clock:DunedinPACE", "target": "concept:clock", "rel": "is-a"}, + {"source": "repo:pyaging", "target": "modality:dnam", "rel": "supports"}, + {"source": "repo:pyaging", "target": "modality:rna", "rel": "supports"}, + {"source": "repo:pyaging", "target": "modality:atac", "rel": "supports"}, + {"source": "repo:pyaging", "target": "modality:histone", "rel": "supports"}, + {"source": "repo:pyaging", "target": "modality:chem", "rel": "supports"}, + {"source": "fn:predict_age", "target": "ext:clockbench", "rel": "feeds"} + ], + "counts": {"nodes": 20, "edges": 20} +} diff --git a/hub/pyaging/manifest.json b/hub/pyaging/manifest.json new file mode 100644 index 0000000..7db76e3 --- /dev/null +++ b/hub/pyaging/manifest.json @@ -0,0 +1,16 @@ +{ + "repo": "rsinghlab/pyaging", + "url": "https://github.com/rsinghlab/pyaging", + "source_sha": "c2b3000e3c2c", + "generated_at": "2026-07-19", + "license": "BSD-3-Clause", + "stars_at_generation": 127, + "one_line": "A Python compendium of GPU-optimized biological aging clocks (170+ published clocks).", + "artifacts": ["knowledge-graph.json", "SKILL.md"], + "engines": { + "knowledge_graph": "grounded from the pyaging repo + readthedocs (production: graphify / understand-anything)", + "skill": "grounded from the pyaging real API (production: anyagent reverse -> refine)" + }, + "provenance_note": "Every fact is grounded in the pyaging repo/docs at source_sha. This is the Phase-2 exemplar of the JIT deep layer (docs/HUB_ARCHITECTURE.md): regenerate when source_sha changes — `scripts/hub_gen.py --refresh` flags staleness. No evidence => no claim.", + "why": "pyaging computes the epigenetic-clock panel that feeds scripts/clockbench.py (gaps-analysis G1) and Turn 01 — the loop's #1 measurement target." +} diff --git a/scripts/build.py b/scripts/build.py index 7bd95d6..7b3b066 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -150,7 +150,9 @@ def render_readme() -> str: "Design (high-quality · fast · cheap · fresh · future-proof): " "**[docs/HUB_ARCHITECTURE.md](docs/HUB_ARCHITECTURE.md)** · per-repo generation recipes: " "**[docs/REPOS.md](docs/REPOS.md)**. Fresh index, just-in-time depth — deep artifacts are " - "generated on demand and cached by commit SHA, never committed stale.", "", "---", ""] + "generated on demand and cached by commit SHA, never committed stale. First deep exemplar " + "(Phase 2): **[hub/pyaging](hub/pyaging)** — a grounded knowledge graph + a gated skill, " + "SHA-stamped, with a live staleness check (`make hub` / `scripts/hub_gen.py --refresh`).", "", "---", ""] # What we're shooting (static pointer to the problem map) L += ['

🎯 What we\'re shooting

', "", diff --git a/scripts/hub_gen.py b/scripts/hub_gen.py new file mode 100644 index 0000000..fe177a1 --- /dev/null +++ b/scripts/hub_gen.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""hub_gen — validate + freshness-check the hub's JIT deep artifacts (docs/HUB_ARCHITECTURE.md). + +Phase 2 of the hub: for a cited repo we generate a knowledge graph + a skill, SHA-stamped, +under hub// (grounded in the real repo — no evidence => no claim). This script is the +GATE and the FRESHNESS mechanism around those artifacts: + + hub_gen.py --check # OFFLINE integrity gate (manifest well-formed, artifacts present, + # KG valid, SKILL.md has frontmatter) — wired into `make check`. + hub_gen.py --refresh # NETWORK: git ls-remote each repo HEAD vs the pinned source_sha, + # report FRESH/STALE (the "never ship stale" check; for the cron). + hub_gen.py --selftest # assert the validator catches a broken exemplar. + +Deep generation itself (graphify / anyagent reverse) is done on demand; this keeps what's +committed honest and detects when it drifts from the source. +""" +from __future__ import annotations + +import json +import pathlib +import re +import subprocess +import sys + +ROOT = pathlib.Path(__file__).resolve().parent.parent +HUB = ROOT / "hub" +MANIFEST_KEYS = ("repo", "url", "source_sha", "artifacts") + + +def exemplars() -> list[pathlib.Path]: + return sorted(p.parent for p in HUB.glob("*/manifest.json")) if HUB.exists() else [] + + +def validate(d: pathlib.Path) -> list[str]: + """Offline integrity checks for one hub// exemplar. Returns a list of problems.""" + errs = [] + try: + man = json.loads((d / "manifest.json").read_text()) + except (OSError, json.JSONDecodeError) as e: + return [f"{d.name}: unreadable manifest.json ({e})"] + for k in MANIFEST_KEYS: + if not man.get(k): + errs.append(f"{d.name}: manifest missing '{k}'") + if not re.fullmatch(r"[0-9a-f]{7,40}", str(man.get("source_sha", ""))): + errs.append(f"{d.name}: source_sha not a commit hash ({man.get('source_sha')!r})") + for art in man.get("artifacts", []): + if not (d / art).exists(): + errs.append(f"{d.name}: artifact '{art}' listed but missing") + kg = d / "knowledge-graph.json" + if kg.exists(): + try: + g = json.loads(kg.read_text()) + if not (isinstance(g.get("nodes"), list) and g["nodes"] and isinstance(g.get("edges"), list)): + errs.append(f"{d.name}: knowledge-graph.json needs non-empty 'nodes' + 'edges'") + except json.JSONDecodeError as e: + errs.append(f"{d.name}: knowledge-graph.json invalid JSON ({e})") + skill = d / "SKILL.md" + if skill.exists(): + m = re.match(r"^---\n(.*?)\n---\n", skill.read_text(), re.S) + if not m: + errs.append(f"{d.name}: SKILL.md missing '---' frontmatter") + elif not ("name:" in m.group(1) and "description:" in m.group(1)): + errs.append(f"{d.name}: SKILL.md frontmatter needs name + description") + return errs + + +def check() -> int: + dirs = exemplars() + if not dirs: + print("hub: no exemplars yet (nothing to gate).") + return 0 + problems = [e for d in dirs for e in validate(d)] + if problems: + print("HUB integrity FAILED:", file=sys.stderr) + for p in problems: + print(f" ✗ {p}", file=sys.stderr) + return 1 + print(f"hub integrity: OK — {len(dirs)} exemplar(s) " + f"({', '.join(d.name for d in dirs)}) well-formed + SHA-stamped.") + return 0 + + +def refresh() -> int: + """Network: is each exemplar still fresh vs the repo's live HEAD?""" + rc = 0 + for d in exemplars(): + man = json.loads((d / "manifest.json").read_text()) + try: + out = subprocess.run(["git", "ls-remote", man["url"], "HEAD"], + capture_output=True, text=True, timeout=20, check=True).stdout + live = out.split()[0][:len(str(man["source_sha"]))] if out else "" + except Exception as e: # noqa: BLE001 + print(f" ? {d.name}: could not reach {man['url']} ({type(e).__name__})") + continue + fresh = live == man["source_sha"] + print(f" {'✅ FRESH' if fresh else '♻ STALE'} {d.name}: pinned {man['source_sha']} " + f"{'==' if fresh else '!='} live {live} — {'up to date' if fresh else 'regenerate'}") + rc = rc or (0 if fresh else 2) + return rc + + +def selftest() -> int: + # the real exemplar must validate clean + for d in exemplars(): + assert not validate(d), f"exemplar {d.name} should be clean: {validate(d)}" + # a synthetic broken manifest must be caught + import tempfile + with tempfile.TemporaryDirectory() as t: + bad = pathlib.Path(t) / "broken" + bad.mkdir() + (bad / "manifest.json").write_text('{"repo": "x"}') # missing keys + bad sha + assert validate(bad), "validator must flag a broken manifest" + print(f"hub_gen selftest: OK ({len(exemplars())} exemplar(s) clean; broken manifest caught).") + return 0 + + +def main() -> int: + if "--refresh" in sys.argv: + return refresh() + if "--selftest" in sys.argv: + return selftest() + return check() # default + --check + + +if __name__ == "__main__": + raise SystemExit(main())