Aging Biology on One Graph
Wire the 12 hallmarks of aging into a queryable knowledge graph.
Introduction
30 Second Summary
You have read the hallmarks reviews and longevity papers, but the field is still a pile of disconnected facts. The hallmarks, the pillars, the pathways, and the landmark papers float unlinked in your head.
In this project, you will wire the whole field of aging biology into one queryable knowledge graph using Python and NetworkX, then use centrality analysis and shortest-path queries to answer real mechanism questions with cited evidence.
What You'll Build
Type a hallmark name into your terminal, and get back every node it connects to, every edge type, and every source citation that backs each relationship.
By the end of this project, you'll have:
- A queryable directed graph with 60+ nodes and 150+ typed edges spanning the 12 hallmarks of aging, 7 pillars of geroscience, key genes and pathways, and landmark papers. Every edge carries its source citation so the graph is an auditable claim map, not an opinion map.
- Centrality rankings that reveal which mechanisms are genuine research hubs, validated against the field's known hub structure. You run the numbers, and the graph proves which hallmarks sit at the structural center of aging biology.
- Shortest-path queries that trace mechanism chains between hallmarks with cited evidence. Ask "what connects telomere attrition to dysbiosis?" and get a multi-hop chain with the paper backing each link.
- Secret Mission: Export a spaced-repetition deck from the graph structure and build an interactive teach-back instrument that proves the field's structure lives in your head, not just in code.
Are there any prerequisites?
You should be comfortable reading Python code and following guided code construction. Familiarity with the hallmarks-of-aging framework helps (having read at least one of the Lopez-Otin reviews).
You need an existing Claude Pro or Max subscription (or equivalent flat-rate AI coding client) and a GitHub account. No prior graph theory is required.
Before We Start
Before any hands-on work, commit to what you are building and why it matters for your path. The proof here is not a summary but a working system you can query.
Set Up Your Environment
To build a queryable knowledge graph of aging biology, you need the right tools in place first.
In this step, you'll install Python and its graph library, confirm your AI coding agent is responding, and set up the delivery infrastructure that tracks your build from start to tagged release.
In this step, get ready to:
- Install Python 3.11+, NetworkX, and confirm Claude Code.
- Create a private GitHub repo and a Linear project with planned tickets.
- Obtain and verify a free OpenAlex API key.
Install and verify your tools
The graph runs on NetworkX, a Python library for creating and analyzing graphs. You also need the requests library for API calls and Claude Code as your AI coding agent.
- Check your Python version by running this command:
python3 --version
Look at the version number that prints.
✔️ I see version 3.11 or higher
Your Python version is ready. Move on to installing the packages below.
ⓧ I see an older version
Your system has an older Python that won't work with NetworkX 3.6.1. You need Python 3.11+ installed.
- Download the latest Python 3.11+ installer from python.org/downloads.
- Run the installer and follow the prompts. On Windows, check the box that says Add Python to PATH.
- Close and reopen your terminal, then re-run python3 --version to confirm.
macOS default Python
macOS may ship with an older system Python that shadows your new install. If python3 --version still shows the old version after installing, use the full path /Library/Frameworks/Python.framework/Versions/3.11/bin/python3 or install via pyenv to manage versions.
ⓧ Command not found
Python is not installed on your machine yet.
- Download the Python 3.11+ installer from python.org/downloads.
- Run the installer. On Windows, check Add Python to PATH before clicking Install Now.
- Close and reopen your terminal, then run python3 --version to confirm it prints 3.11 or higher.
- Install NetworkX and the requests library by running:
pip install networkx==3.6.1 requests
What does this install?
networkx==3.6.1 is the graph library you'll use to create, wire, and analyze the directed knowledge graph.
requests handles HTTP calls to the OpenAlex API for fetching paper metadata.
🙋♀️ Seeing 'pip not found'?
Try pip3 install networkx==3.6.1 requests instead. On some systems, pip maps to Python 2 while pip3 maps to Python 3.
help me fix a pip not found error when installing Python packages
- Confirm Claude Code is responding by running:
claude --version
You should see a version number printed, confirming Claude Code is installed and available.
Preflight: check your plan limits
Claude Code is included in your Pro or Max subscription with bundled usage limits. Plans change, so confirm your limits are sufficient for a coding session before starting.
Check anthropic.com/pricing to verify your plan's current usage allowances. No per-token charges apply within plan limits.
🙋♀️ Claude command not found?
Claude Code requires an active Pro ($20/mo) or Max ($100/mo or $200/mo) subscription. If the command isn't recognized, follow the install instructions at code.claude.com/docs.
help me install Claude Code on my machine
Create your GitHub repo and Linear project
Every edge and node you build will live in a private GitHub repo, and every task will be tracked as a Linear ticket closed by a merged PR.
- Open github.com/new in your browser.
- Enter aging-on-one-graph as the repository name.
- Select Private for visibility.
- Check Add a README file.
- Click Create repository.
You should see your new repo page with the README displayed.
- Move to your Desktop by running this command:
cd ~/Desktop
- Clone your new repo by running:
git clone https://github.com/YOUR_USERNAME/aging-on-one-graph.git
cd aging-on-one-graph
- Confirm the folder exists by running:
ls
You should see README.md listed in the output.
- Create a .gitignore file and the decisions directory by running:
mkdir decisions
touch .gitignore
- Write the decision record to document your stack choices. Create decisions/001-stack.md with this content:
# Decision 001: Stack Choices
## Context
Building a knowledge graph of aging biology with ~80 nodes and ~200 edges.
## Decisions
### NetworkX over Neo4j
NetworkX runs in-process with zero infrastructure. At this scale (~80 nodes),
it installs in one pip command and needs no database server. Neo4j adds
Docker/install complexity that does not teach geroscience concepts.
### OpenAlex over manual curation
OpenAlex provides verifiable paper metadata (titles, DOIs, citation counts)
via a free API. Manual curation cannot be audited or reproduced.
### Claude Code as flat-rate AI client
Claude Code is included in the Pro/Max subscription with no per-token bill
within plan limits. No incremental cost for this coding session.
Why write a decision record?
Decision records capture the reasoning behind stack choices at the moment you make them. When you revisit this project in six months, you'll know why you chose NetworkX over Neo4j without needing to reconstruct the thinking.
- Push your initial files to GitHub by running:
git add .
git commit -m "Initial commit: README, .gitignore, decision record"
git push
Refresh your repo page on GitHub. You should see .gitignore and the decisions/ folder with your decision record.
Git push asking for authentication?
GitHub requires a personal access token or SSH key for pushing. If prompted for a password, generate a token at github.com/settings/tokens and use it as your password.
help me authenticate git push to GitHub
- Open linear.app and create a new project called Aging on One Graph.
- Create 5 tickets with these titles:
- Extract nodes and scaffold the graph (maps to Step 2).
- Wire edges and hit the flat-centrality wall (maps to Step 3).
- Enrich, re-analyze, and query (maps to Step 4).
- Validate, diagram, document, and present (maps to Step 5).
- Spaced-repetition deck and teach-back (maps to Secret Mission).
You should see all 5 tickets in your Linear project, ready to be moved to In Progress as you start each step.
Get your OpenAlex API key
OpenAlex is a free, open catalog of scholarly papers. You'll use its API to fetch metadata (titles, DOIs, citation counts) for the ~40 landmark papers in your graph.
- Visit openalex.org/settings/api.
- Create a free account (takes about 30 seconds).
- Copy your API key from the settings page.
Is this free?
Singleton lookups (fetching one paper by DOI) are unlimited and free with a free API key. The ~40 lookups this project needs cost $0.
- Verify your key works by running this command (replace your-api-key-here with the key you just copied):
curl "https://api.openalex.org/works/doi:10.1016/j.cell.2022.11.001?api_key=your-api-key-here"
You should see a JSON response containing the title of the Lopez-Otin 2023 paper: "Hallmarks of aging: An expanding universe."
Seeing an empty response or error?
Double-check that you copied the full API key with no extra spaces. Make sure the DOI in the URL matches exactly: 10.1016/j.cell.2022.11.001.
help me debug my OpenAlex API call
Verify your full environment
Before you run this, what do you expect to see printed? Think about what the import statement does and what the print call should output.
- Run the final preflight check:
python3 -c "import networkx as nx; print(nx.__version__)"
You should see 3.6.1 printed. This confirms Python can import NetworkX at the correct version.
✔️ I see 3.6.1 printed
Your environment is fully set up. Python, NetworkX, your OpenAlex key, GitHub repo, and Linear project are all ready.
ⓧ I see an import error or wrong version
If you see ModuleNotFoundError, the install didn't target the right Python. Re-run the install using the same Python binary:
python3 -m pip install networkx==3.6.1 requests
Then re-run the version check. If it prints a version other than 3.6.1, re-run the install command above with the pinned version.
help me fix a NetworkX import or version error
Your environment is ready and your delivery infrastructure is in place. Next up, you'll populate the graph with its first 60+ nodes by extracting hallmarks, pillars, genes, and landmark papers.
Extract Nodes and Scaffold the Graph
A knowledge graph starts with its entities. Before you can wire relationships or run centrality queries, you need every hallmark, every pillar, the key genes and pathways, and the landmark papers as named nodes in a single data structure.
This step produces the baby version of your end product: a graph object with 60+ nodes you can already inspect. The 12 hallmarks are not the 7 pillars. They are two overlapping frameworks that the graph maps rather than conflates.
In this step, get ready to:
- Define the agent architecture with class contracts and a Mermaid topology diagram.
- Implement node extraction for hallmarks, pillars, genes, and papers via OpenAlex.
- Verify all 12 hallmarks and 7 pillars are present as distinct node types and push to GitHub.
Create the file and define agent contracts
Every agent in the system needs a clear contract before you write its logic. You will define each agent class with a docstring stating its goal, inputs, and outputs.
- Create a new file called aging_knowledge_graph.py inside your aging-on-one-graph repo folder.
- Add the module docstring and imports by pasting this code at the top of the file:
"""\nAging on One Graph: A Geroscience Knowledge Graph\nDay one of the mastery path. Wire the field into one queryable structure,\nuse it to answer real questions, teach it back.\n\nSources: Lopez-Otin et al. (Cell, 2023), Kennedy et al. (Cell, 2014),\nOpenAlex API, iCite API.\n\nNot medical advice. Interventions named as nodes are study topics, not protocols.\nHigh centrality means heavily studied and connected, not proven causation.\n"""\n\nimport networkx as nx\nimport requests\nimport json\nimport csv\nimport random\nfrom pathlib import Path
What do these imports do?
- networkx is the graph library for building and analyzing directed graphs.
- requests handles HTTP calls to the OpenAlex API for paper metadata.
- json, csv, random, and Path are standard library utilities used by later agents for file I/O and randomized queries.
- Below the imports, add the agent contracts section header and the first two class stubs:
# =============================================================================\n# AGENT CONTRACTS\n# =============================================================================\n\nclass NodeExtractorAgent:\n \"\"\"\n Goal: Populate the graph with hallmark, pillar, gene/pathway, and paper nodes.\n Inputs: Primary review data (hard-coded), list of DOIs for OpenAlex lookup.\n Outputs: A DiGraph with 60+ attributed nodes.\n \"\"\"\n pass\n\n\nclass EdgeTyperAgent:\n \"\"\"\n Goal: Wire typed, evidence-linked edges between nodes.\n Inputs: A graph with nodes already populated.\n Outputs: The same graph with drives, regulates, maps_to, and evidenced_by edges.\n Discipline: cite-every-edge. Each edge stores source_paper and doi.\n \"\"\"\n pass
What is a docstring contract?
Each agent class declares its goal, inputs, and outputs in a docstring before any implementation. This is the contract: when you implement the agent later, you know exactly what it must deliver. The pass keyword is a Python placeholder that lets the class exist without any methods yet.
- Add the remaining three agent stubs below the EdgeTyperAgent class:
class CentralityAnalyzerAgent:\n \"\"\"\n Goal: Compute degree and betweenness centrality, return sorted rankings.\n Inputs: A populated, wired graph.\n Outputs: Printed rankings and a dict of centrality scores.\n Caveat: High centrality = heavily studied/connected, NOT proven causation.\n \"\"\"\n pass\n\n\nclass ShortestPathAgent:\n \"\"\"\n Goal: Run shortest-path queries between hallmarks, printing mechanism chains.\n Inputs: A populated graph and a list of (source, target) pairs.\n Outputs: Printed paths with edge types and sources for each hop.\n \"\"\"\n pass\n\n\nclass IntegrityCheckerAgent:\n \"\"\"\n Goal: Validate graph completeness and source-linkage.\n Inputs: A populated, wired graph.\n Outputs: PASSED/FAILED for each check, with details on failures.\n \"\"\"\n pass
- Add the Orchestrator stub and the entry point at the bottom of the file:
# =============================================================================\n# ORCHESTRATOR\n# =============================================================================\n\nclass Orchestrator:\n \"\"\"\n Coordinates all agents in design-before-build order.\n Sequence: extract -> wire -> analyze -> query -> validate -> document.\n \"\"\"\n pass\n\n\nif __name__ == \"__main__\":\n # Set your free OpenAlex API key here (or leave empty for limited access)\n OPENALEX_API_KEY = \"your-api-key-here\"\n print(\"Scaffold ready. Implement NodeExtractorAgent next.\")
- Save aging_knowledge_graph.py.
- Run the scaffold to confirm it loads without error:
python aging_knowledge_graph.py
You should see Scaffold ready. Implement NodeExtractorAgent next. printed with no import errors.
Seeing an import error?
- If you see ModuleNotFoundError: No module named 'networkx', confirm you installed it with pip install networkx==3.6.1 requests in the same Python environment you are running.
- If python opens Python 2, try python3 aging_knowledge_graph.py instead.
help me fix my Python import error for networkx
- Write the decision record for the agent pattern. Create decisions/002-agent-pattern.md with this content:
# 002: Agent Classes Over Plain Functions\n\n## Decision\nUse lightweight agent classes (goal, inputs, outputs, run method) rather than\nplain functions for each graph-building responsibility.\n\n## Why\n- Each class encapsulates a single responsibility with a clear contract.\n- The docstring contract (goal/inputs/outputs) makes the architecture self-documenting and testable.\n- The pattern is transferable to heavier agent frameworks later.\n- Plain functions lack a natural place for shared config (graph ref, API key).
- Below the Why section in the same file, add the topology diagram and trade-offs:
## Agent Topology (Mermaid)\n```mermaid\ngraph TD\n O[Orchestrator] --> NE[NodeExtractorAgent]\n O --> ET[EdgeTyperAgent]\n O --> CA[CentralityAnalyzerAgent]\n O --> SP[ShortestPathAgent]\n O --> IC[IntegrityCheckerAgent]\n NE -->|nodes| G[(DiGraph)]\n ET -->|edges| G\n CA -->|reads| G\n SP -->|reads| G\n IC -->|validates| G\n```\n\n## Trade-offs\n- Slightly more boilerplate than bare functions.\n- At this scale (5 agents) the overhead is negligible.\n- If the graph grows to 20+ agents, consider a registry pattern.
Why agent classes instead of plain functions?
Each agent class holds its own state (the graph reference, API key) and declares a run() method. This gives you a transferable architecture concept: goal, inputs, outputs, execute. Plain functions scatter this information across parameters and module globals.
Implement NodeExtractorAgent
Now you will replace the NodeExtractorAgent stub with its full implementation. This agent populates the graph with all four node types: hallmarks, pillars, genes/pathways, and papers.
- In aging_knowledge_graph.py, find the NodeExtractorAgent class and remove the pass statement. Replace the class body (keeping the docstring) with the HALLMARKS list:
HALLMARKS = [\n \"genomic_instability\",\n \"telomere_attrition\",\n \"epigenetic_alterations\",\n \"loss_of_proteostasis\",\n \"disabled_macroautophagy\",\n \"deregulated_nutrient_sensing\",\n \"mitochondrial_dysfunction\",\n \"cellular_senescence\",\n \"stem_cell_exhaustion\",\n \"altered_intercellular_communication\",\n \"chronic_inflammation\",\n \"dysbiosis\",\n ]
Where do the 12 hallmarks come from?
These are the 12 hallmarks of aging from Lopez-Otin et al. (Cell, 2023). The 2023 update added three new hallmarks beyond the original 9 from 2013: disabled macroautophagy, chronic inflammation, and dysbiosis.
- Below the HALLMARKS list, add the HALLMARK_CATEGORIES dictionary that groups them into primary, antagonistic, and integrative:
HALLMARK_CATEGORIES = {\n \"primary\": [\n \"genomic_instability\",\n \"telomere_attrition\",\n \"epigenetic_alterations\",\n \"loss_of_proteostasis\",\n \"disabled_macroautophagy\",\n ],\n \"antagonistic\": [\n \"deregulated_nutrient_sensing\",\n \"mitochondrial_dysfunction\",\n \"cellular_senescence\",\n ],\n \"integrative\": [\n \"stem_cell_exhaustion\",\n \"altered_intercellular_communication\",\n \"chronic_inflammation\",\n \"dysbiosis\",\n ],\n }
What are the three hallmark categories?
Primary hallmarks are causes of damage (DNA breaks, telomere shortening). Antagonistic hallmarks are responses that become harmful over time (nutrient-sensing dysfunction, senescence). Integrative hallmarks are system-level consequences (inflammation, stem cell exhaustion). This hierarchy drives the edge wiring in the next step.
- Add the PILLARS list and GENES_PATHWAYS list below the categories:
PILLARS = [\n \"adaptation_to_stress\",\n \"epigenetics\",\n \"inflammation\",\n \"macromolecular_damage\",\n \"metabolism\",\n \"proteostasis\",\n \"stem_cells_and_regeneration\",\n ]\n\n GENES_PATHWAYS = [\n {\"id\": \"mTOR\", \"full_name\": \"mechanistic target of rapamycin\"},\n {\"id\": \"AMPK\", \"full_name\": \"AMP-activated protein kinase\"},\n {\"id\": \"IGF1\", \"full_name\": \"insulin-like growth factor 1\"},\n {\"id\": \"sirtuins\", \"full_name\": \"sirtuin family (SIRT1-7)\"},\n {\"id\": \"p53\", \"full_name\": \"tumor protein p53\"},\n {\"id\": \"TERT\", \"full_name\": \"telomerase reverse transcriptase\"},\n {\"id\": \"NF_kB\", \"full_name\": \"nuclear factor kappa-B\"},\n {\"id\": \"SASP\", \"full_name\": \"senescence-associated secretory phenotype\"},\n ]
Where do the 7 pillars come from?
The 7 pillars come from Kennedy et al. (Cell, 2014): Adaptation to Stress, Epigenetics, Inflammation, Macromolecular Damage, Metabolism, Proteostasis, and Stem Cells and Regeneration. These are a separate framework from the hallmarks. The graph maps them with maps_to edges rather than conflating the two.
- Add the LANDMARK_DOIS list and the __init__ method:
# ~40 landmark papers curated from the reference lists of the primary reviews.\n # The learner extends this list as they read deeper into the field.\n LANDMARK_DOIS = [\n # Foundational reviews\n \"10.1016/j.cell.2013.05.039\", # Lopez-Otin 2013 Hallmarks\n \"10.1016/j.cell.2022.11.001\", # Lopez-Otin 2023 Hallmarks update\n \"10.1016/j.cell.2014.10.039\", # Kennedy 2014 Geroscience\n # Add remaining DOIs from the primary review reference lists.\n # Pattern: one DOI per landmark paper, fetched via OpenAlex singleton.\n # The learner curates ~37 more from the Lopez-Otin 2023 reference list\n # covering senescence, telomeres, mTOR/rapamycin, caloric restriction,\n # epigenetic clocks, inflammaging, microbiome, and autophagy landmarks.\n ]\n\n def __init__(self, graph, api_key=\"\"):\n self.graph = graph\n self.api_key = api_key
Why only 3 DOIs to start?
The three foundational review DOIs are hard-coded as your starting set. The comments mark where you will curate ~37 more from the Lopez-Otin 2023 reference list. Each DOI is fetched from OpenAlex as a singleton lookup. You extend this list as you read deeper into the field.
- Add the extract_hallmarks and extract_pillars methods:
def extract_hallmarks(self):\n for h in self.HALLMARKS:\n category = \"unknown\"\n for cat, members in self.HALLMARK_CATEGORIES.items():\n if h in members:\n category = cat\n break\n self.graph.add_node(\n h,\n node_type=\"hallmark\",\n category=category,\n source=\"Lopez-Otin et al. 2023\",\n doi=\"10.1016/j.cell.2022.11.001\",\n )\n\n def extract_pillars(self):\n for p in self.PILLARS:\n self.graph.add_node(\n p,\n node_type=\"pillar\",\n source=\"Kennedy et al. 2014\",\n doi=\"10.1016/j.cell.2014.10.039\",\n )
What metadata does each node carry?
Every node gets a node_type attribute (hallmark, pillar, gene_pathway, or paper) plus its source citation. Hallmarks also carry their category (primary, antagonistic, integrative). This metadata lets you filter and query by type later.
- Add the extract_genes and run methods:
def extract_genes(self):\n for gene in self.GENES_PATHWAYS:\n self.graph.add_node(\n gene[\"id\"],\n node_type=\"gene_pathway\",\n full_name=gene[\"full_name\"],\n source=\"Lopez-Otin et al. 2023\",\n )\n\n def run(self, dois=None):\n self.extract_hallmarks()\n self.extract_pillars()\n self.extract_genes()\n self.extract_papers_from_openalex(dois)\n return self.graph
The run() method calls extract_papers_from_openalex which you will add next.
- Add the first part of the extract_papers_from_openalex method (the success path) between extract_genes and run:
def extract_papers_from_openalex(self, dois=None):\n if dois is None:\n dois = self.LANDMARK_DOIS\n base_url = \"https://api.openalex.org/works/doi:\"\n for doi in dois:\n try:\n url = f\"{base_url}{doi}\"\n params = {}\n if self.api_key:\n params[\"api_key\"] = self.api_key\n response = requests.get(url, params=params, timeout=10)\n if response.status_code == 200:\n data = response.json()\n node_id = f\"paper_{doi.replace('/', '_').replace('.', '_')}\"\n self.graph.add_node(\n node_id,\n node_type=\"paper\",\n title=data.get(\"display_name\", \"Unknown\"),\n doi=doi,\n cited_by_count=data.get(\"cited_by_count\", 0),\n openalex_id=data.get(\"id\", \"\"),\n source=\"OpenAlex API\",\n )
What does this method do so far?
For each DOI in your curated list, it calls the OpenAlex singleton endpoint (https://api.openalex.org/works/doi:{doi}) and adds the returned paper as a node with its title, citation count, and OpenAlex ID. The node ID is derived from the DOI with slashes and dots replaced by underscores.
- Complete the method by adding the error-handling branches (the else and except blocks) directly after the closing paren of the success add_node call:
else:\n node_id = f\"paper_{doi.replace('/', '_').replace('.', '_')}\"\n self.graph.add_node(\n node_id,\n node_type=\"paper\",\n title=f\"[Fetch failed] DOI: {doi}\",\n doi=doi,\n cited_by_count=0,\n source=\"DOI (fetch failed)\",\n )\n except requests.RequestException:\n node_id = f\"paper_{doi.replace('/', '_').replace('.', '_')}\"\n self.graph.add_node(\n node_id,\n node_type=\"paper\",\n title=f\"[Network error] DOI: {doi}\",\n doi=doi,\n cited_by_count=0,\n source=\"DOI (network error)\",\n )
Why add nodes even when the fetch fails?
The graph must represent every paper in your curated DOI list whether or not the network call succeeds. A failed fetch still creates the node with the DOI recorded. You can re-fetch its metadata later. This ensures the graph's node count is predictable regardless of network conditions.
- Save aging_knowledge_graph.py.
- Update the if __name__ block to instantiate and run the extractor. Replace the print line with this:
if __name__ == \"__main__\":\n # Set your free OpenAlex API key here (or leave empty for limited access)\n OPENALEX_API_KEY = \"your-api-key-here\"\n\n G = nx.DiGraph()\n extractor = NodeExtractorAgent(G, api_key=OPENALEX_API_KEY)\n extractor.run()\n print(f\"Nodes: {G.number_of_nodes()}\")\n # Print nodes by type\n types = {}\n for _, data in G.nodes(data=True):\n ntype = data.get(\"node_type\", \"unknown\")\n types[ntype] = types.get(ntype, 0) + 1\n for ntype, count in sorted(types.items()):\n print(f\" {ntype}: {count}\")
Run the script and verify node counts
Before you run this, think about how many nodes you expect. You have 12 hallmarks, 7 pillars, 8 gene/pathway nodes, and 3 papers from the DOI list. What total do you predict?
- Run the script:
python aging_knowledge_graph.py
You should see output confirming 30 nodes (with the 3 starter DOIs) broken down by type: 12 hallmarks, 7 pillars, 8 gene_pathway, and 3 papers. When you extend the DOI list to ~40 papers, this rises to 65+.
How do I get to 65+ nodes?
The 3 foundational DOIs are your starting set. To reach 65+ nodes, add ~37 more landmark DOIs to the LANDMARK_DOIS list by curating from the Lopez-Otin 2023 reference list. Each DOI you add becomes a paper node. Cover landmark papers on senescence, telomeres, mTOR/rapamycin, caloric restriction, epigenetic clocks, inflammaging, microbiome, and autophagy.
- Confirm all 12 hallmarks and all 7 pillars are present by checking the printed output. You should see hallmark: 12 and pillar: 7 as separate lines.
Not seeing the expected node counts?
- If you see fewer than 12 hallmarks, check that HALLMARKS has all 12 entries (including disabled_macroautophagy, chronic_inflammation, and dysbiosis from the 2023 update).
- If you see 0 papers, your API key may be invalid or your network may be blocking the request. Try running without an API key (leave OPENALEX_API_KEY as an empty string) to test.
help me debug my node count issue in the aging knowledge graph
- Create a feature branch, commit, push, and open a PR by running these commands:
git checkout -b step-2-extract-nodes
git add .
git commit -m "feat: scaffold agent architecture and implement NodeExtractorAgent"
git push -u origin step-2-extract-nodes
- Open your repo on GitHub. You will see a banner with a Compare & pull request button. Click it.
- Give the PR a title like feat: extract nodes and scaffold graph and click Create pull request.
- Merge the PR on GitHub by clicking Merge pull request, then Confirm merge.
- In Linear, find the Step 2 ticket and mark it closed.
Why merge through a PR instead of pushing to main?
This mirrors the professional delivery workflow: planned tickets in Linear, code on a feature branch, reviewed via PR, merged to main, ticket closed. The tagged release at the end of the project proves every piece of work has a visible audit trail.
✔️ Awesome, I've got everything!
Great. Double check you have saved aging_knowledge_graph.py and decisions/002-agent-pattern.md. Your PR should be merged and the Linear ticket closed.
ⓧ I'd like to double check the full code
Your aging_knowledge_graph.py at this point should contain these sections in order. Scroll up and confirm each is present:
- Module docstring and 6 imports (networkx, requests, json, csv, random, Path).
- AGENT CONTRACTS section header.
- NodeExtractorAgent class with HALLMARKS (12), HALLMARK_CATEGORIES (3 keys), PILLARS (7), GENES_PATHWAYS (8), LANDMARK_DOIS (3+ DOIs), __init__, extract_hallmarks, extract_pillars, extract_genes, extract_papers_from_openalex, and run methods.
- EdgeTyperAgent stub with docstring and pass.
- CentralityAnalyzerAgent stub with docstring and pass.
- ShortestPathAgent stub with docstring and pass.
- IntegrityCheckerAgent stub with docstring and pass.
- ORCHESTRATOR section header with Orchestrator stub.
- if __name__ block that creates a DiGraph, runs NodeExtractorAgent, and prints node counts by type.
Your graph has its first layer of nodes. Next up, you will wire the edges that give the graph its structure and make it queryable.
Wire Edges and Hit the Wall
In Step 2, you built a graph with 65+ nodes spanning the hallmarks, pillars, genes, and papers. Right now those nodes float in isolation. Nodes without edges are a list, not a graph.
In this step, you wire the relationships the reviews describe and run centrality analysis to see what the graph can tell you. You will discover that a coarse hallmark-only layer produces flat, uninformative rankings. That wall is the lesson.
In this step, get ready to:
- Wire 23 hallmark-to-hallmark "drives" edges and 11 pillar-to-hallmark "maps_to" edges, each carrying a source citation.
- Run betweenness centrality on the hallmark-only subgraph and observe the flat distribution.
- Confirm the problem with a shortest-path query that returns a trivially short chain.
Implement the EdgeTyperAgent edge data
The cite-every-edge discipline starts here. Every edge you add stores its source paper and DOI so the graph is an auditable claim graph, not an opinion map.
- In aging_knowledge_graph.py, find the EdgeTyperAgent class stub (it currently has only its docstring).
- Replace the stub body with the HALLMARK_DRIVES list by copying the following code inside the class, directly below the docstring:
HALLMARK_DRIVES = [
("genomic_instability", "cellular_senescence"),
("genomic_instability", "epigenetic_alterations"),
("telomere_attrition", "cellular_senescence"),
("telomere_attrition", "stem_cell_exhaustion"),
("epigenetic_alterations", "stem_cell_exhaustion"),
("epigenetic_alterations", "cellular_senescence"),
("loss_of_proteostasis", "cellular_senescence"),
("loss_of_proteostasis", "mitochondrial_dysfunction"),
("disabled_macroautophagy", "loss_of_proteostasis"),
("disabled_macroautophagy", "mitochondrial_dysfunction"),
("deregulated_nutrient_sensing", "disabled_macroautophagy"),
("deregulated_nutrient_sensing", "mitochondrial_dysfunction"),
("deregulated_nutrient_sensing", "cellular_senescence"),
("deregulated_nutrient_sensing", "loss_of_proteostasis"),
("mitochondrial_dysfunction", "chronic_inflammation"),
("mitochondrial_dysfunction", "stem_cell_exhaustion"),
("cellular_senescence", "chronic_inflammation"),
("cellular_senescence", "altered_intercellular_communication"),
("cellular_senescence", "stem_cell_exhaustion"),
("chronic_inflammation", "dysbiosis"),
("chronic_inflammation", "stem_cell_exhaustion"),
("altered_intercellular_communication", "dysbiosis"),
("dysbiosis", "chronic_inflammation"),
]
What does this data encode?
Each tuple is a directed "drives" relationship from the Lopez-Otin 2023 category structure. Primary hallmarks drive antagonistic hallmarks. Antagonistic hallmarks drive integrative hallmarks.
For example, ("genomic_instability", "cellular_senescence") means genomic instability (a primary hallmark) drives cellular senescence (an antagonistic hallmark).
- Below the HALLMARK_DRIVES list, add the GENE_REGULATES stub and PILLAR_MAPS_TO list:
GENE_REGULATES = [] # Populated in Step 4
PILLAR_MAPS_TO = [
("adaptation_to_stress", "cellular_senescence"),
("adaptation_to_stress", "genomic_instability"),
("epigenetics", "epigenetic_alterations"),
("inflammation", "chronic_inflammation"),
("macromolecular_damage", "genomic_instability"),
("macromolecular_damage", "loss_of_proteostasis"),
("metabolism", "deregulated_nutrient_sensing"),
("metabolism", "mitochondrial_dysfunction"),
("proteostasis", "loss_of_proteostasis"),
("proteostasis", "disabled_macroautophagy"),
("stem_cells_and_regeneration", "stem_cell_exhaustion"),
]
What are maps_to edges?
The 12 hallmarks and 7 pillars are two different frameworks that overlap but are not the same thing. The maps_to edges make the relationship explicit rather than conflating the two.
For example, the "proteostasis" pillar maps to both loss_of_proteostasis and disabled_macroautophagy hallmarks because both involve protein quality control.
- Save aging_knowledge_graph.py.
Add the EdgeTyperAgent methods
The edge data lists define what to wire. The methods iterate those lists and add each edge to the graph with its type and citation attributes.
- Below the PILLAR_MAPS_TO list, add the __init__ and wiring methods:
def __init__(self, graph):
self.graph = graph
def wire_hallmark_drives(self):
for source, target in self.HALLMARK_DRIVES:
self.graph.add_edge(
source,
target,
edge_type="drives",
source_paper="Lopez-Otin et al. 2023",
doi="10.1016/j.cell.2022.11.001",
)
def wire_pillar_maps(self):
for pillar, hallmark in self.PILLAR_MAPS_TO:
self.graph.add_edge(
pillar,
hallmark,
edge_type="maps_to",
source_paper="Kennedy et al. 2014",
doi="10.1016/j.cell.2014.10.039",
)
def wire_paper_evidence(self):
pass # Implemented in Step 5
def run(self):
self.wire_hallmark_drives()
self.wire_pillar_maps()
return self.graph
What does cite-every-edge mean in practice?
Every call to add_edge includes source_paper and doi attributes. If you cannot cite a relationship back to a paper, it does not belong in the graph.
This makes the graph an auditable claim graph. Anyone who inherits it can trace any edge back to its primary source.
- Save aging_knowledge_graph.py.
Seeing an indentation error?
All the methods and class attributes must be indented one level inside the EdgeTyperAgent class. Check that HALLMARK_DRIVES, PILLAR_MAPS_TO, and all def lines start at the same indent level (4 spaces inside the class).
help me fix my EdgeTyperAgent indentation
Build the flat-centrality demonstration
Now you need a function that builds a hallmark-only subgraph, runs betweenness centrality on it, and prints the results. This is the test that will reveal the obstacle.
- Scroll past the IntegrityCheckerAgent stub (and before if __name__), and add the first half of the demonstration function:
def demonstrate_flat_centrality():
"""
Shows why hallmark-only edges produce flat, uninformative centrality.
Run this BEFORE the full orchestrator to see the problem.
"""
print("\n" + "=" * 60)
print(" OBSTACLE DEMONSTRATION: Flat Centrality")
print("=" * 60)
G_flat = nx.DiGraph()
for h in NodeExtractorAgent.HALLMARKS:
G_flat.add_node(h, node_type="hallmark")
for source, target in EdgeTyperAgent.HALLMARK_DRIVES:
G_flat.add_edge(source, target, edge_type="drives")
bc = nx.betweenness_centrality(G_flat)
sorted_bc = sorted(bc.items(), key=lambda x: x[1], reverse=True)
What does this code do?
This builds a temporary graph with only the 12 hallmark nodes and the 23 "drives" edges. No genes, no pillars, no papers.
It then computes nx.betweenness_centrality(G_flat) which measures how often each node sits on the shortest path between other nodes. Nodes with high betweenness are structural hubs.
- Directly below (still inside the same function), add the analysis and output portion:
print("\n Betweenness centrality (hallmark-only graph):")
for node, score in sorted_bc:
print(f" {node:<45s} {score:.4f}")
top_score = sorted_bc[0][1]
fifth_score = sorted_bc[4][1] if len(sorted_bc) > 4 else sorted_bc[-1][1]
spread = top_score - fifth_score
print(f"\n Spread between rank 1 and rank 5: {spread:.4f}")
if spread < 0.05:
print(" PROBLEM: Scores are too flat to differentiate hubs.")
print(" The hallmark-only layer is too coarse for meaningful queries.")
print(" SOLUTION: Add gene/pathway intermediaries to break symmetry.")
print("=" * 60)
What does the spread check prove?
If the spread between rank 1 and rank 5 is less than 0.05, the centrality scores are too flat to differentiate hubs from periphery. The graph is technically correct but useless for discovery because every antagonistic hallmark scores nearly the same.
- Now update the if __name__ block at the bottom of the file to call the demonstration:
if __name__ == "__main__":
# Set your free OpenAlex API key here (or leave empty for limited access)
OPENALEX_API_KEY = "your-api-key-here"
# Step 3 demonstration: show the flat-centrality obstacle
demonstrate_flat_centrality()
- Save aging_knowledge_graph.py.
Before you run this, predict: do you think the centrality rankings will clearly separate the hallmarks into distinct tiers, or will the scores cluster together?
- Run the script:
python aging_knowledge_graph.py
You will see the betweenness centrality scores for all 12 hallmarks printed in descending order. The top 5 scores are within 0.02 of each other.
What should I see?
The output prints each hallmark with a 4-decimal centrality score. The antagonistic hallmarks (cellular_senescence, deregulated_nutrient_sensing, mitochondrial_dysfunction) cluster near the top, but their scores are almost identical.
The script then prints PROBLEM: Scores are too flat to differentiate hubs because the spread is below 0.05. This is the designed obstacle. The hallmark-only layer cannot answer fine-grained mechanism questions.
Confirm with a shortest-path query
Flat centrality is one symptom. The other is that shortest-path queries return trivially short chains because everything connects to almost everything at this granularity.
- Confirm this by running a quick query in your terminal:
python -c "
import networkx as nx
from aging_knowledge_graph import NodeExtractorAgent, EdgeTyperAgent
G = nx.DiGraph()
for h in NodeExtractorAgent.HALLMARKS:
G.add_node(h)
for s, t in EdgeTyperAgent.HALLMARK_DRIVES:
G.add_edge(s, t)
path = nx.shortest_path(G, 'telomere_attrition', 'dysbiosis')
print(f'Path: {path}')
print(f'Hops: {len(path) - 1}')
"
The path from telomere_attrition to dysbiosis is only a few hops. Every hallmark reaches every other in 1-3 steps because the category-level wiring makes them densely connected.
Why is this a problem?
If every hallmark reaches every other in 1-3 hops, a shortest-path query tells you nothing you did not already know. The graph is correct (the edges come from the review) but useless for discovery.
The fix is granularity: gene and pathway nodes act as mechanistic intermediaries that break the symmetry. When mTOR regulates nutrient-sensing but NOT telomere attrition, the path between those two hallmarks must now route through specific mechanisms. That is Step 4.
🙋♀️ Import error when running the one-liner?
Make sure you run the command from the same directory where aging_knowledge_graph.py lives. If you see ModuleNotFoundError, check that your terminal is in the project folder.
help me fix my import error when running the shortest-path check
✔️ Awesome, I've got everything!
Great. Confirm you have saved aging_knowledge_graph.py and that running it prints the flat-centrality obstacle demonstration.
ⓧ I'd like to double check the full code
Your aging_knowledge_graph.py is now too long for a single code block. Scroll up and confirm each chunk is present, in order:
- Module docstring and imports (networkx, requests, json, csv, random, pathlib.Path).
- NodeExtractorAgent class (unchanged from Step 2).
- EdgeTyperAgent class with HALLMARK_DRIVES (23 pairs), GENE_REGULATES = [], PILLAR_MAPS_TO (11 pairs), __init__, wire_hallmark_drives(), wire_pillar_maps(), wire_paper_evidence() stub, and run().
- CentralityAnalyzerAgent, ShortestPathAgent, IntegrityCheckerAgent stubs (unchanged).
- demonstrate_flat_centrality() function (builds hallmark-only subgraph, runs betweenness, prints spread).
- if __name__ block calling demonstrate_flat_centrality().
You hit the wall. The hallmark-only layer is too coarse for meaningful centrality or queries. In the next step, you add gene and pathway nodes as mechanistic intermediaries that break the symmetry and make centrality mean something.
Enrich, Re-Analyze, and Query
In Step 3 you hit the wall: hallmark-only edges produce flat betweenness centrality where every antagonistic hallmark scores within 0.02 of the others. The graph is technically correct but useless for discovery.
Gene and pathway nodes are the fix. When you wire mTOR to nutrient-sensing, macroautophagy, and proteostasis (but NOT directly to telomere attrition), the graph gains asymmetry. Now centrality differentiates genuine hubs from periphery, and shortest-path queries trace real mechanism chains with cited evidence.
In this step, get ready to:
- Wire 22 gene-to-hallmark "regulates" edges that break the flat centrality.
- Wire paper-to-hallmark "evidenced_by" edges connecting landmark papers to the frameworks they support.
- Implement and run the centrality analyzer and shortest-path agents, proving the graph now answers real mechanism questions.
Wire gene-to-hallmark "regulates" edges
Each gene or pathway node connects to the hallmarks it regulates. These edges are selective: mTOR regulates nutrient-sensing and macroautophagy but has no direct edge to telomere attrition. That selectivity is what breaks the centrality symmetry.
- In aging_knowledge_graph.py, find the empty GENE_REGULATES list inside the EdgeTyperAgent class.
- Replace it with this populated list:
GENE_REGULATES = [
("mTOR", "deregulated_nutrient_sensing"),
("mTOR", "disabled_macroautophagy"),
("mTOR", "loss_of_proteostasis"),
("mTOR", "cellular_senescence"),
("AMPK", "deregulated_nutrient_sensing"),
("AMPK", "mitochondrial_dysfunction"),
("AMPK", "disabled_macroautophagy"),
("IGF1", "deregulated_nutrient_sensing"),
("IGF1", "cellular_senescence"),
("sirtuins", "epigenetic_alterations"),
("sirtuins", "mitochondrial_dysfunction"),
("sirtuins", "chronic_inflammation"),
("sirtuins", "deregulated_nutrient_sensing"),
("p53", "cellular_senescence"),
("p53", "genomic_instability"),
("TERT", "telomere_attrition"),
("TERT", "stem_cell_exhaustion"),
("NF_kB", "chronic_inflammation"),
("NF_kB", "cellular_senescence"),
("SASP", "altered_intercellular_communication"),
("SASP", "chronic_inflammation"),
("SASP", "stem_cell_exhaustion"),
]
What do these edges encode?
Each tuple is a directed edge from a gene/pathway to the hallmark it regulates. The list has 22 edges total. Notice the asymmetry: mTOR has 4 targets while TERT has only 2. This uneven connectivity is exactly what makes centrality informative.
The source for these relationships is Lopez-Otin et al. 2023, which maps each gene to the hallmarks it mechanistically influences.
- Now add the wire_gene_regulates() method inside EdgeTyperAgent, below the wire_hallmark_drives() method:
def wire_gene_regulates(self):
# Wire each gene/pathway to the hallmarks it regulates
for gene, hallmark in self.GENE_REGULATES:
self.graph.add_edge(
gene,
hallmark,
edge_type="regulates",
source_paper="Lopez-Otin et al. 2023",
doi="10.1016/j.cell.2022.11.001",
)
What does this method do?
It iterates over the GENE_REGULATES list and adds each edge with three attributes: edge_type set to "regulates", plus the source paper and DOI. This is the cite-every-edge discipline: no edge exists without a verifiable citation.
- Make sure the run() method of EdgeTyperAgent calls self.wire_gene_regulates() between self.wire_hallmark_drives() and self.wire_pillar_maps().
- Save aging_knowledge_graph.py.
- Verify the new edges registered by running this in your terminal:
python -c "import aging_knowledge_graph as akg; import networkx as nx; G = nx.DiGraph(); e = akg.EdgeTyperAgent(G); akg.NodeExtractorAgent(G).run(); e.run(); print('Edges:', G.number_of_edges())"
You should see an edge count higher than before (the hallmark drives alone were 23, pillar maps were 11, and you just added 22 regulates edges).
Seeing fewer edges than expected?
Check that self.wire_gene_regulates() is called inside run(). A common miss is adding the method but not calling it from the orchestration flow.
Also confirm the gene nodes exist in the graph. The NodeExtractorAgent must run first to populate the gene/pathway nodes that these edges reference.
help me debug why my gene-to-hallmark edges are not appearing in the graph
Wire paper-to-hallmark "evidenced_by" edges
The evidenced_by edge type connects landmark paper nodes to the hallmarks and pillars they provide evidence for. The Lopez-Otin 2013 and 2023 papers connect to all 12 hallmarks. Kennedy 2014 connects to all 7 pillars.
- Find the wire_paper_evidence() stub inside EdgeTyperAgent and replace it with this implementation that wires Lopez-Otin papers to all hallmarks:
def wire_paper_evidence(self):
# Connect foundational papers to the frameworks they evidence
hallmarks_2023 = "paper_10_1016_j_cell_2022_11_001"
hallmarks_2013 = "paper_10_1016_j_cell_2013_05_039"
kennedy_2014 = "paper_10_1016_j_cell_2014_10_039"
# Lopez-Otin papers evidence all 12 hallmarks
for h in NodeExtractorAgent.HALLMARKS:
if self.graph.has_node(hallmarks_2023):
self.graph.add_edge(
hallmarks_2023,
h,
edge_type="evidenced_by",
source_paper="Lopez-Otin et al. 2023",
doi="10.1016/j.cell.2022.11.001",
)
if self.graph.has_node(hallmarks_2013):
self.graph.add_edge(
hallmarks_2013,
h,
edge_type="evidenced_by",
source_paper="Lopez-Otin et al. 2013",
doi="10.1016/j.cell.2013.05.039",
)
Why check has_node before adding each edge?
The paper nodes come from OpenAlex API calls that might fail (network errors, rate limits). The has_node() guard prevents the graph from creating dangling edges to nodes that do not exist.
The node IDs (like paper_10_1016_j_cell_2022_11_001) are deterministic: the DOI with slashes and dots replaced by underscores, prefixed with paper_. This naming convention matches what NodeExtractorAgent creates.
- Still inside wire_paper_evidence(), add this block directly below the hallmarks loop to wire Kennedy 2014 to all 7 pillars:
# Kennedy 2014 evidences all 7 pillars
for p in NodeExtractorAgent.PILLARS:
if self.graph.has_node(kennedy_2014):
self.graph.add_edge(
kennedy_2014,
p,
edge_type="evidenced_by",
source_paper="Kennedy et al. 2014",
doi="10.1016/j.cell.2014.10.039",
)
What does the full method now cover?
The Lopez-Otin 2023 paper connects to all 12 hallmarks (12 edges). The Lopez-Otin 2013 paper also connects to all 12 hallmarks (12 edges). Kennedy 2014 connects to all 7 pillars (7 edges). That is up to 31 new evidenced_by edges, depending on which paper nodes were successfully fetched from OpenAlex.
- Confirm self.wire_paper_evidence() is called in the run() method (it should already be there from the stub).
- Save aging_knowledge_graph.py and re-run the edge count check:
python -c "import aging_knowledge_graph as akg; import networkx as nx; G = nx.DiGraph(); akg.NodeExtractorAgent(G).run(); akg.EdgeTyperAgent(G).run(); print('Total edges:', G.number_of_edges())"
You should see a total well above 50 edges (23 drives + 22 regulates + 11 maps_to + up to 31 evidenced_by edges from the three foundational papers).
Seeing 0 evidenced_by edges?
If the total did not increase after adding wire_paper_evidence(), the paper nodes likely did not get created. The NodeExtractorAgent fetches papers from OpenAlex. If your network call failed, the paper nodes exist with "[Fetch failed]" titles but the node IDs still match. Verify with:
python -c "import aging_knowledge_graph as akg; import networkx as nx; G = nx.DiGraph(); akg.NodeExtractorAgent(G).run(); print([n for n in G.nodes if n.startswith('paper_')])"
help me figure out why my evidenced_by edges are not being created
Implement the analysis agents and run queries
With all edges wired, the graph has the asymmetry it needs. Now you implement the two analysis agents that prove the enriched graph actually differentiates hubs and traces mechanism chains.
- Find the CentralityAnalyzerAgent stub class. Replace its methods with this implementation:
def __init__(self, graph):
self.graph = graph
def degree_centrality(self):
return nx.degree_centrality(self.graph)
def betweenness_centrality(self):
return nx.betweenness_centrality(self.graph)
def print_top_n(self, centrality_dict, label, n=10):
# Sort nodes by score and display the top N with their types
sorted_nodes = sorted(
centrality_dict.items(), key=lambda x: x[1], reverse=True
)
print(f"\n{'='*60}")
print(f" {label} - Top {n}")
print(f"{'='*60}")
for i, (node, score) in enumerate(sorted_nodes[:n], 1):
node_data = self.graph.nodes[node]
node_type = node_data.get("node_type", "unknown")
print(f" {i:2d}. {node:<40s} {score:.4f} [{node_type}]")
return sorted_nodes
What does print_top_n do?
It sorts any centrality dictionary by score (descending), then prints a ranked table showing node name, score, and node type. This lets you compare hallmarks, genes, and papers in one ranked view.
The nx.degree_centrality() call measures how many connections each node has (normalized). The nx.betweenness_centrality() call measures how many shortest paths flow through each node. Betweenness centrality is the one that reveals structural bottlenecks.
- Add the run() method to CentralityAnalyzerAgent, below print_top_n:
def run(self):
# Run both centrality measures and print ranked results
print("\n" + "#" * 60)
print(" CENTRALITY ANALYSIS")
print(" (High centrality = heavily connected, NOT proven causation)")
print("#" * 60)
dc = self.degree_centrality()
bc = self.betweenness_centrality()
self.print_top_n(dc, "DEGREE CENTRALITY")
self.print_top_n(bc, "BETWEENNESS CENTRALITY")
return {"degree": dc, "betweenness": bc}
Why the caveat in the output?
The printed banner says "heavily connected, NOT proven causation" because centrality is a structural metric. A node with high betweenness is a bottleneck in the network. It does not mean that node causes aging more than others. This distinction is baked into every output so anyone reading the results understands the claim.
- Save aging_knowledge_graph.py and confirm the centrality agent works by running:
python -c "import aging_knowledge_graph as akg; import networkx as nx; G = nx.DiGraph(); akg.NodeExtractorAgent(G).run(); akg.EdgeTyperAgent(G).run(); akg.CentralityAnalyzerAgent(G).run()"
You should see two ranked tables: DEGREE CENTRALITY and BETWEENNESS CENTRALITY. Look for deregulated_nutrient_sensing and cellular_senescence near the top of the betweenness rankings.
Centrality scores all showing 0.0000?
This means the graph has nodes but no edges when the analyzer runs. Confirm EdgeTyperAgent(G).run() is called before CentralityAnalyzerAgent(G).run(). The order matters: nodes first, then edges, then analysis.
help me debug why all my centrality scores are zero
- Now implement ShortestPathAgent. Find the stub class and replace its methods with the query logic:
DEFAULT_QUERIES = [
("deregulated_nutrient_sensing", "cellular_senescence"),
("telomere_attrition", "dysbiosis"),
("genomic_instability", "chronic_inflammation"),
]
def __init__(self, graph):
self.graph = graph
def query(self, source, target):
# Print the shortest path with edge types and sources
print(f"\n Query: {source} -> {target}")
print(f" {'-'*50}")
try:
path = nx.shortest_path(self.graph, source, target)
print(f" Path length: {len(path) - 1} hops")
print(f" Chain: {' -> '.join(path)}")
print(f" Edge details:")
for i in range(len(path) - 1):
edge_data = self.graph.edges[path[i], path[i + 1]]
edge_type = edge_data.get("edge_type", "unknown")
source_paper = edge_data.get("source_paper", "unknown")
print(f" {path[i]} --[{edge_type}]--> {path[i+1]}")
print(f" Source: {source_paper}")
return path
except nx.NetworkXNoPath:
print(f" No path found between {source} and {target}")
return None
except nx.NodeNotFound as e:
print(f" Node not found: {e}")
return None
What does query() print?
For each source-target pair, it calls nx.shortest_path() to find the shortest directed path. Then it walks each hop, printing the edge type (drives, regulates) and the source paper. This is the query-driven proof: the graph answers questions no flat list can.
The try/except blocks handle two failure modes: no path exists between the nodes, or a node name was misspelled. Both print a clear message instead of crashing.
- Add the run() method to ShortestPathAgent, below query():
def run(self, queries=None):
# Run all default queries or a custom list
if queries is None:
queries = self.DEFAULT_QUERIES
print("\n" + "#" * 60)
print(" SHORTEST-PATH QUERIES")
print("#" * 60)
results = []
for source, target in queries:
result = self.query(source, target)
results.append((source, target, result))
return results
What are the three default queries?
Each query tests a different kind of path through the graph:
- deregulated_nutrient_sensing to cellular_senescence: both are antagonistic hallmarks, but the path goes through specific gene intermediaries.
- telomere_attrition to dysbiosis: a primary hallmark to an integrative hallmark, crossing category boundaries.
- genomic_instability to chronic_inflammation: tests how primary damage cascades into systemic inflammation.
- Finally, update the Orchestrator.run() method to call both new agents. Add Phase 3 and Phase 4 after the existing Phase 2 (edge wiring):
# Phase 3: Analyze
print("\n[Phase 3] Analyzing centrality...")
analyzer = CentralityAnalyzerAgent(self.graph)
self.centrality_results = analyzer.run()
# Phase 4: Query
print("\n[Phase 4] Running shortest-path queries...")
querier = ShortestPathAgent(self.graph)
querier.run()
- Save aging_knowledge_graph.py.
Before you run the full script, predict: will deregulated_nutrient_sensing rank higher in betweenness than it did on the flat hallmark-only graph? Why or why not?
- Run the full script:
python aging_knowledge_graph.py
You should see the flat-centrality demonstration print first (from Step 3), then the full build with differentiated rankings. In the BETWEENNESS CENTRALITY table, deregulated_nutrient_sensing and gene nodes like mTOR and AMPK should appear in the top 5.
In the SHORTEST-PATH QUERIES section, look for multi-hop chains. The query from telomere_attrition to dysbiosis should show multiple hops with each edge naming its type and source paper.
Seeing 1-hop paths for every query?
If all paths are trivially short, the gene nodes may not be wired correctly. The enrichment's purpose is to create intermediary hops. Check that wire_gene_regulates() is called in EdgeTyperAgent.run() so the gene edges exist.
Also confirm the queries use the exact node IDs from the graph. A typo like nutrient_sensing instead of deregulated_nutrient_sensing triggers the "Node not found" error.
help me understand why my shortest-path queries show trivially short paths
- Push your branch, open a PR, merge it, and close the Linear ticket for Step 4.
✔️ Awesome, I've got everything!
Great. Double check you have saved aging_knowledge_graph.py and your PR is merged.
ⓧ I'd like to double check the full code
Your aging_knowledge_graph.py file is now long. Scroll through and confirm these sections are present in order:
- NodeExtractorAgent class with HALLMARKS, HALLMARK_CATEGORIES, PILLARS, GENES_PATHWAYS, LANDMARK_DOIS, and all extraction methods.
- EdgeTyperAgent class with HALLMARK_DRIVES (23 pairs), GENE_REGULATES (22 pairs), PILLAR_MAPS_TO (11 pairs), wire_hallmark_drives(), wire_gene_regulates(), wire_pillar_maps(), wire_paper_evidence(), and run() calling all four.
- CentralityAnalyzerAgent class with degree_centrality(), betweenness_centrality(), print_top_n(), and run().
- ShortestPathAgent class with DEFAULT_QUERIES (3 pairs), query(), and run().
- Orchestrator.run() calling Phases 1 through 4 (extract, wire, analyze, query).
- demonstrate_flat_centrality() function unchanged.
- if __name__ block calling demonstrate_flat_centrality() then Orchestrator(api_key=OPENALEX_API_KEY).run().
The graph now differentiates hubs from periphery and traces mechanism chains with cited evidence. Next up, you validate this structure with an integrity check and generate the documentation that makes the graph a deliverable.
Validate, Diagram, Document, and Present
Your graph has 65+ nodes and 150+ edges, and the centrality and query results look promising. But without validation, it is a guess.
In this step you will prove the graph is complete, consistent, and faithful to the primary reviews. You will implement an integrity check, a centrality self-check, generate a Mermaid architecture diagram, write a plain-language guide, produce a two-audience walkthrough, and tag the release.
In this step, get ready to:
- Validate graph integrity with three automated checks (no orphans, all frameworks present, all edges sourced).
- Implement the centrality self-check comparing rankings against domain expectations.
- Generate documentation, tag the release, and close the final Linear ticket.
Implement and run the IntegrityCheckerAgent
The IntegrityCheckerAgent runs three checks that prove graph completeness and source-linkage. If any check fails, you fix the graph and re-run until all three pass.
- In aging_knowledge_graph.py, find the IntegrityCheckerAgent stub class and replace it with the full implementation, starting with the class declaration and the orphan-node check:
class IntegrityCheckerAgent:
"""
Goal: Validate graph completeness and source-linkage.
Inputs: A populated, wired graph.
Outputs: PASSED/FAILED for each check, with details on failures.
"""
def __init__(self, graph):
self.graph = graph
def check_no_orphans(self):
orphans = list(nx.isolates(self.graph))
if not orphans:
print(" [PASSED] No orphan nodes")
return True
else:
print(f" [FAILED] Orphan nodes found: {orphans}")
return False
What does this code do?
nx.isolates(G) returns any node with zero edges. An orphan means you added a node but never wired it to anything, which is a data integrity failure.
- Add the framework-presence check below check_no_orphans:
def check_frameworks_present(self):
hallmark_nodes = [
n for n, d in self.graph.nodes(data=True)
if d.get("node_type") == "hallmark"
]
pillar_nodes = [
n for n, d in self.graph.nodes(data=True)
if d.get("node_type") == "pillar"
]
hallmarks_ok = set(NodeExtractorAgent.HALLMARKS).issubset(set(hallmark_nodes))
pillars_ok = set(NodeExtractorAgent.PILLARS).issubset(set(pillar_nodes))
if hallmarks_ok and pillars_ok:
print(f" [PASSED] All 12 hallmarks and 7 pillars present")
return True
else:
missing_h = set(NodeExtractorAgent.HALLMARKS) - set(hallmark_nodes)
missing_p = set(NodeExtractorAgent.PILLARS) - set(pillar_nodes)
if missing_h:
print(f" [FAILED] Missing hallmarks: {missing_h}")
if missing_p:
print(f" [FAILED] Missing pillars: {missing_p}")
return False
What does this code do?
This check uses set comparison to verify all 12 hallmarks and all 7 pillars exist as distinct node types. If any are missing, it names them specifically so you can fix the gap.
- Add the edge-sourcing check and the run() method below check_frameworks_present:
def check_edges_sourced(self):
unsourced = []
for u, v, data in self.graph.edges(data=True):
if not data.get("source_paper") and not data.get("doi"):
unsourced.append((u, v))
if not unsourced:
print(f" [PASSED] All {self.graph.number_of_edges()} edges have sources")
return True
else:
print(f" [FAILED] {len(unsourced)} edges lack sources: {unsourced[:5]}...")
return False
def run(self):
print("\n" + "#" * 60)
print(" INTEGRITY CHECK")
print("#" * 60)
results = [
self.check_no_orphans(),
self.check_frameworks_present(),
self.check_edges_sourced(),
]
all_passed = all(results)
print(f"\n Overall: {'ALL PASSED' if all_passed else 'SOME CHECKS FAILED'}")
return all_passed
What does this code do?
check_edges_sourced enforces the cite-every-edge discipline: every edge must carry a source_paper or doi attribute. The run() method executes all three checks and prints an overall verdict.
- In the Orchestrator.run() method, find the comment # Phase 4: Query block and add Phase 5 below it:
# Phase 5: Validate
print("\n[Phase 5] Validating integrity...")
checker = IntegrityCheckerAgent(self.graph)
checker.run()
- Save aging_knowledge_graph.py.
- Before you run the script, predict: do you think all three checks will pass given the graph you built in Steps 2-4?
- Run the script to see the integrity results:
python aging_knowledge_graph.py
You should see all three checks print [PASSED] followed by Overall: ALL PASSED. If any check fails, fix the graph data and re-run until all three pass.
Seeing a FAILED check?
- Orphan nodes found: you added a node in Step 2 but never wired an edge to it in Steps 3-4. Add the missing edge.
- Missing hallmarks or pillars: check that your NodeExtractorAgent.HALLMARKS list has all 12 entries and PILLARS has all 7.
- Unsourced edges: ensure every add_edge() call includes source_paper or doi as a keyword argument.
Help me debug a FAILED integrity check in my aging knowledge graph.
Implement the centrality self-check
The centrality self-check compares your computed betweenness rankings against domain expectations. This check is falsifiable, not guaranteed: a hub hallmark is a research focal point, not a claimed root cause.
- Below the IntegrityCheckerAgent class, add the CentralitySelfCheckAgent class with its expected-hubs constant and constructor:
class CentralitySelfCheckAgent:
"""
Goal: Compare top centrality nodes against domain expectations.
Inputs: Centrality results dict.
Outputs: Confirmation or explanation prompt. Falsifiable, not guaranteed.
The expected hubs are the three ANTAGONISTIC hallmarks (not integrative).
"""
EXPECTED_HUBS = [
"deregulated_nutrient_sensing",
"mitochondrial_dysfunction",
"cellular_senescence",
]
def __init__(self, graph, centrality_results):
self.graph = graph
self.centrality_results = centrality_results
What does this code do?
EXPECTED_HUBS names the three antagonistic hallmarks as the predicted structural hubs. These are the hallmarks that sit between primary causes and integrative consequences, making them natural bottlenecks.
- Add the run() method to CentralitySelfCheckAgent, starting with the filtering and sorting logic:
def run(self):
print("\n" + "#" * 60)
print(" CENTRALITY SELF-CHECK (falsifiable)")
print(" Expected hubs: the three antagonistic hallmarks")
print("#" * 60)
bc = self.centrality_results.get("betweenness", {})
hallmark_bc = {
k: v for k, v in bc.items()
if self.graph.nodes[k].get("node_type") == "hallmark"
}
sorted_hallmarks = sorted(
hallmark_bc.items(), key=lambda x: x[1], reverse=True
)
top_3 = [node for node, _ in sorted_hallmarks[:3]]
- Still inside the run() method, add the comparison logic below the top_3 line:
print(f" Expected top hubs (antagonistic hallmarks): {self.EXPECTED_HUBS}")
print(f" Actual top-3 hallmarks by betweenness: {top_3}")
overlap = set(top_3) & set(self.EXPECTED_HUBS)
if len(overlap) >= 2:
print(f" [CONFIRMED] {len(overlap)}/3 match expected hubs.")
print(" Interpretation: The antagonistic hallmarks are structurally")
print(" central because gene/pathway nodes fan into them from multiple")
print(" primary hallmarks, making them bottlenecks in the network.")
else:
print(f" [DIVERGENT] Only {len(overlap)}/3 match expected hubs.")
print(" This is not a failure. Explain from the edge set why these")
print(" nodes rank higher. High centrality = heavily connected,")
print(" not proven causation.")
for node, score in sorted_hallmarks[:3]:
in_deg = self.graph.in_degree(node)
out_deg = self.graph.out_degree(node)
print(f" {node}: betweenness={score:.4f}, "
f"in_degree={in_deg}, out_degree={out_deg}")
Why is this check falsifiable?
If at least 2 of the top 3 match the expected antagonistic hallmarks, the check prints CONFIRMED with an interpretation. If fewer match, it prints DIVERGENT and prompts you to explain why from the actual edge counts.
This is not a pass/fail gate. A divergent result teaches you something about the graph's structure, not about a bug.
- In the Orchestrator.run() method, add Phase 6 below the Phase 5 block:
# Phase 6: Centrality self-check
self_check = CentralitySelfCheckAgent(self.graph, self.centrality_results)
self_check.run()
- Save aging_knowledge_graph.py and run the script again:
python aging_knowledge_graph.py
You should see the self-check output showing either CONFIRMED (2-3 of 3 match) or DIVERGENT with edge-count details. Either result is valid.
Self-check not printing anything?
- Confirm the Orchestrator stores centrality results: check that self.centrality_results = analyzer.run() exists in Phase 3.
- Verify the CentralitySelfCheckAgent receives self.centrality_results as its second argument, not an empty dict.
Help me debug why CentralitySelfCheckAgent produces no output.
Generate documentation, tag the release, and close the ticket
The DocumentationAgent generates three outputs: a Mermaid topology diagram printed to console, a plain-language guide written to GUIDE.md, and a two-audience walkthrough written to WALKTHROUGH.md.
- Below the CentralitySelfCheckAgent class, add the DocumentationAgent class with the Mermaid topology constant:
class DocumentationAgent:
"""
Goal: Generate Mermaid topology diagram, plain-language guide, and walkthrough.
Inputs: The completed, validated graph.
Outputs: Mermaid printed to console, GUIDE.md and WALKTHROUGH.md written to disk.
"""
MERMAID_TOPOLOGY = """```mermaid
graph TD
O[Orchestrator] --> NE[NodeExtractorAgent]
O --> ET[EdgeTyperAgent]
O --> CA[CentralityAnalyzerAgent]
O --> SP[ShortestPathAgent]
O --> IC[IntegrityCheckerAgent]
O --> CSC[CentralitySelfCheckAgent]
O --> DOC[DocumentationAgent]
NE -->|nodes| G[(DiGraph)]
ET -->|edges| G
CA -->|reads| G
SP -->|reads| G
IC -->|validates| G
DOC -->|documents| G
```"""
What is this Mermaid diagram?
This is a text-based diagram syntax that renders as a visual flowchart. It captures the agent architecture: the Orchestrator delegates to seven agents, each reading from or writing to the shared DiGraph.
- Add the constructor, the Mermaid print method, and the run() method:
def __init__(self, graph):
self.graph = graph
def print_mermaid(self):
print("\n" + "#" * 60)
print(" AGENT TOPOLOGY (Mermaid)")
print("#" * 60)
print(self.MERMAID_TOPOLOGY)
def run(self):
self.print_mermaid()
self.write_guide()
self.write_walkthrough()
- Add the write_guide method that computes graph statistics and writes the guide file:
def write_guide(self, filepath="GUIDE.md"):
node_counts = {}
for _, data in self.graph.nodes(data=True):
ntype = data.get("node_type", "unknown")
node_counts[ntype] = node_counts.get(ntype, 0) + 1
edge_counts = {}
for _, _, data in self.graph.edges(data=True):
etype = data.get("edge_type", "unknown")
edge_counts[etype] = edge_counts.get(etype, 0) + 1
guide = f"""# Aging on One Graph: Plain-Language Guide
## What This Graph Is
A queryable knowledge graph of aging biology containing:
- {self.graph.number_of_nodes()} nodes ({', '.join(f'{v} {k}' for k, v in node_counts.items())})
- {self.graph.number_of_edges()} edges ({', '.join(f'{v} {k}' for k, v in edge_counts.items())})
The graph encodes relationships described in the primary geroscience reviews.
Every edge carries a source citation (cite-every-edge discipline).
What does this code do?
The method dynamically counts node types and edge types from the live graph, then embeds those counts into the guide's opening section. This means the guide is always accurate to the graph's actual state.
- Continue the write_guide f-string with the "What This Graph Is NOT" section and the three method patterns:
## What This Graph Is NOT
- Not medical advice. Interventions named as nodes are study topics, not protocols.
- Not a causal model. High centrality means heavily studied and connected,
not proven causation. A hub hallmark is a research focal point, not a claimed
root cause.
- Not a complete map of aging. It is a study artifact built from landmark reviews
and selected papers.
- Not a poster. It is a tool to query, not a diagram to hang.
## Three Method Patterns
1. Cite-every-edge: each drives, regulates, or evidenced-by link stores its source,
so the graph is an auditable claim graph, not an opinion map.
2. Centrality-as-a-test: the graph must reproduce the field's known hub structure,
so it is validated by numbers, not by looking complete.
3. Query-driven: the graph is only proven when it answers a real mechanism question,
so a shortest-path query between two hallmarks is part of the deliverable.
- Continue the f-string with the corrections, query instructions, and rebuild guide:
## Three Corrections
1. The 12 hallmarks are not the 7 pillars. They are two overlapping frameworks;
the graph maps them with maps_to edges rather than conflating them.
2. High centrality is not causation. A hub node is heavily studied and connected.
3. A knowledge graph is not a poster. The deliverable is the graph in use.
## How to Query
```python
import networkx as nx
# Load or rebuild the graph (run aging_knowledge_graph.py)
# Then:
path = nx.shortest_path(G, "telomere_attrition", "dysbiosis")
centrality = nx.betweenness_centrality(G)
neighbors = list(G.neighbors("mTOR"))
```
## How to Rebuild
Run `python aging_knowledge_graph.py` with your OpenAlex API key set
in the OPENALEX_API_KEY variable. The script rebuilds the full graph
from curated data and live API calls.
- Finish the f-string with the extension guide and primary sources, then close the method:
## How to Extend (Adoption Target)
Place a new landmark paper within 30 days:
1. Get the DOI
2. Add it to LANDMARK_DOIS
3. Wire evidenced_by edges to the hallmarks it addresses
4. Re-run centrality and queries to see what changed
## Primary Sources
- Lopez-Otin et al. (2023) "Hallmarks of aging: An expanding universe."
Cell 186(2):243-278. DOI: 10.1016/j.cell.2022.11.001
- Lopez-Otin et al. (2013) "The Hallmarks of Aging."
Cell 153(6):1194-1217. DOI: 10.1016/j.cell.2013.05.039
- Kennedy et al. (2014) "Geroscience: Linking Aging to Chronic Disease."
Cell 159(4):709-713. DOI: 10.1016/j.cell.2014.10.039
## Where Deeper Material Lives
- OpenAlex API: https://api.openalex.org (paper metadata, citation graphs)
- iCite API: https://icite.od.nih.gov/api (relative citation ratios, NIH impact)
- NetworkX docs: https://networkx.org/documentation/stable/
- Neo4j (optional): for persistent graph storage if the graph grows beyond scripts
- Obsidian (optional): as a graph-note vault companion for reading notes
"""
Path(filepath).write_text(guide, encoding="utf-8")
print(f"\n Guide written to {filepath}")
- Save aging_knowledge_graph.py and run the script to confirm GUIDE.md is created:
python aging_knowledge_graph.py
- Verify GUIDE.md exists by running:
ls GUIDE.md
You should see GUIDE.md listed and the console output should show Guide written to GUIDE.md.
- Now add the write_walkthrough method below write_guide, starting with the method signature and the technical audience section:
def write_walkthrough(self, filepath="WALKTHROUGH.md"):
walkthrough = """# Aging on One Graph: Two-Audience Walkthrough
## For the Technical Reader
### Graph Structure
- Directed graph (nx.DiGraph) with typed nodes (hallmark, pillar, gene_pathway, paper)
- Typed edges: drives (hallmark-to-hallmark), regulates (gene-to-hallmark),
maps_to (pillar-to-hallmark), evidenced_by (paper-to-hallmark/pillar)
- Every edge carries source_paper and doi attributes (cite-every-edge)
### Validation Results
- Integrity: no orphans, all 12 hallmarks and 7 pillars present, all edges sourced
- Centrality self-check: antagonistic hallmarks (nutrient-sensing, mitochondrial
dysfunction, cellular senescence) rank as top betweenness hubs because gene/pathway
nodes create bottleneck paths through them
- Query check: shortest-path queries return multi-hop mechanism chains matching
the relationships described in the primary reviews
- Continue the walkthrough f-string with the centrality interpretation and novice section:
### Centrality Interpretation
High betweenness = many shortest paths flow through this node.
For the antagonistic hallmarks, this happens because:
- Multiple gene nodes (mTOR, AMPK, IGF1, sirtuins) regulate them directly
- Multiple primary hallmarks drive into them
- They in turn drive the integrative hallmarks
This makes them structural bottlenecks, not proven root causes.
## For the Novice Reader
### What Are the Hallmarks of Aging?
In 2013 (updated 2023), researchers identified 12 biological processes that
together drive aging. Think of them as 12 kinds of damage or dysfunction that
accumulate over time. They are grouped into three categories:
- Primary (causes of damage): genomic instability, telomere shortening,
epigenetic drift, protein misfolding, broken autophagy
- Antagonistic (responses that become harmful): nutrient-sensing gone wrong,
mitochondrial decline, cells refusing to die (senescence)
- Integrative (system-level consequences): stem cells giving up, miscommunication
between cells, chronic inflammation, gut microbiome disruption
- Continue with the novice explanations and lessons learned, then close the method:
### What Does This Graph Show?
It shows how these 12 processes connect to each other, to 7 broader research
themes (the "pillars"), and to the specific genes and pathways that regulate them.
You can ask it questions like "what connects telomere shortening to gut problems?"
and get a chain of mechanisms with the paper that supports each link.
### Why It Matters
When you read a new aging paper, you can place it in this structure and see
what it connects to. The field stops being a pile of facts and becomes a map.
## Lessons Learned
- A flat graph (all nodes connected to all nodes) is useless for centrality.
Granularity matters: intermediary nodes break symmetry.
- The cite-every-edge discipline forces you to justify every claim. Edges you
cannot cite get removed, and that is a feature.
- The hallmarks and pillars overlap but are not the same framework. Mapping them
explicitly (maps_to edges) prevents conflation.
- High centrality surprised by being structural, not intuitive. The "most important"
hallmark is not necessarily the one you hear about most in popular science.
"""
Path(filepath).write_text(walkthrough, encoding="utf-8")
print(f" Walkthrough written to {filepath}")
Why two audiences?
The technical section speaks to someone who can read Python and graph theory. The novice section speaks to someone who has heard of aging research but never seen a knowledge graph. Writing for both proves you can translate between levels.
- Add Phase 7 to the Orchestrator.run() method below Phase 6:
# Phase 7: Document
print("\n[Phase 7] Generating documentation...")
documenter = DocumentationAgent(self.graph)
documenter.run()
- Save aging_knowledge_graph.py and run the full script one final time:
python aging_knowledge_graph.py
You should see the Mermaid topology diagram printed to the console, followed by Guide written to GUIDE.md and Walkthrough written to WALKTHROUGH.md.
- Confirm both files exist:
ls GUIDE.md WALKTHROUGH.md
Both files should appear in the output.
Files not appearing?
- Check that you imported Path at the top of your file: from pathlib import Path.
- Ensure your terminal's working directory matches where the script lives.
- Look for a Python traceback mentioning NameError: name 'Path' is not defined.
Help me debug why GUIDE.md or WALKTHROUGH.md are not being created.
- Commit all changes, push the branch, open a PR, and merge:
git add .
git commit -m "Implement validation, self-check, and documentation agents"
- Push and merge the PR on GitHub.
- Tag the release as v1.0.0 by running:
git tag -a v1.0.0 -m "First validated release of aging knowledge graph"
- Push the tag to GitHub:
git push origin v1.0.0
- Close the final Linear ticket for Step 5.
Before you check the release on GitHub, predict: what will you see on the repository's releases page?
You should see a v1.0.0 release listed on your repository's releases page with the tag message you provided.
What does the tagged release prove?
The tag marks a specific commit as the validated, documented state of your graph. Anyone (including future-you) can check out v1.0.0 and get a working graph with passing integrity checks, complete documentation, and the full build trail in merged PRs.
✔️ Awesome, I've got everything!
Confirm you have saved aging_knowledge_graph.py with all seven agents implemented and all three phases (5, 6, 7) added to the Orchestrator.
ⓧ I'd like to double check the full code
Your aging_knowledge_graph.py should now contain the following classes and functions in order. Scroll up through your file and confirm each is present:
- NodeExtractorAgent (with HALLMARKS, HALLMARK_CATEGORIES, PILLARS, GENES_PATHWAYS, LANDMARK_DOIS, and all extract methods).
- EdgeTyperAgent (with HALLMARK_DRIVES, GENE_REGULATES, PILLAR_MAPS_TO, and all wire methods).
- CentralityAnalyzerAgent (with degree_centrality, betweenness_centrality, print_top_n, run).
- ShortestPathAgent (with DEFAULT_QUERIES, query, run).
- IntegrityCheckerAgent (with check_no_orphans, check_frameworks_present, check_edges_sourced, run).
- CentralitySelfCheckAgent (with EXPECTED_HUBS, run comparing top-3 against expected).
- DocumentationAgent (with MERMAID_TOPOLOGY, print_mermaid, write_guide, write_walkthrough, run).
- Orchestrator (with all 7 phases in run() and _print_node_summary).
- demonstrate_flat_centrality() function.
- if __name__ == "__main__" block calling demonstrate_flat_centrality() then orchestrator.run().
Your graph is validated, documented, and tagged. You now own a queryable map of aging biology that passes its own integrity check and answers mechanism questions with cited evidence. Next up: the Secret Mission, where you will export a spaced-repetition deck and build a teach-back instrument that proves the graph lives in your head, not just in code.
Secret mission
Spaced-Repetition Deck and Teach-Back
A graph you built once and never revisit decays. Export a spaced-repetition deck from the graph structure itself, then build an interactive teach-back instrument that proves you can explain any node cold. The teach-back is the mentor's proof: the structure lives in your head, not just in code.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with free APIs and free-tier services, so there are no ongoing costs.
Resources you used:
- Local project folder (aging-on-one-graph on your Desktop) containing aging_knowledge_graph.py, GUIDE.md, WALKTHROUGH.md, aging_deck.csv, and decisions/.
- Python packages: networkx and requests.
- Private GitHub repository (aging-on-one-graph).
- Linear project with tickets.
- Free OpenAlex API key.
Keep everything running
No action needed. Choose this if you plan to extend the graph as you read new papers or move into the next wave of the mastery path.
- Your repo, graph, and guide are persistent. The adoption target is to place a new landmark paper within 30 days.
- Every later wave of the path (survival analysis, aging clocks, interventions) adds nodes and edges to this graph.
- The spaced-repetition deck (aging_deck.csv) keeps the structure learned between build sessions.
- Your OpenAlex API key remains free for singleton lookups.
Pause - I'll come back to this later
Free up local resources while keeping your repo and cloud accounts intact for later.
- Your GitHub repo stays available as a private repo at no cost. No action needed there.
- Your Linear project stays on the free tier. No action needed.
- Your OpenAlex API key remains active at no cost.
- If you want to reclaim disk space, you can delete the local folder and re-clone from GitHub when you return.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below to fully clean up.
- Delete the local project folder. On macOS/Linux, run:
cd ~/Desktop
rm -rf aging-on-one-graph
On Windows, run:
cd ~\Desktop
rmdir /s /q aging-on-one-graph
- Uninstall the Python packages by running:
pip uninstall networkx requests -y
- Delete the GitHub repository. Go to your aging-on-one-graph repo on GitHub. Click Settings. Scroll to the bottom to the Danger Zone section. Click Delete this repository and confirm.
- Delete the Linear project. Open your Linear workspace. Click the project name. Open Settings for the project. Click Delete project and confirm.
- Revoke your OpenAlex API key. Visit https://openalex.org/settings/api and delete your key from the dashboard.
Nice Work!
Nice Work!
You just wired the field of aging biology into a single queryable structure, validated it against its own claims, and proved it answers real mechanism questions with cited evidence.
You've learned how to:
- Build a directed knowledge graph with 60+ nodes and 150+ typed, evidence-linked edges using NetworkX, enforcing the cite-every-edge discipline so every relationship is auditable back to a primary source.
- Compute betweenness centrality and shortest-path queries that identify structural hubs and trace mechanism chains between hallmarks, proving the graph answers questions no flat list can.
- Validate, document, and deliver a knowledge graph through a private GitHub repo with Linear tickets, merged PRs, decision records, a plain-language guide, and a tagged release.
- Secret Mission: Exported a spaced-repetition deck from the graph structure and built an interactive teach-back instrument proving you can explain any node cold and redraw the field's structure from memory.
Ready to quiz yourself?