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.
- research_timeline-0.2.1/LICENSE +21 -0
- research_timeline-0.2.1/PKG-INFO +145 -0
- research_timeline-0.2.1/README.md +113 -0
- research_timeline-0.2.1/pyproject.toml +57 -0
- research_timeline-0.2.1/setup.cfg +4 -0
- research_timeline-0.2.1/src/research_timeline/__init__.py +2 -0
- research_timeline-0.2.1/src/research_timeline/__main__.py +6 -0
- research_timeline-0.2.1/src/research_timeline/cli.py +415 -0
- research_timeline-0.2.1/src/research_timeline/models.py +134 -0
- research_timeline-0.2.1/src/research_timeline.egg-info/PKG-INFO +145 -0
- research_timeline-0.2.1/src/research_timeline.egg-info/SOURCES.txt +15 -0
- research_timeline-0.2.1/src/research_timeline.egg-info/dependency_links.txt +1 -0
- research_timeline-0.2.1/src/research_timeline.egg-info/entry_points.txt +2 -0
- research_timeline-0.2.1/src/research_timeline.egg-info/requires.txt +8 -0
- research_timeline-0.2.1/src/research_timeline.egg-info/top_level.txt +1 -0
- research_timeline-0.2.1/tests/test_cli.py +146 -0
- research_timeline-0.2.1/tests/test_models.py +89 -0
|
@@ -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
|
+
[](https://doi.org/10.5281/zenodo.21855315)
|
|
36
|
+
[](https://github.com/Strugiss/research-timeline/actions/workflows/ci.yml)
|
|
37
|
+
[](https://www.python.org/)
|
|
38
|
+
[](https://opensource.org/licenses/MIT)
|
|
39
|
+
[](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
|
+
[](https://doi.org/10.5281/zenodo.21855315)
|
|
4
|
+
[](https://github.com/Strugiss/research-timeline/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](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,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
|
+
[](https://doi.org/10.5281/zenodo.21855315)
|
|
36
|
+
[](https://github.com/Strugiss/research-timeline/actions/workflows/ci.yml)
|
|
37
|
+
[](https://www.python.org/)
|
|
38
|
+
[](https://opensource.org/licenses/MIT)
|
|
39
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -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"])
|