research-timeline 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 N47Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: research-timeline
3
+ Version: 0.2.1
4
+ Summary: Track, visualize, and export research timelines from first AI interaction to scientific discovery
5
+ Author-email: Alessandro Tulli <2injob.at2@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Strugiss/research-timeline
8
+ Project-URL: Repository, https://github.com/Strugiss/research-timeline
9
+ Project-URL: Issues, https://github.com/Strugiss/research-timeline/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering
19
+ Classifier: Topic :: Scientific/Engineering :: Physics
20
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: typer>=0.9.0
25
+ Requires-Dist: pydantic>=2.5.0
26
+ Requires-Dist: rich>=13.0.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
29
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
30
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # Research Timeline
34
+
35
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21855315.svg)](https://doi.org/10.5281/zenodo.21855315)
36
+ [![CI](https://github.com/Strugiss/research-timeline/actions/workflows/ci.yml/badge.svg)](https://github.com/Strugiss/research-timeline/actions/workflows/ci.yml)
37
+ [![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
+ [![SWH](https://archive.softwareheritage.org/badge/swh:1:snp:62a2f748e52113016cf291c4b8c944e86c6848bf/)](https://archive.softwareheritage.org/swh:1:snp:62a2f748e52113016cf291c4b8c944e86c6848bf)
40
+
41
+ Track, visualize, and export research timelines — from first AI interaction to scientific discovery.
42
+
43
+ `research-timeline` documents the **process** of research, not just its artifacts: every milestone of a project (the first AI interaction that shaped the protocol, the first QPU commit with its evidence, pivots, controls, submissions, publications) is recorded in a single versioned JSON file with typed events, quantitative metrics, and supporting evidence.
44
+
45
+ ## Features
46
+
47
+ - **Typed events** — `T0`, `T1`…`Tn`, `pivot`, `control`, `submission`, `publication`, `milestone`
48
+ - **Metrics** — attach any quantitative result (z-scores, shots, backend, MI, …) to an event
49
+ - **Evidence** — git commits, IBM Quantum job IDs, data links, code links
50
+ - **AI-role disclosure** — each timeline declares how AI was used (`cognitive_prosthesis`, `co_pilot`, `autonomous_agent`)
51
+ - **Exports** — LaTeX table (papers/reports), Markdown, standalone HTML, schema.org JSON-LD
52
+ - **Validate** — structural checks with CI-friendly exit codes
53
+ - **Simple JSON storage** — human readable, diff-friendly, git-native, zero lock-in
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install research-timeline
59
+ # or from source:
60
+ pip install git+https://github.com/Strugiss/research-timeline.git
61
+ # or editable for development:
62
+ pip install -e .
63
+ ```
64
+
65
+ ## Usage
66
+
67
+ ```bash
68
+ # Initialize a timeline
69
+ research-timeline init --output timeline.json
70
+
71
+ # Log a typed event (with metrics and evidence)
72
+ research-timeline log T1 --desc "First commit: 14 QPU experiments, Z>50sigma" \
73
+ --z-combined 50.0 --git-commit c3ddc4a --job-ids abc,def --tags commit,qpu
74
+
75
+ # List events (optionally with metrics)
76
+ research-timeline list --metrics
77
+
78
+ # Export to LaTeX (papers), Markdown, HTML, or JSON-LD
79
+ research-timeline export --format latex -o timeline.tex
80
+ research-timeline export --format markdown -o timeline.md
81
+ research-timeline export --format html -o timeline.html
82
+ research-timeline export --format jsonld -o timeline.jsonld
83
+
84
+ # Validate
85
+ research-timeline validate
86
+ ```
87
+
88
+ See [example/timeline.json](example/timeline.json) for a real-world timeline
89
+ (the PASM DTC Discovery project, N47Lab MatterMemory research program) and the
90
+ generated exports in `example/`.
91
+
92
+ ## Event IDs
93
+
94
+ `T0`, `T1`, `T2`, …, `Tn` (ordered research phases) plus special events:
95
+ `pivot`, `control`, `submission`, `publication`, `milestone`.
96
+
97
+ ## File Format
98
+
99
+ A timeline is a single JSON document:
100
+
101
+ ```json
102
+ {
103
+ "project": {"name": "PASM DTC Discovery", "description": "...", "domain": "quantum"},
104
+ "author": {"name": "N47Lab", "affiliation": "independent", "ai_role": "cognitive_prosthesis"},
105
+ "events": [{
106
+ "id": "T1", "type": "T1", "date": "2026-07-31",
107
+ "description": "First commit: 14 QPU experiments, Z>50sigma",
108
+ "metrics": {"z_score_combined": 50.0},
109
+ "evidence": {"git_commit": "c3ddc4a", "job_ids": ["abc"]}
110
+ }]
111
+ }
112
+ ```
113
+
114
+ The schema is documented in `schema/timeline.schema.json` (JSON Schema draft-07).
115
+
116
+ ## Related work
117
+
118
+ - **Notes/task tools (Notion, Obsidian, Logseq, Trello)** — general-purpose notes or task boards; no typed research phases, no JSON schema, no CI validation, cloud-dependent storage.
119
+ - **Experiment trackers (Weights & Biases, MLflow, DVC)** — track model *runs*, artifacts, and metrics; they do not record researcher-level process events (first insight, pivot, control, submission) nor provide paper-oriented exports (LaTeX).
120
+ - **Notebooks (Jupyter, Quarto)** — rich narrative but unstructured; no enforcement of a timeline schema, no machine-readable JSON-LD export.
121
+ - **Lab notebooks (ELN, Code Ocean)** — heavyweight, instrument-locked, or cloud-bound; too heavy for long-term, single-author project process tracking.
122
+
123
+ `research-timeline` fills the empty slot: a zero-dependency, git-native, JSON-backed tracker for the research *narrative* with an explicit schema, structured `evidence` fields, and LaTeX/JSON-LD exports for the writing stage.
124
+
125
+ ## AI Usage Disclosure
126
+
127
+ This project was developed with the assistance of generative AI tools (interactive AI coding assistants with agentic workflows). AI assistance covered initial code scaffolding, the test suite, and documentation drafting (June–August 2026). All AI-assisted output was reviewed line-by-line by the human author, whose design decisions (schema, event types, export contracts, `ai_role` semantics) drove the project; algorithmic behavior is covered by the test suite in `tests/` and by CI. See [AI_POLICY.md](AI_POLICY.md) for the full policy.
128
+
129
+ ## Development & Contributing
130
+
131
+ See [CONTRIBUTING.md](CONTRIBUTING.md) — tests, coding conventions, and governance.
132
+
133
+ ```bash
134
+ pip install -e ".[dev]"
135
+ pytest tests/ -v
136
+ ```
137
+
138
+ ## Software Heritage
139
+
140
+ This repository is archived in permanent storage:
141
+ `swh:1:snp:62a2f748e52113016cf291c4b8c944e86c6848bf`
142
+
143
+ ## License
144
+
145
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,113 @@
1
+ # Research Timeline
2
+
3
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21855315.svg)](https://doi.org/10.5281/zenodo.21855315)
4
+ [![CI](https://github.com/Strugiss/research-timeline/actions/workflows/ci.yml/badge.svg)](https://github.com/Strugiss/research-timeline/actions/workflows/ci.yml)
5
+ [![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![SWH](https://archive.softwareheritage.org/badge/swh:1:snp:62a2f748e52113016cf291c4b8c944e86c6848bf/)](https://archive.softwareheritage.org/swh:1:snp:62a2f748e52113016cf291c4b8c944e86c6848bf)
8
+
9
+ Track, visualize, and export research timelines — from first AI interaction to scientific discovery.
10
+
11
+ `research-timeline` documents the **process** of research, not just its artifacts: every milestone of a project (the first AI interaction that shaped the protocol, the first QPU commit with its evidence, pivots, controls, submissions, publications) is recorded in a single versioned JSON file with typed events, quantitative metrics, and supporting evidence.
12
+
13
+ ## Features
14
+
15
+ - **Typed events** — `T0`, `T1`…`Tn`, `pivot`, `control`, `submission`, `publication`, `milestone`
16
+ - **Metrics** — attach any quantitative result (z-scores, shots, backend, MI, …) to an event
17
+ - **Evidence** — git commits, IBM Quantum job IDs, data links, code links
18
+ - **AI-role disclosure** — each timeline declares how AI was used (`cognitive_prosthesis`, `co_pilot`, `autonomous_agent`)
19
+ - **Exports** — LaTeX table (papers/reports), Markdown, standalone HTML, schema.org JSON-LD
20
+ - **Validate** — structural checks with CI-friendly exit codes
21
+ - **Simple JSON storage** — human readable, diff-friendly, git-native, zero lock-in
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install research-timeline
27
+ # or from source:
28
+ pip install git+https://github.com/Strugiss/research-timeline.git
29
+ # or editable for development:
30
+ pip install -e .
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```bash
36
+ # Initialize a timeline
37
+ research-timeline init --output timeline.json
38
+
39
+ # Log a typed event (with metrics and evidence)
40
+ research-timeline log T1 --desc "First commit: 14 QPU experiments, Z>50sigma" \
41
+ --z-combined 50.0 --git-commit c3ddc4a --job-ids abc,def --tags commit,qpu
42
+
43
+ # List events (optionally with metrics)
44
+ research-timeline list --metrics
45
+
46
+ # Export to LaTeX (papers), Markdown, HTML, or JSON-LD
47
+ research-timeline export --format latex -o timeline.tex
48
+ research-timeline export --format markdown -o timeline.md
49
+ research-timeline export --format html -o timeline.html
50
+ research-timeline export --format jsonld -o timeline.jsonld
51
+
52
+ # Validate
53
+ research-timeline validate
54
+ ```
55
+
56
+ See [example/timeline.json](example/timeline.json) for a real-world timeline
57
+ (the PASM DTC Discovery project, N47Lab MatterMemory research program) and the
58
+ generated exports in `example/`.
59
+
60
+ ## Event IDs
61
+
62
+ `T0`, `T1`, `T2`, …, `Tn` (ordered research phases) plus special events:
63
+ `pivot`, `control`, `submission`, `publication`, `milestone`.
64
+
65
+ ## File Format
66
+
67
+ A timeline is a single JSON document:
68
+
69
+ ```json
70
+ {
71
+ "project": {"name": "PASM DTC Discovery", "description": "...", "domain": "quantum"},
72
+ "author": {"name": "N47Lab", "affiliation": "independent", "ai_role": "cognitive_prosthesis"},
73
+ "events": [{
74
+ "id": "T1", "type": "T1", "date": "2026-07-31",
75
+ "description": "First commit: 14 QPU experiments, Z>50sigma",
76
+ "metrics": {"z_score_combined": 50.0},
77
+ "evidence": {"git_commit": "c3ddc4a", "job_ids": ["abc"]}
78
+ }]
79
+ }
80
+ ```
81
+
82
+ The schema is documented in `schema/timeline.schema.json` (JSON Schema draft-07).
83
+
84
+ ## Related work
85
+
86
+ - **Notes/task tools (Notion, Obsidian, Logseq, Trello)** — general-purpose notes or task boards; no typed research phases, no JSON schema, no CI validation, cloud-dependent storage.
87
+ - **Experiment trackers (Weights & Biases, MLflow, DVC)** — track model *runs*, artifacts, and metrics; they do not record researcher-level process events (first insight, pivot, control, submission) nor provide paper-oriented exports (LaTeX).
88
+ - **Notebooks (Jupyter, Quarto)** — rich narrative but unstructured; no enforcement of a timeline schema, no machine-readable JSON-LD export.
89
+ - **Lab notebooks (ELN, Code Ocean)** — heavyweight, instrument-locked, or cloud-bound; too heavy for long-term, single-author project process tracking.
90
+
91
+ `research-timeline` fills the empty slot: a zero-dependency, git-native, JSON-backed tracker for the research *narrative* with an explicit schema, structured `evidence` fields, and LaTeX/JSON-LD exports for the writing stage.
92
+
93
+ ## AI Usage Disclosure
94
+
95
+ This project was developed with the assistance of generative AI tools (interactive AI coding assistants with agentic workflows). AI assistance covered initial code scaffolding, the test suite, and documentation drafting (June–August 2026). All AI-assisted output was reviewed line-by-line by the human author, whose design decisions (schema, event types, export contracts, `ai_role` semantics) drove the project; algorithmic behavior is covered by the test suite in `tests/` and by CI. See [AI_POLICY.md](AI_POLICY.md) for the full policy.
96
+
97
+ ## Development & Contributing
98
+
99
+ See [CONTRIBUTING.md](CONTRIBUTING.md) — tests, coding conventions, and governance.
100
+
101
+ ```bash
102
+ pip install -e ".[dev]"
103
+ pytest tests/ -v
104
+ ```
105
+
106
+ ## Software Heritage
107
+
108
+ This repository is archived in permanent storage:
109
+ `swh:1:snp:62a2f748e52113016cf291c4b8c944e86c6848bf`
110
+
111
+ ## License
112
+
113
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "research-timeline"
7
+ version = "0.2.1"
8
+ description = "Track, visualize, and export research timelines from first AI interaction to scientific discovery"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [
12
+ {name = "Alessandro Tulli", email = "2injob.at2@gmail.com"}
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Science/Research",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Scientific/Engineering",
24
+ "Topic :: Scientific/Engineering :: Physics",
25
+ "Topic :: Scientific/Engineering :: Information Analysis",
26
+ ]
27
+ requires-python = ">=3.9"
28
+ dependencies = [
29
+ "typer>=0.9.0",
30
+ "pydantic>=2.5.0",
31
+ "rich>=13.0.0",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "pytest>=7.4.0",
37
+ "pytest-cov>=4.1.0",
38
+ "ruff>=0.1.0",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/Strugiss/research-timeline"
43
+ Repository = "https://github.com/Strugiss/research-timeline"
44
+ Issues = "https://github.com/Strugiss/research-timeline/issues"
45
+
46
+ [project.scripts]
47
+ research-timeline = "research_timeline.cli:app"
48
+
49
+ [tool.setuptools.packages.find]
50
+ where = ["src"]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+
55
+ [tool.ruff]
56
+ line-length = 100
57
+ target-version = "py39"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ """Research Timeline package."""
2
+ __version__ = "0.2.0"
@@ -0,0 +1,6 @@
1
+ """Entry point for research-timeline CLI."""
2
+ from .cli import app
3
+
4
+ if __name__ == "__main__":
5
+ import sys
6
+ sys.exit(app())
@@ -0,0 +1,415 @@
1
+ import typer
2
+ from typing import Optional, List
3
+ from datetime import date
4
+ from pathlib import Path
5
+ import json
6
+ import re
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+ from rich import print as rprint
10
+
11
+ from .models import ResearchTimeline, ProjectInfo, Author, Event
12
+
13
+ VALID_EVENT_ID = re.compile(r"^T([0-9]+|n)$|^(pivot|control|submission|publication|milestone)$")
14
+
15
+ app = typer.Typer(
16
+ name="research-timeline",
17
+ help="Track, visualize, and export research timelines from first AI interaction to scientific discovery",
18
+ add_completion=False,
19
+ no_args_is_help=True,
20
+ )
21
+
22
+ console = Console()
23
+
24
+ # Default timeline file
25
+ DEFAULT_TIMELINE_FILE = Path(".research-timeline.json")
26
+
27
+
28
+ def load_timeline(path: Path) -> Optional[dict]:
29
+ """Load timeline from JSON file."""
30
+ if not path.exists():
31
+ return None
32
+ with open(path, 'r', encoding='utf-8') as f:
33
+ return json.load(f)
34
+
35
+
36
+ def save_timeline(path: Path, data: dict) -> None:
37
+ """Save timeline to JSON file."""
38
+ with open(path, 'w', encoding='utf-8') as f:
39
+ json.dump(data, f, indent=2, ensure_ascii=False)
40
+
41
+
42
+ def print_ok(msg: str) -> None:
43
+ """Print success message."""
44
+ print(f"[OK] {msg}")
45
+
46
+
47
+ def print_error(msg: str) -> None:
48
+ print(f"[ERROR] {msg}")
49
+
50
+
51
+ @app.command()
52
+ def init(
53
+ name: str = typer.Option(..., "--name", "-n", help="Project name"),
54
+ description: str = typer.Option(..., "--desc", "-d", help="Project description"),
55
+ domain: str = typer.Option("quantum", "--domain", help="Research domain"),
56
+ author_name: str = typer.Option(..., "--author", "-a", help="Author name"),
57
+ affiliation: str = typer.Option("independent", "--affiliation", help="Affiliation"),
58
+ orcid: Optional[str] = typer.Option(None, "--orcid", help="ORCID URL"),
59
+ background: str = typer.Option("without academic degrees", "--background", help="Academic background"),
60
+ ai_role: str = typer.Option("cognitive_prosthesis", "--ai-role", help="AI role"),
61
+ output: Path = typer.Option(Path(".research-timeline.json"), "--output", "-o", help="Output file"),
62
+ ):
63
+ """Initialize a new research timeline."""
64
+ from datetime import date
65
+
66
+ timeline = {
67
+ "project": {
68
+ "name": name,
69
+ "description": description,
70
+ "domain": domain
71
+ },
72
+ "author": {
73
+ "name": author_name,
74
+ "affiliation": affiliation,
75
+ "orcid": orcid,
76
+ "background": background,
77
+ "ai_role": ai_role
78
+ },
79
+ "events": [],
80
+ "created_at": str(date.today()),
81
+ "updated_at": str(date.today()),
82
+ "version": "1.0"
83
+ }
84
+
85
+ output_path = Path(output)
86
+ with open(output_path, 'w', encoding='utf-8') as f:
87
+ json.dump(timeline, f, indent=2, ensure_ascii=False)
88
+
89
+ print(f"[OK] Initialized timeline at {output}")
90
+ print(f" Project: {name} ({domain})")
91
+ print(f" Author: {author_name} ({background})")
92
+
93
+
94
+ @app.command()
95
+ def log(
96
+ event_id: str = typer.Argument(..., help="Event ID (T0, T1, T2, Tn, pivot, control, etc.)"),
97
+ event_type: str = typer.Option(..., "--type", help="Event type (T0, T1, T2, Tn, pivot, control, submission, publication, milestone)"),
98
+ event_date: str = typer.Option(str(date.today()), "--date", help="Event date (YYYY-MM-DD)"),
99
+ description: str = typer.Option(..., "--desc", help="Event description"),
100
+ tags: Optional[str] = typer.Option(None, "--tags", help="Tags (comma-separated)"),
101
+ z_score: Optional[float] = typer.Option(None, "--z-score", help="Z-score"),
102
+ shots: Optional[int] = typer.Option(None, "--shots", help="Number of shots"),
103
+ backend: Optional[str] = typer.Option(None, "--backend", help="Quantum backend"),
104
+ job_ids: Optional[List[str]] = typer.Option(None, "--job-ids", help="Job IDs (comma-separated)"),
105
+ z_score_combined: Optional[float] = typer.Option(None, "--z-combined", help="Combined Z-score"),
106
+ git_commit: Optional[str] = typer.Option(None, "--git-commit", help="Git commit hash"),
107
+ data_links: Optional[List[str]] = typer.Option(None, "--data-links", help="Data links (comma-separated)"),
108
+ code_links: Optional[List[str]] = typer.Option(None, "--code-links", help="Code links (comma-separated)"),
109
+ timeline_file: Path = typer.Option(Path(".research-timeline.json"), "--file", "-f", help="Timeline file"),
110
+ ):
111
+ """Log a new event to the timeline."""
112
+ if not Path(timeline_file).exists():
113
+ print(f"[ERROR] Timeline file not found: {timeline_file}. Run 'init' first.")
114
+ raise typer.Exit(1)
115
+
116
+ with open(timeline_file, 'r', encoding='utf-8') as f:
117
+ timeline = json.load(f)
118
+
119
+ # Validate event ID
120
+ event_id = event_id.strip()
121
+ if not VALID_EVENT_ID.match(event_id):
122
+ print("[ERROR] Event ID must match T0, T1, T2, Tn, pivot, control, submission, publication, milestone")
123
+ raise typer.Exit(1)
124
+
125
+ # Check for duplicate ID
126
+ for event in timeline.get("events", []):
127
+ if event.get("id") == event_id:
128
+ print(f"[ERROR] Event ID {event_id} already exists")
129
+ raise typer.Exit(1)
130
+
131
+ # Build event
132
+ event = {
133
+ "id": event_id,
134
+ "type": event_type,
135
+ "date": event_date,
136
+ "description": description,
137
+ "tags": [t.strip() for t in tags.split(",")] if tags else [],
138
+ }
139
+
140
+ # Add metrics if provided
141
+ metrics = {}
142
+ if z_score is not None:
143
+ metrics["z_score"] = z_score
144
+ if shots is not None:
145
+ metrics["shots"] = shots
146
+ if backend:
147
+ metrics["backend"] = backend
148
+ if z_score_combined is not None:
149
+ metrics["z_score_combined"] = z_score_combined
150
+ if metrics:
151
+ event["metrics"] = {k: v for k, v in metrics.items() if v is not None}
152
+
153
+ # Evidence
154
+ evidence = {}
155
+ if git_commit:
156
+ evidence["git_commit"] = git_commit
157
+ if job_ids:
158
+ evidence["job_ids"] = [j.strip() for tok in job_ids for j in tok.split(",") if j.strip()]
159
+ if data_links:
160
+ evidence["data_links"] = [d.strip() for tok in data_links for d in tok.split(",") if d.strip()]
161
+ if code_links:
162
+ evidence["code_links"] = [c.strip() for tok in code_links for c in tok.split(",") if c.strip()]
163
+ if evidence:
164
+ event["evidence"] = {k: v for k, v in evidence.items() if v}
165
+
166
+ timeline.setdefault("events", []).append(event)
167
+ timeline["updated_at"] = str(date.today())
168
+
169
+ with open(timeline_file, 'w', encoding='utf-8') as f:
170
+ json.dump(timeline, f, indent=2, ensure_ascii=False)
171
+
172
+ print(f"[OK] Logged event {event_id}: {description}")
173
+
174
+
175
+ @app.command()
176
+ def list(
177
+ timeline_file: Path = typer.Option(Path(".research-timeline.json"), "--file", "-f", help="Timeline file"),
178
+ show_metrics: bool = typer.Option(False, "--metrics", "-m", help="Show metrics"),
179
+ ):
180
+ """List all events in the timeline."""
181
+ if not Path(timeline_file).exists():
182
+ print(f"[ERROR] Timeline file not found: {timeline_file}")
183
+ raise typer.Exit(1)
184
+
185
+ with open(timeline_file, 'r', encoding='utf-8') as f:
186
+ timeline = json.load(f)
187
+
188
+ # Print as simple text table to avoid Unicode issues on Windows
189
+ print("Research Timeline")
190
+ print("=" * 80)
191
+ header = f"{'ID':<4} | {'Type':<6} | {'Date':<12} | {'Description':<40} | {'Tags':<20} | {'Metrics':<30}"
192
+ print(header)
193
+ print("-" * 120)
194
+
195
+ for event in timeline.get("events", []):
196
+ metrics_str = ""
197
+ if event.get("metrics"):
198
+ # Replace sigma character for Windows compatibility
199
+ metrics_str = ", ".join(f"{k}={v}".replace('\u03c3', 'sigma') for k, v in event.get("metrics", {}).items() if v is not None)
200
+
201
+ desc = event["description"][:50]
202
+ tags_str = ", ".join(event.get("tags", []))
203
+ print(f"{event['id']:<4} | {event['type']:<6} | {event['date']:<12} | {desc:<50} | {tags_str:<20} | {metrics_str}")
204
+
205
+
206
+ @app.command()
207
+ def export(
208
+ format: str = typer.Option("latex", "--format", help="Export format: latex, markdown, jsonld, html"),
209
+ timeline_file: Path = typer.Option(Path(".research-timeline.json"), "--file", help="Timeline file"),
210
+ output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file"),
211
+ ):
212
+ """Export timeline to various formats."""
213
+ if not Path(timeline_file).exists():
214
+ print(f"[ERROR] Timeline file not found: {timeline_file}")
215
+ raise typer.Exit(1)
216
+
217
+ with open(timeline_file, 'r', encoding='utf-8') as f:
218
+ timeline = json.load(f)
219
+
220
+ if format == "latex":
221
+ output_content = export_latex(timeline)
222
+ elif format == "markdown":
223
+ output_content = export_markdown(timeline)
224
+ elif format == "jsonld":
225
+ output_content = export_jsonld(timeline)
226
+ elif format == "html":
227
+ output_content = export_html(timeline)
228
+ else:
229
+ print(f"[ERROR] Unknown format: {format}")
230
+ raise typer.Exit(1)
231
+
232
+ if output:
233
+ with open(output, 'w', encoding='utf-8') as f:
234
+ f.write(output_content)
235
+ print(f"[OK] Exported to {output}")
236
+ else:
237
+ print(output_content)
238
+
239
+
240
+ def export_latex(timeline: dict) -> str:
241
+ """Export timeline as LaTeX table."""
242
+ lines = [
243
+ "\\begin{table}[ht]",
244
+ "\\centering",
245
+ "\\caption{Research Timeline: First AI Interaction to Discovery}",
246
+ "\\label{tab:timeline}",
247
+ "\\begin{tabular}{lllll}",
248
+ "\\toprule",
249
+ "\\textbf{Phase} & \\textbf{Date} & \\textbf{Event} & \\textbf{Metrics} & \\textbf{Evidence} \\\\",
250
+ "\\midrule",
251
+ ]
252
+
253
+ for event in timeline.get("events", []):
254
+ metrics = event.get("metrics", {})
255
+ metrics_str = ", ".join(f"{k}={v}" for k, v in metrics.items() if v is not None) if metrics else ""
256
+ evidence = event.get("evidence", {})
257
+ evidence_str = ""
258
+ if evidence.get("git_commit"):
259
+ evidence_str += f"git {evidence['git_commit'][:8]} "
260
+ if evidence.get("job_ids"):
261
+ evidence_str += f"jobs: {', '.join(evidence['job_ids'][:3])} "
262
+
263
+ lines.append(
264
+ f"{event['id']} & {event['date']} & {event['description'][:50]} & {metrics_str} & {evidence_str} \\\\"
265
+ )
266
+
267
+ lines.extend([
268
+ "\\bottomrule",
269
+ "\\end{tabular}",
270
+ "\\end{table}",
271
+ ])
272
+ return "\n".join(lines)
273
+
274
+
275
+ def export_markdown(timeline: dict) -> str:
276
+ """Export timeline as Markdown table."""
277
+ lines = ["## Research Timeline", "", "| Phase | Date | Event | Metrics | Evidence |", "|-------|------|-------|---------|----------|"]
278
+
279
+ for event in timeline.get("events", []):
280
+ metrics = event.get("metrics", {})
281
+ metrics_str = ", ".join(f"{k}={v}" for k, v in metrics.items() if v is not None) if metrics else ""
282
+ evidence = event.get("evidence", {})
283
+ evidence_str = ""
284
+ if evidence.get("git_commit"):
285
+ evidence_str += f"git {evidence['git_commit'][:8]} "
286
+ if evidence.get("job_ids"):
287
+ evidence_str += f"jobs: {', '.join(evidence['job_ids'][:3])} "
288
+
289
+ lines.append(f"| {event['id']} | {event['date']} | {event['description'][:60]} | {metrics_str} | {evidence_str} |")
290
+
291
+ return "\n".join(lines)
292
+
293
+
294
+ def export_jsonld(timeline: dict) -> str:
295
+ """Export as JSON-LD for Schema.org."""
296
+ context = {
297
+ "@context": "https://schema.org",
298
+ "@type": "ResearchProject",
299
+ "name": timeline.get("project", {}).get("name", ""),
300
+ "description": timeline.get("project", {}).get("description", ""),
301
+ "author": {
302
+ "@type": "Person",
303
+ "name": timeline.get("author", {}).get("name", ""),
304
+ "affiliation": timeline.get("author", {}).get("affiliation", ""),
305
+ "orcid": timeline.get("author", {}).get("orcid", "")
306
+ },
307
+ "dateCreated": timeline.get("created_at"),
308
+ "dateModified": timeline.get("updated_at"),
309
+ "hasPart": []
310
+ }
311
+
312
+ for event in timeline.get("events", []):
313
+ event_obj = {
314
+ "@type": "ResearchEvent",
315
+ "identifier": event["id"],
316
+ "name": event["description"],
317
+ "startDate": event["date"],
318
+ "eventType": event["type"],
319
+ "description": event["description"]
320
+ }
321
+ if event.get("metrics"):
322
+ event_obj["measurementTechnique"] = ", ".join(f"{k}: {v}" for k, v in event.get("metrics", {}).items() if v is not None)
323
+ context["hasPart"].append(event_obj)
324
+
325
+ return json.dumps(context, indent=2, ensure_ascii=False)
326
+
327
+
328
+ def export_html(timeline: dict) -> str:
329
+ """Export as interactive HTML widget."""
330
+ html = """<!DOCTYPE html>
331
+ <html>
332
+ <head>
333
+ <title>Research Timeline</title>
334
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
335
+ <style>
336
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
337
+ .timeline { position: relative; padding: 20px 0; }
338
+ .event { position: relative; padding: 15px; margin: 10px 0; background: #1a1f2a; border-radius: 8px; border-left: 4px solid #6fc3df; }
339
+ .event-id { font-family: monospace; color: #6fc3df; font-weight: bold; }
340
+ .event-date { color: #8892a8; font-size: 0.9em; }
341
+ .event-desc { color: #e0e4ee; margin: 8px 0; }
342
+ .event-metrics { color: #facc15; font-family: monospace; font-size: 0.85em; }
343
+ .event-tags { margin-top: 8px; }
344
+ .tag { display: inline-block; background: rgba(111,195,223,0.12); color: #6fc3df; padding: 2px 8px; border-radius: 12px; font-size: 0.75em; margin-right: 4px; }
345
+ </style>
346
+ </head>
347
+ <body>
348
+ <h1>Research Timeline</h1>
349
+ <div class="timeline">"""
350
+
351
+ for event in timeline.get("events", []):
352
+ metrics = event.get("metrics", {})
353
+ metrics_html = ""
354
+ if event.get("metrics"):
355
+ metrics_html = f'<div class="event-metrics">{" | ".join(f"{k}={v}" for k, v in event["metrics"].items() if v is not None)}</div>'
356
+
357
+ tags_html = ""
358
+ if event.get("tags"):
359
+ tags_html = '<div class="event-tags">' + "".join(f'<span class="tag">{t}</span>' for t in event["tags"]) + '</div>'
360
+
361
+ html += f"""
362
+ <div class="event">
363
+ <div class="event-id">{event['id']}</div>
364
+ <div class="event-date">{event['date']}</div>
365
+ <div class="event-desc">{event['description']}</div>
366
+ {metrics_html}
367
+ {tags_html}
368
+ </div>"""
369
+
370
+ html += """ </div>
371
+ </body>
372
+ </html>"""
373
+ return html
374
+
375
+
376
+ @app.command()
377
+ def validate(
378
+ timeline_file: Path = typer.Option(Path(".research-timeline.json"), "--file", "-f", help="Timeline file"),
379
+ ):
380
+ """Validate timeline against schema."""
381
+ if not Path(timeline_file).exists():
382
+ print(f"[ERROR] Timeline file not found: {timeline_file}")
383
+ raise typer.Exit(1)
384
+
385
+ with open(timeline_file, 'r', encoding='utf-8') as f:
386
+ timeline = json.load(f)
387
+
388
+ # Basic validation
389
+ errors = []
390
+ if "project" not in timeline:
391
+ errors.append("Missing 'project' section")
392
+ if "author" not in timeline:
393
+ errors.append("Missing 'author' section")
394
+ if "events" not in timeline or not timeline["events"]:
395
+ errors.append("Missing or empty 'events' section")
396
+
397
+ for i, event in enumerate(timeline.get("events", [])):
398
+ if "id" not in event:
399
+ errors.append(f"Event {i}: missing 'id'")
400
+ if "type" not in event:
401
+ errors.append(f"Event {event.get('id', i)}: missing 'type'")
402
+ if "date" not in event:
403
+ errors.append(f"Event {event.get('id', i)}: missing 'date'")
404
+
405
+ if errors:
406
+ print("[ERROR] Validation failed:")
407
+ for err in errors:
408
+ print(f" [ERROR] {err}")
409
+ raise typer.Exit(1)
410
+ else:
411
+ print("[OK] Timeline is valid!")
412
+
413
+
414
+ if __name__ == "__main__":
415
+ app()
@@ -0,0 +1,134 @@
1
+ from typing import Optional, List, Dict, Any
2
+ from pydantic import BaseModel, Field
3
+ from datetime import date
4
+ from enum import Enum
5
+
6
+
7
+ class ProjectInfo(BaseModel):
8
+ name: str = Field(..., description="Project name")
9
+ description: str = Field(..., description="Brief project description")
10
+ domain: str = Field(..., description="Research domain (e.g., quantum, biology, ml, physics, chemistry, materials, computer_science, other)")
11
+
12
+
13
+ class Author(BaseModel):
14
+ name: str = Field(..., description="Author name")
15
+ affiliation: str = Field(..., description="Institutional affiliation (or 'independent')")
16
+ orcid: Optional[str] = Field(None, description="ORCID identifier")
17
+ background: str = Field(..., description="Academic background / credentials")
18
+ ai_role: str = Field(..., description="Role of AI in the research process (e.g., cognitive_prosthesis, co_pilot, autonomous_agent)")
19
+
20
+
21
+ class Evidence(BaseModel):
22
+ git_commit: Optional[str] = Field(None, description="Git commit hash")
23
+ job_ids: List[str] = Field(default_factory=list, description="Related job IDs")
24
+ data_links: List[str] = Field(default_factory=list, description="Links to data repositories")
25
+ code_links: List[str] = Field(default_factory=list, description="Links to code repositories")
26
+
27
+
28
+ class Metrics(BaseModel):
29
+ z_score: Optional[float] = Field(None, description="Z-score for the event")
30
+ shots: Optional[int] = Field(None, ge=0, description="Number of measurement shots")
31
+ backend: Optional[str] = Field(None, description="Quantum backend used")
32
+ job_ids: List[str] = Field(default_factory=list, description="Job IDs from quantum processor")
33
+ z_score_combined: Optional[float] = Field(None, description="Combined Z-score (Fisher method)")
34
+
35
+
36
+ class Event(BaseModel):
37
+ id: str = Field(..., pattern=r"^T([0-9]+|n)$|^(pivot|control|submission|publication|milestone)$", description="Event identifier (T0, T1, T2, Tn, pivot, control, submission, publication, milestone)")
38
+ event_type: str = Field(..., description="Event type", alias="type")
39
+ event_date: date = Field(..., description="Event date (ISO 8601)", alias="date")
40
+ description: str = Field(..., description="Human-readable description of the event")
41
+ tags: List[str] = Field(default_factory=list, description="Tags for categorization")
42
+ metrics: Optional[Dict[str, Any]] = Field(None, description="Quantitative metrics for the event (arbitrary key-value pairs)")
43
+ evidence: Optional[Dict[str, Any]] = Field(None, description="Supporting evidence for the event")
44
+
45
+
46
+ class ResearchTimeline(BaseModel):
47
+ project: dict = Field(..., description="Project information")
48
+ author: dict = Field(..., description="Author information")
49
+ events: List[dict] = Field(..., min_length=1, description="Timeline events")
50
+ created_at: date = Field(default_factory=date.today, description="Timeline creation date")
51
+ updated_at: date = Field(default_factory=date.today, description="Last update date")
52
+ version: str = Field(default="1.0", description="Timeline schema version")
53
+
54
+
55
+ if __name__ == "__main__":
56
+ import json
57
+ timeline = {
58
+ "project": {
59
+ "name": "PASM DTC Discovery",
60
+ "description": "Observation of Classical Prethermal DTC on IBM Heron",
61
+ "domain": "quantum"
62
+ },
63
+ "author": {
64
+ "name": "N47Lab",
65
+ "affiliation": "independent",
66
+ "orcid": "https://orcid.org/0009-0008-9201-6080",
67
+ "background": "without academic degrees",
68
+ "ai_role": "cognitive_prosthesis"
69
+ },
70
+ "events": [
71
+ {
72
+ "id": "T0",
73
+ "type": "T0",
74
+ "date": "2026-06-06",
75
+ "description": "First AI interaction: setup, theory, protocol design",
76
+ "tags": ["setup", "theory", "protocol_design"],
77
+ "evidence": {
78
+ "data_links": [".opencode log"]
79
+ }
80
+ },
81
+ {
82
+ "id": "T1",
83
+ "type": "T1",
84
+ "date": "2026-07-31",
85
+ "description": "First commit: 14 QPU experiments, Z>50σ",
86
+ "tags": ["commit", "qpu", "baseline"],
87
+ "metrics": {"z_score_combined": 50.0},
88
+ "evidence": {
89
+ "git_commit": "c3ddc4a",
90
+ "job_ids": []
91
+ }
92
+ }
93
+ ]
94
+ }
95
+ print("Schema validation passed!")
96
+ import json
97
+ print(json.dumps({
98
+ "project": {
99
+ "name": "PASM DTC Discovery",
100
+ "description": "Observation of Classical Prethermal DTC on IBM Heron",
101
+ "domain": "quantum"
102
+ },
103
+ "author": {
104
+ "name": "N47Lab",
105
+ "affiliation": "independent",
106
+ "orcid": "https://orcid.org/0009-0008-9201-6080",
107
+ "background": "without academic degrees",
108
+ "ai_role": "cognitive_prosthesis"
109
+ },
110
+ "events": [
111
+ {
112
+ "id": "T0",
113
+ "type": "T0",
114
+ "date": "2026-06-06",
115
+ "description": "First AI interaction: setup, theory, protocol design",
116
+ "tags": ["setup", "theory", "protocol_design"],
117
+ "evidence": {
118
+ "data_links": [".opencode log"]
119
+ }
120
+ },
121
+ {
122
+ "id": "T1",
123
+ "type": "T1",
124
+ "date": "2026-07-31",
125
+ "description": "First commit: 14 QPU experiments, Z>50σ",
126
+ "tags": ["commit", "qpu", "baseline"],
127
+ "metrics": {"z_score_combined": 50.0},
128
+ "evidence": {
129
+ "git_commit": "c3ddc4a",
130
+ "job_ids": []
131
+ }
132
+ }
133
+ ]
134
+ }, indent=2))
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: research-timeline
3
+ Version: 0.2.1
4
+ Summary: Track, visualize, and export research timelines from first AI interaction to scientific discovery
5
+ Author-email: Alessandro Tulli <2injob.at2@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Strugiss/research-timeline
8
+ Project-URL: Repository, https://github.com/Strugiss/research-timeline
9
+ Project-URL: Issues, https://github.com/Strugiss/research-timeline/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering
19
+ Classifier: Topic :: Scientific/Engineering :: Physics
20
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: typer>=0.9.0
25
+ Requires-Dist: pydantic>=2.5.0
26
+ Requires-Dist: rich>=13.0.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
29
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
30
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # Research Timeline
34
+
35
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21855315.svg)](https://doi.org/10.5281/zenodo.21855315)
36
+ [![CI](https://github.com/Strugiss/research-timeline/actions/workflows/ci.yml/badge.svg)](https://github.com/Strugiss/research-timeline/actions/workflows/ci.yml)
37
+ [![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
+ [![SWH](https://archive.softwareheritage.org/badge/swh:1:snp:62a2f748e52113016cf291c4b8c944e86c6848bf/)](https://archive.softwareheritage.org/swh:1:snp:62a2f748e52113016cf291c4b8c944e86c6848bf)
40
+
41
+ Track, visualize, and export research timelines — from first AI interaction to scientific discovery.
42
+
43
+ `research-timeline` documents the **process** of research, not just its artifacts: every milestone of a project (the first AI interaction that shaped the protocol, the first QPU commit with its evidence, pivots, controls, submissions, publications) is recorded in a single versioned JSON file with typed events, quantitative metrics, and supporting evidence.
44
+
45
+ ## Features
46
+
47
+ - **Typed events** — `T0`, `T1`…`Tn`, `pivot`, `control`, `submission`, `publication`, `milestone`
48
+ - **Metrics** — attach any quantitative result (z-scores, shots, backend, MI, …) to an event
49
+ - **Evidence** — git commits, IBM Quantum job IDs, data links, code links
50
+ - **AI-role disclosure** — each timeline declares how AI was used (`cognitive_prosthesis`, `co_pilot`, `autonomous_agent`)
51
+ - **Exports** — LaTeX table (papers/reports), Markdown, standalone HTML, schema.org JSON-LD
52
+ - **Validate** — structural checks with CI-friendly exit codes
53
+ - **Simple JSON storage** — human readable, diff-friendly, git-native, zero lock-in
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install research-timeline
59
+ # or from source:
60
+ pip install git+https://github.com/Strugiss/research-timeline.git
61
+ # or editable for development:
62
+ pip install -e .
63
+ ```
64
+
65
+ ## Usage
66
+
67
+ ```bash
68
+ # Initialize a timeline
69
+ research-timeline init --output timeline.json
70
+
71
+ # Log a typed event (with metrics and evidence)
72
+ research-timeline log T1 --desc "First commit: 14 QPU experiments, Z>50sigma" \
73
+ --z-combined 50.0 --git-commit c3ddc4a --job-ids abc,def --tags commit,qpu
74
+
75
+ # List events (optionally with metrics)
76
+ research-timeline list --metrics
77
+
78
+ # Export to LaTeX (papers), Markdown, HTML, or JSON-LD
79
+ research-timeline export --format latex -o timeline.tex
80
+ research-timeline export --format markdown -o timeline.md
81
+ research-timeline export --format html -o timeline.html
82
+ research-timeline export --format jsonld -o timeline.jsonld
83
+
84
+ # Validate
85
+ research-timeline validate
86
+ ```
87
+
88
+ See [example/timeline.json](example/timeline.json) for a real-world timeline
89
+ (the PASM DTC Discovery project, N47Lab MatterMemory research program) and the
90
+ generated exports in `example/`.
91
+
92
+ ## Event IDs
93
+
94
+ `T0`, `T1`, `T2`, …, `Tn` (ordered research phases) plus special events:
95
+ `pivot`, `control`, `submission`, `publication`, `milestone`.
96
+
97
+ ## File Format
98
+
99
+ A timeline is a single JSON document:
100
+
101
+ ```json
102
+ {
103
+ "project": {"name": "PASM DTC Discovery", "description": "...", "domain": "quantum"},
104
+ "author": {"name": "N47Lab", "affiliation": "independent", "ai_role": "cognitive_prosthesis"},
105
+ "events": [{
106
+ "id": "T1", "type": "T1", "date": "2026-07-31",
107
+ "description": "First commit: 14 QPU experiments, Z>50sigma",
108
+ "metrics": {"z_score_combined": 50.0},
109
+ "evidence": {"git_commit": "c3ddc4a", "job_ids": ["abc"]}
110
+ }]
111
+ }
112
+ ```
113
+
114
+ The schema is documented in `schema/timeline.schema.json` (JSON Schema draft-07).
115
+
116
+ ## Related work
117
+
118
+ - **Notes/task tools (Notion, Obsidian, Logseq, Trello)** — general-purpose notes or task boards; no typed research phases, no JSON schema, no CI validation, cloud-dependent storage.
119
+ - **Experiment trackers (Weights & Biases, MLflow, DVC)** — track model *runs*, artifacts, and metrics; they do not record researcher-level process events (first insight, pivot, control, submission) nor provide paper-oriented exports (LaTeX).
120
+ - **Notebooks (Jupyter, Quarto)** — rich narrative but unstructured; no enforcement of a timeline schema, no machine-readable JSON-LD export.
121
+ - **Lab notebooks (ELN, Code Ocean)** — heavyweight, instrument-locked, or cloud-bound; too heavy for long-term, single-author project process tracking.
122
+
123
+ `research-timeline` fills the empty slot: a zero-dependency, git-native, JSON-backed tracker for the research *narrative* with an explicit schema, structured `evidence` fields, and LaTeX/JSON-LD exports for the writing stage.
124
+
125
+ ## AI Usage Disclosure
126
+
127
+ This project was developed with the assistance of generative AI tools (interactive AI coding assistants with agentic workflows). AI assistance covered initial code scaffolding, the test suite, and documentation drafting (June–August 2026). All AI-assisted output was reviewed line-by-line by the human author, whose design decisions (schema, event types, export contracts, `ai_role` semantics) drove the project; algorithmic behavior is covered by the test suite in `tests/` and by CI. See [AI_POLICY.md](AI_POLICY.md) for the full policy.
128
+
129
+ ## Development & Contributing
130
+
131
+ See [CONTRIBUTING.md](CONTRIBUTING.md) — tests, coding conventions, and governance.
132
+
133
+ ```bash
134
+ pip install -e ".[dev]"
135
+ pytest tests/ -v
136
+ ```
137
+
138
+ ## Software Heritage
139
+
140
+ This repository is archived in permanent storage:
141
+ `swh:1:snp:62a2f748e52113016cf291c4b8c944e86c6848bf`
142
+
143
+ ## License
144
+
145
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/research_timeline/__init__.py
5
+ src/research_timeline/__main__.py
6
+ src/research_timeline/cli.py
7
+ src/research_timeline/models.py
8
+ src/research_timeline.egg-info/PKG-INFO
9
+ src/research_timeline.egg-info/SOURCES.txt
10
+ src/research_timeline.egg-info/dependency_links.txt
11
+ src/research_timeline.egg-info/entry_points.txt
12
+ src/research_timeline.egg-info/requires.txt
13
+ src/research_timeline.egg-info/top_level.txt
14
+ tests/test_cli.py
15
+ tests/test_models.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ research-timeline = research_timeline.cli:app
@@ -0,0 +1,8 @@
1
+ typer>=0.9.0
2
+ pydantic>=2.5.0
3
+ rich>=13.0.0
4
+
5
+ [dev]
6
+ pytest>=7.4.0
7
+ pytest-cov>=4.1.0
8
+ ruff>=0.1.0
@@ -0,0 +1 @@
1
+ research_timeline
@@ -0,0 +1,146 @@
1
+ """Tests for research-timeline CLI (typer-based)."""
2
+
3
+ import json
4
+ import os
5
+ import tempfile
6
+
7
+ from typer.testing import CliRunner
8
+
9
+ from research_timeline.cli import app
10
+
11
+ runner = CliRunner()
12
+
13
+
14
+ def _tmp_path():
15
+ tmp = tempfile.mkdtemp()
16
+ return os.path.join(tmp, "tl.json")
17
+
18
+
19
+ def _init(path):
20
+ return runner.invoke(app, [
21
+ "init",
22
+ "--name", "Test Project",
23
+ "--desc", "Test description",
24
+ "--domain", "quantum",
25
+ "--author", "Test Author",
26
+ "--affiliation", "independent",
27
+ "--output", path,
28
+ ])
29
+
30
+
31
+ def test_init_creates_timeline():
32
+ path = _tmp_path()
33
+ result = _init(path)
34
+ assert result.exit_code == 0
35
+ assert os.path.exists(path)
36
+ data = json.load(open(path, encoding="utf-8"))
37
+ assert data["project"]["name"] == "Test Project"
38
+ assert data["events"] == []
39
+
40
+
41
+ def test_log_then_list():
42
+ path = _tmp_path()
43
+ _init(path)
44
+ r = runner.invoke(app, [
45
+ "log", "T0", "--type", "T0",
46
+ "--desc", "First AI interaction", "--tags", "setup,theory",
47
+ "--file", path,
48
+ ])
49
+ assert r.exit_code == 0
50
+ r2 = runner.invoke(app, ["list", "--file", path])
51
+ assert r2.exit_code == 0
52
+ assert "T0" in r2.stdout
53
+
54
+
55
+ def test_log_metrics_and_evidence():
56
+ path = _tmp_path()
57
+ _init(path)
58
+ r = runner.invoke(app, [
59
+ "log", "T1", "--type", "T1",
60
+ "--desc", "First QPU run", "--z-combined", "50.0",
61
+ "--z-score", "12.5", "--shots", "8192", "--backend", "ibm_kingston",
62
+ "--job-ids", "abc,def", "--git-commit", "c3ddc4a",
63
+ "--tags", "qpu", "--file", path,
64
+ ])
65
+ assert r.exit_code == 0
66
+ data = json.load(open(path, encoding="utf-8"))
67
+ event = data["events"][0]
68
+ assert event["metrics"]["z_score_combined"] == 50.0
69
+ assert event["metrics"]["shots"] == 8192
70
+ assert event["metrics"]["backend"] == "ibm_kingston"
71
+ assert event["evidence"]["job_ids"] == ["abc", "def"]
72
+ assert event["evidence"]["git_commit"] == "c3ddc4a"
73
+
74
+
75
+ def test_log_valid_ids():
76
+ path = _tmp_path()
77
+ _init(path)
78
+ for eid in ["T0", "T1", "Tn", "pivot", "control", "submission", "publication", "milestone"]:
79
+ r = runner.invoke(app, [
80
+ "log", eid, "--type", eid, "--desc", f"Test {eid}", "--file", path,
81
+ ])
82
+ assert r.exit_code == 0, f"{eid} should be accepted"
83
+
84
+
85
+ def test_log_invalid_id_rejected():
86
+ path = _tmp_path()
87
+ _init(path)
88
+ r = runner.invoke(app, [
89
+ "log", "X0", "--type", "T0", "--desc", "Bad", "--file", path,
90
+ ])
91
+ assert r.exit_code != 0
92
+
93
+
94
+ def test_log_duplicate_id_rejected():
95
+ path = _tmp_path()
96
+ _init(path)
97
+ runner.invoke(app, ["log", "T0", "--type", "T0", "--desc", "First", "--file", path])
98
+ r = runner.invoke(app, ["log", "T0", "--type", "T0", "--desc", "Dup", "--file", path])
99
+ assert r.exit_code != 0
100
+ assert "already exists" in r.stdout.lower()
101
+
102
+
103
+ def test_export_latex():
104
+ path = _tmp_path()
105
+ _init(path)
106
+ runner.invoke(app, ["log", "T0", "--type", "T0", "--desc", "Start", "--file", path])
107
+ r = runner.invoke(app, ["export", "--format", "latex", "--file", path])
108
+ assert r.exit_code == 0
109
+ assert "tabular" in r.stdout
110
+ assert "T0" in r.stdout
111
+
112
+
113
+ def test_export_jsonld():
114
+ path = _tmp_path()
115
+ _init(path)
116
+ r = runner.invoke(app, ["export", "--format", "jsonld", "--file", path])
117
+ assert r.exit_code == 0
118
+ assert '"@context": "https://schema.org"' in r.stdout
119
+ assert '"@type": "ResearchProject"' in r.stdout
120
+
121
+
122
+ def test_export_html_to_file():
123
+ path = _tmp_path()
124
+ _init(path)
125
+ runner.invoke(app, ["log", "Tn", "--type", "Tn", "--desc", "Discovery", "--file", path])
126
+ out = os.path.join(os.path.dirname(path), "out.html")
127
+ r = runner.invoke(app, ["export", "--format", "html", "--file", path, "-o", out])
128
+ assert r.exit_code == 0
129
+ assert os.path.exists(out)
130
+ assert "<html>" in open(out, encoding="utf-8").read()
131
+
132
+
133
+ def test_validate_ok_and_fail():
134
+ path = _tmp_path()
135
+ _init(path)
136
+ runner.invoke(app, ["log", "T0", "--type", "T0", "--desc", "Ok", "--file", path])
137
+ r = runner.invoke(app, ["validate", "--file", path])
138
+ assert r.exit_code == 0
139
+ r2 = runner.invoke(app, ["validate", "--file", os.path.join(os.path.dirname(path), "none.json")])
140
+ assert r2.exit_code != 0
141
+
142
+
143
+ def test_help():
144
+ r = runner.invoke(app, ["--help"])
145
+ assert r.exit_code == 0
146
+ assert "research-timeline" in r.stdout
@@ -0,0 +1,89 @@
1
+ import pytest
2
+ from datetime import date
3
+ from research_timeline.models import ResearchTimeline, ProjectInfo, Author, Event, Metrics, Evidence
4
+
5
+
6
+ def test_basic_timeline():
7
+ """Test basic timeline creation."""
8
+ timeline = {
9
+ "project": {
10
+ "name": "Test Project",
11
+ "description": "Test",
12
+ "domain": "quantum"
13
+ },
14
+ "author": {
15
+ "name": "Test Author",
16
+ "affiliation": "independent",
17
+ "orcid": "https://orcid.org/0000-0000-0000-0000",
18
+ "background": "without academic degrees",
19
+ "ai_role": "cognitive_prosthesis"
20
+ },
21
+ "events": [
22
+ {
23
+ "id": "T0",
24
+ "type": "T0",
25
+ "date": "2026-06-06",
26
+ "description": "First AI interaction",
27
+ "tags": ["setup"],
28
+ "metrics": {},
29
+ "evidence": {}
30
+ }
31
+ ],
32
+ "created_at": "2026-08-06",
33
+ "updated_at": "2026-08-06",
34
+ "version": "1.0"
35
+ }
36
+
37
+ # This would be validated by pydantic in real usage
38
+ assert True
39
+
40
+
41
+ def test_event_validation():
42
+ """Test event ID validation."""
43
+ from research_timeline.models import Event
44
+
45
+ # Valid IDs
46
+ Event(id="T0", type="T0", date="2026-06-06", description="Test")
47
+ Event(id="T1", type="T1", date="2026-06-06", description="Test")
48
+ Event(id="Tn", type="Tn", date="2026-06-06", description="Test")
49
+ Event(id="pivot", type="pivot", date="2026-08-01", description="Pivot")
50
+ Event(id="control", type="control", date="2026-08-01", description="Control")
51
+ Event(id="submission", type="submission", date="2026-08-01", description="Submission")
52
+ Event(id="publication", type="publication", date="2026-08-01", description="Publication")
53
+ Event(id="milestone", type="milestone", date="2026-08-01", description="Milestone")
54
+
55
+ # Invalid ID should raise
56
+ try:
57
+ Event(id="X0", type="T0", date="2026-06-06", description="Test")
58
+ assert False, "Should have raised"
59
+ except ValueError:
60
+ pass # Expected
61
+
62
+
63
+ def test_metrics_optional():
64
+ """Test that metrics are optional."""
65
+ from research_timeline.models import Event
66
+
67
+ event = Event(
68
+ id="T0",
69
+ type="T0",
70
+ date="2026-06-06",
71
+ description="Test"
72
+ )
73
+ assert event.metrics is None
74
+
75
+ # With metrics (as dict)
76
+ event2 = Event(
77
+ id="T1",
78
+ type="T1",
79
+ date="2026-07-01",
80
+ description="Test",
81
+ metrics={"z_score": 50.0, "shots": 8192}
82
+ )
83
+ assert event2.metrics is not None
84
+ assert event2.metrics["z_score"] == 50.0
85
+ assert event2.metrics["shots"] == 8192
86
+
87
+
88
+ if __name__ == "__main__":
89
+ pytest.main([__file__, "-v"])