roadmap-core 0.1.0__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) 2026 roadmap-core contributors
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,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: roadmap-core
3
+ Version: 0.1.0
4
+ Summary: The roadmap work-item and arc graph: status derivation, validation, and markdown rendering. Stdlib-only, so any repo can adopt it without adopting a backend.
5
+ License: MIT
6
+ Project-URL: Source, https://github.com/gald33/lucille/tree/main/roadmap-core
7
+ Keywords: roadmap,backlog,dependency-graph,agents,planning
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Build Tools
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Provides-Extra: files
17
+ Requires-Dist: pyyaml>=6; extra == "files"
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # roadmap-core
23
+
24
+ The roadmap graph: status derivation, dependency and relation edges, arc state,
25
+ validation, and the markdown renderers behind `roadmap/ROADMAP.md` and `ARCS.md`.
26
+
27
+ **Stdlib-only and dependency-free**, which is the point rather than a nicety.
28
+ Three properties depend on it:
29
+
30
+ 1. `scripts/roadmap.py` loads `roadmap_core/graph.py` **by path**, so a coding
31
+ agent in a checkout can read the backlog with no install, no DB, no admin
32
+ token and no network.
33
+ 2. The Lucille backend imports the same module, so a status derived by the API
34
+ and a status rendered into the committed markdown cannot disagree — one
35
+ implementation, two callers. That divergence is a failure this repo has been
36
+ bitten by before.
37
+ 3. It is the extraction seam. The roadmap is being prepared to run as its own
38
+ product whose default store is a single SQLite file with nothing to
39
+ provision; a package that pulled in a web framework or an ORM could not be
40
+ adopted by another repo without adopting Lucille with it.
41
+
42
+ ## Storage
43
+
44
+ `store.py` is the schema — one SQLite file, `CREATE TABLE IF NOT EXISTS` on first
45
+ open, no migration step. `stores.py` is what you talk to:
46
+
47
+ | | `LocalStore` | `ApiStore` |
48
+ |---|---|---|
49
+ | needs | a writable path | a `call(method, path, payload)` |
50
+ | provisioning | none | a running host |
51
+ | `claim`/`release` | one `BEGIN IMMEDIATE` transaction | one HTTP request |
52
+ | `impact` | raises `Unsupported` | the host's tickets |
53
+
54
+ Both satisfy the same `Store` protocol, so a caller never branches on which it
55
+ holds. `ApiStore` is constructed with the host's own caller and holds no token,
56
+ no URL and no `urllib` import — auth stays entirely a host concern, and
57
+ `test_stores.py` asserts that rather than trusting it.
58
+
59
+ ```python
60
+ from roadmap_core.stores import LocalStore
61
+
62
+ with LocalStore("roadmap/roadmap.db") as store: # created if absent
63
+ store.upsert_item({"key": "a-thing", "title": "A thing"})
64
+ store.claim("a-thing", by="claude/some-branch")
65
+ ```
66
+
67
+ From the CLI, `--source local` on `push`, `claim`, `release` and `status`, or
68
+ `ROADMAP_SOURCE=local` once. `ROADMAP_STORE` sets the path.
69
+
70
+ ## Adopting it in another project
71
+
72
+ Measured end to end by `tests/test_adoption.py`, which runs the CLI as a
73
+ subprocess against a scratch project with nothing on the path but this package
74
+ — no backend, no FastAPI, no SQLAlchemy, no Postgres, no server, no token.
75
+
76
+ ```bash
77
+ pip install "roadmap-core[files]" # [files] adds PyYAML, which authoring needs
78
+ mkdir -p roadmap/items
79
+ export ROADMAP_SOURCE=local
80
+ ```
81
+
82
+ That is the whole install. `roadmap` is a console script that comes with the
83
+ package — there is nothing to copy. (It used to say `curl -o scripts/roadmap.py
84
+ <this repo>/scripts/roadmap.py`, and this repo is private, so the package was
85
+ installable, importable and useless to anyone outside it.)
86
+
87
+ Then the ordinary loop, which needs nothing else:
88
+
89
+ ```bash
90
+ cat > roadmap/items/first-thing.yaml <<'YAML'
91
+ id: first-thing
92
+ title: The first thing to do
93
+ status: ready
94
+ evidence: |
95
+ Why this is worth doing, and how you will know it worked.
96
+ YAML
97
+
98
+ roadmap push # files -> store
99
+ roadmap ready # what is startable
100
+ roadmap claim first-thing
101
+ roadmap release first-thing
102
+ ```
103
+
104
+ The store is one SQLite file at `roadmap/roadmap.db`. There is nothing to
105
+ provision and no migration to run: it is created on first open.
106
+
107
+ **Two things that are conventions rather than choices**, both found by doing
108
+ this rather than by reading the code:
109
+
110
+ - **Your project root is the nearest ancestor holding `roadmap/items` or
111
+ `.git`**, so the commands work from anywhere inside it. Set
112
+ `ROADMAP_REPO_ROOT` to pin it. Deliberately not bare `roadmap/`: that is a
113
+ directory the tool *creates*, so keying on it let one command run in the
114
+ wrong place mint the marker that made that place look like a project
115
+ forever after.
116
+ - **Authoring is writing a YAML file**, not calling an API. `push` is what
117
+ moves it into the store; there is no `roadmap new`. That is deliberate:
118
+ filing an item belongs in a diff somebody reviews.
119
+
120
+ `ROADMAP_SOURCE=local` selects the SQLite store. Without it the CLI expects the
121
+ API store, which is how Lucille runs it — see `roadmap_core.stores`.
122
+
123
+ ### CI
124
+
125
+ Copy `templates/roadmap.yml` to `.github/workflows/roadmap.yml`. That is the
126
+ whole CI story for the floor:
127
+
128
+ ```
129
+ push # files -> store, rebuilt fresh each run
130
+ validate # schema, dangling dependencies, cycles
131
+ sync --check # is the committed ROADMAP.md still what the graph renders?
132
+ ```
133
+
134
+ No schedule, no credentials, no bot identity, no commit back to the default
135
+ branch, no self-hosted runner. `tests/test_adoption.py` reads the commands out
136
+ of that file and runs them, so a template that has drifted from the CLI fails
137
+ rather than reading as tested.
138
+
139
+ The third line is the one that earns the workflow. `ROADMAP.md` is generated but
140
+ committed — that is what lets an agent read the backlog with no install and no
141
+ network — and a generated file nobody regenerates is a file that lies.
142
+
143
+ **Do not commit `roadmap/roadmap.db`.** It is derived: `push` rebuilds it from
144
+ the YAML on first open, and a binary file in git conflicts on every claim. The
145
+ files are the record; the store is the transaction that decides who gets one.
146
+
147
+ ### Upgrading to a served store
148
+
149
+ The floor's simplicity comes from one property: the store is inside the
150
+ checkout, so there is no second copy to drift from. Move the store to a server —
151
+ so that claims are visible across machines the moment they are taken, rather
152
+ than when a branch merges — and four things come back, none of which the
153
+ template can supply for you:
154
+
155
+ | What returns | Why |
156
+ |---|---|
157
+ | A **credential** step | the store is now behind auth, and the CLI needs a token per run |
158
+ | A **wait-for-reachable** step | a concurrent deploy can hold the store down, and being early is not being wrong |
159
+ | `pull` and a **bot commit** | the store now knows things no checkout does, and they have to land in the files an agent reads |
160
+ | A **schedule** | finishing an item is usually a code change somewhere else entirely, so no path filter can catch it — only re-asking on a clock can |
161
+
162
+ `ApiStore` is constructed with your own caller, so the auth stays yours (see the
163
+ table under [Storage](#storage)). Lucille's `.github/workflows/roadmap-sync.yml`
164
+ is the worked example of all four, and the reason it is not shipped as a
165
+ template: almost every line of it is a consequence of Lucille's own deployment,
166
+ and handing an adopter that machinery for a problem they do not have reads as
167
+ required rather than as one option.
168
+
169
+ ## What is NOT here
170
+
171
+ HTTP, auth, and the CLI. The graph is pure functions over plain dicts keyed by
172
+ `key`, so the same code serves DB rows, API payloads and parsed YAML with no
173
+ adapter. Lucille's own persistence stays in `backend/app/crud/roadmap.py` over
174
+ SQLAlchemy; the two definitions of the same three tables are held together by
175
+ `backend/tests/test_roadmap_store_parity.py`, which asserts both the columns and
176
+ the row dicts the two readers produce.
177
+
178
+ The package's tests live in `tests/` here and import nothing outside the stdlib.
179
+ `.github/workflows/roadmap-core-tests.yml` runs them in a job that fails if
180
+ `app`, `fastapi`, `sqlalchemy` or `yaml` can be imported at all — the isolation
181
+ is asserted, not assumed.
@@ -0,0 +1,160 @@
1
+ # roadmap-core
2
+
3
+ The roadmap graph: status derivation, dependency and relation edges, arc state,
4
+ validation, and the markdown renderers behind `roadmap/ROADMAP.md` and `ARCS.md`.
5
+
6
+ **Stdlib-only and dependency-free**, which is the point rather than a nicety.
7
+ Three properties depend on it:
8
+
9
+ 1. `scripts/roadmap.py` loads `roadmap_core/graph.py` **by path**, so a coding
10
+ agent in a checkout can read the backlog with no install, no DB, no admin
11
+ token and no network.
12
+ 2. The Lucille backend imports the same module, so a status derived by the API
13
+ and a status rendered into the committed markdown cannot disagree — one
14
+ implementation, two callers. That divergence is a failure this repo has been
15
+ bitten by before.
16
+ 3. It is the extraction seam. The roadmap is being prepared to run as its own
17
+ product whose default store is a single SQLite file with nothing to
18
+ provision; a package that pulled in a web framework or an ORM could not be
19
+ adopted by another repo without adopting Lucille with it.
20
+
21
+ ## Storage
22
+
23
+ `store.py` is the schema — one SQLite file, `CREATE TABLE IF NOT EXISTS` on first
24
+ open, no migration step. `stores.py` is what you talk to:
25
+
26
+ | | `LocalStore` | `ApiStore` |
27
+ |---|---|---|
28
+ | needs | a writable path | a `call(method, path, payload)` |
29
+ | provisioning | none | a running host |
30
+ | `claim`/`release` | one `BEGIN IMMEDIATE` transaction | one HTTP request |
31
+ | `impact` | raises `Unsupported` | the host's tickets |
32
+
33
+ Both satisfy the same `Store` protocol, so a caller never branches on which it
34
+ holds. `ApiStore` is constructed with the host's own caller and holds no token,
35
+ no URL and no `urllib` import — auth stays entirely a host concern, and
36
+ `test_stores.py` asserts that rather than trusting it.
37
+
38
+ ```python
39
+ from roadmap_core.stores import LocalStore
40
+
41
+ with LocalStore("roadmap/roadmap.db") as store: # created if absent
42
+ store.upsert_item({"key": "a-thing", "title": "A thing"})
43
+ store.claim("a-thing", by="claude/some-branch")
44
+ ```
45
+
46
+ From the CLI, `--source local` on `push`, `claim`, `release` and `status`, or
47
+ `ROADMAP_SOURCE=local` once. `ROADMAP_STORE` sets the path.
48
+
49
+ ## Adopting it in another project
50
+
51
+ Measured end to end by `tests/test_adoption.py`, which runs the CLI as a
52
+ subprocess against a scratch project with nothing on the path but this package
53
+ — no backend, no FastAPI, no SQLAlchemy, no Postgres, no server, no token.
54
+
55
+ ```bash
56
+ pip install "roadmap-core[files]" # [files] adds PyYAML, which authoring needs
57
+ mkdir -p roadmap/items
58
+ export ROADMAP_SOURCE=local
59
+ ```
60
+
61
+ That is the whole install. `roadmap` is a console script that comes with the
62
+ package — there is nothing to copy. (It used to say `curl -o scripts/roadmap.py
63
+ <this repo>/scripts/roadmap.py`, and this repo is private, so the package was
64
+ installable, importable and useless to anyone outside it.)
65
+
66
+ Then the ordinary loop, which needs nothing else:
67
+
68
+ ```bash
69
+ cat > roadmap/items/first-thing.yaml <<'YAML'
70
+ id: first-thing
71
+ title: The first thing to do
72
+ status: ready
73
+ evidence: |
74
+ Why this is worth doing, and how you will know it worked.
75
+ YAML
76
+
77
+ roadmap push # files -> store
78
+ roadmap ready # what is startable
79
+ roadmap claim first-thing
80
+ roadmap release first-thing
81
+ ```
82
+
83
+ The store is one SQLite file at `roadmap/roadmap.db`. There is nothing to
84
+ provision and no migration to run: it is created on first open.
85
+
86
+ **Two things that are conventions rather than choices**, both found by doing
87
+ this rather than by reading the code:
88
+
89
+ - **Your project root is the nearest ancestor holding `roadmap/items` or
90
+ `.git`**, so the commands work from anywhere inside it. Set
91
+ `ROADMAP_REPO_ROOT` to pin it. Deliberately not bare `roadmap/`: that is a
92
+ directory the tool *creates*, so keying on it let one command run in the
93
+ wrong place mint the marker that made that place look like a project
94
+ forever after.
95
+ - **Authoring is writing a YAML file**, not calling an API. `push` is what
96
+ moves it into the store; there is no `roadmap new`. That is deliberate:
97
+ filing an item belongs in a diff somebody reviews.
98
+
99
+ `ROADMAP_SOURCE=local` selects the SQLite store. Without it the CLI expects the
100
+ API store, which is how Lucille runs it — see `roadmap_core.stores`.
101
+
102
+ ### CI
103
+
104
+ Copy `templates/roadmap.yml` to `.github/workflows/roadmap.yml`. That is the
105
+ whole CI story for the floor:
106
+
107
+ ```
108
+ push # files -> store, rebuilt fresh each run
109
+ validate # schema, dangling dependencies, cycles
110
+ sync --check # is the committed ROADMAP.md still what the graph renders?
111
+ ```
112
+
113
+ No schedule, no credentials, no bot identity, no commit back to the default
114
+ branch, no self-hosted runner. `tests/test_adoption.py` reads the commands out
115
+ of that file and runs them, so a template that has drifted from the CLI fails
116
+ rather than reading as tested.
117
+
118
+ The third line is the one that earns the workflow. `ROADMAP.md` is generated but
119
+ committed — that is what lets an agent read the backlog with no install and no
120
+ network — and a generated file nobody regenerates is a file that lies.
121
+
122
+ **Do not commit `roadmap/roadmap.db`.** It is derived: `push` rebuilds it from
123
+ the YAML on first open, and a binary file in git conflicts on every claim. The
124
+ files are the record; the store is the transaction that decides who gets one.
125
+
126
+ ### Upgrading to a served store
127
+
128
+ The floor's simplicity comes from one property: the store is inside the
129
+ checkout, so there is no second copy to drift from. Move the store to a server —
130
+ so that claims are visible across machines the moment they are taken, rather
131
+ than when a branch merges — and four things come back, none of which the
132
+ template can supply for you:
133
+
134
+ | What returns | Why |
135
+ |---|---|
136
+ | A **credential** step | the store is now behind auth, and the CLI needs a token per run |
137
+ | A **wait-for-reachable** step | a concurrent deploy can hold the store down, and being early is not being wrong |
138
+ | `pull` and a **bot commit** | the store now knows things no checkout does, and they have to land in the files an agent reads |
139
+ | A **schedule** | finishing an item is usually a code change somewhere else entirely, so no path filter can catch it — only re-asking on a clock can |
140
+
141
+ `ApiStore` is constructed with your own caller, so the auth stays yours (see the
142
+ table under [Storage](#storage)). Lucille's `.github/workflows/roadmap-sync.yml`
143
+ is the worked example of all four, and the reason it is not shipped as a
144
+ template: almost every line of it is a consequence of Lucille's own deployment,
145
+ and handing an adopter that machinery for a problem they do not have reads as
146
+ required rather than as one option.
147
+
148
+ ## What is NOT here
149
+
150
+ HTTP, auth, and the CLI. The graph is pure functions over plain dicts keyed by
151
+ `key`, so the same code serves DB rows, API payloads and parsed YAML with no
152
+ adapter. Lucille's own persistence stays in `backend/app/crud/roadmap.py` over
153
+ SQLAlchemy; the two definitions of the same three tables are held together by
154
+ `backend/tests/test_roadmap_store_parity.py`, which asserts both the columns and
155
+ the row dicts the two readers produce.
156
+
157
+ The package's tests live in `tests/` here and import nothing outside the stdlib.
158
+ `.github/workflows/roadmap-core-tests.yml` runs them in a job that fails if
159
+ `app`, `fastapi`, `sqlalchemy` or `yaml` can be imported at all — the isolation
160
+ is asserted, not assumed.
@@ -0,0 +1,87 @@
1
+ [project]
2
+ name = "roadmap-core"
3
+ version = "0.1.0"
4
+ description = "The roadmap work-item and arc graph: status derivation, validation, and markdown rendering. Stdlib-only, so any repo can adopt it without adopting a backend."
5
+ requires-python = ">=3.11"
6
+ readme = "README.md"
7
+ license = { text = "MIT" }
8
+ keywords = ["roadmap", "backlog", "dependency-graph", "agents", "planning"]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3",
14
+ "Topic :: Software Development :: Build Tools",
15
+ ]
16
+ # Intentionally dependency-free, and this is load-bearing rather than tidy.
17
+ #
18
+ # Two callers need this logic and neither may drag the other in: the Lucille
19
+ # backend (which has a DB) and `scripts/roadmap.py` (which runs in any checkout
20
+ # with no app dependencies, no DB and no network — that is what lets an agent
21
+ # read the backlog offline). The script loads `roadmap_core/graph.py` BY PATH for
22
+ # exactly that reason, which only keeps working while this stays importable with
23
+ # nothing installed.
24
+ #
25
+ # It is also the extraction seam. The roadmap is being prepared to run as its own
26
+ # product whose default store is a single SQLite file; a package that pulls in a
27
+ # web framework or an ORM could not be adopted by another repo without adopting
28
+ # Lucille with it.
29
+ dependencies = []
30
+
31
+ # pytest only, and nothing else. The tests here are synthetic by rule — no DB, no
32
+ # app, no committed roadmap/items/ — so a repo adopting this package can run its
33
+ # proof of correctness without adopting anything from Lucille. That is checked in
34
+ # CI by installing ONLY this package and running them.
35
+ [project.optional-dependencies]
36
+ # Authoring an item IS writing `roadmap/items/<key>.yaml` — there is no
37
+ # `roadmap.py new`, deliberately, because filing work belongs in a diff
38
+ # somebody reviews. So the file paths need a YAML parser, and an adopter who
39
+ # installs the bare package finds that out from `push` rather than from here:
40
+ #
41
+ # PyYAML required for --source files (pip install pyyaml)
42
+ #
43
+ # Measured on 2026-08-18 by `tests/test_adoption.py` running in the isolation
44
+ # job, after the same test passed locally on a machine that happened to have
45
+ # PyYAML installed. An extra rather than a hard dependency because the top of
46
+ # this file means what it says: the store, the graph and the impact rule stay
47
+ # importable with nothing installed, which is what lets the backend and a bare
48
+ # checkout both use them. `[files]` is what the CLI needs, not what the library
49
+ # needs.
50
+ #
51
+ # Deliberately NOT in `dev`. `roadmap-core-tests.yml` asserts that `yaml` is
52
+ # not importable in that job — the guard that keeps "this package stands alone"
53
+ # from quietly becoming false. Putting PyYAML in `dev` to make one test pass
54
+ # would have disabled that guard to do it, which is the trade it exists to
55
+ # refuse. The adoption test runs in its own job with `[files]` installed.
56
+ files = ["pyyaml>=6"]
57
+ dev = ["pytest>=8"]
58
+
59
+ # The CLI ships WITH the package, and this line is the reason adopting it works
60
+ # at all. `README.md` used to say `curl -o scripts/roadmap.py <this repo>/...`,
61
+ # and this repo is private — so the package was installable, importable, and
62
+ # useless, because the 2000-line program that drives it could not be obtained.
63
+ # A dependency-free library whose tool you cannot get is not an adoptable tool.
64
+ [project.scripts]
65
+ roadmap = "roadmap_core.cli:main"
66
+
67
+ [project.urls]
68
+ Source = "https://github.com/gald33/lucille/tree/main/roadmap-core"
69
+
70
+ [build-system]
71
+ requires = ["setuptools>=68"]
72
+ build-backend = "setuptools.build_meta"
73
+
74
+ [tool.setuptools.packages.find]
75
+ include = ["roadmap_core*"]
76
+
77
+ # The CI template an adopter copies. Shipped in the sdist rather than left in
78
+ # the repo, because the README tells them to copy it and a pip install is all
79
+ # some adopters will ever have of this project.
80
+ [tool.setuptools.data-files]
81
+ "share/roadmap-core/templates" = ["templates/roadmap.yml"]
82
+
83
+ [tool.ruff]
84
+ line-length = 100
85
+
86
+ [tool.ruff.lint]
87
+ select = ["E", "F", "I", "B"]
@@ -0,0 +1,21 @@
1
+ """The roadmap graph, store and CLI, as a standalone package.
2
+
3
+ Import the graph explicitly rather than re-exporting it here::
4
+
5
+ from roadmap_core import graph
6
+ from roadmap_core.graph import derive_status, validate_graph
7
+
8
+ Kept bare on purpose, for a reason that outlived the one it was written for.
9
+ It used to be that ``scripts/roadmap.py`` loaded ``graph.py`` *by path* with
10
+ nothing installed, and a package ``__init__`` importing submodules would have
11
+ broken that. The CLI lives in here now (``roadmap_core.cli``) and imports its
12
+ siblings normally, so that particular hazard is gone.
13
+
14
+ What remains is better: ``graph``, ``store`` and ``stores`` are importable with
15
+ no third-party package present at all, and ``cli`` is importable without
16
+ PyYAML — it imports it inside the two functions that parse item files. A
17
+ ``__init__`` that reached for ``cli`` would drag that requirement onto every
18
+ caller of the graph, including the Lucille backend, which needs none of it.
19
+ ``roadmap-core-tests.yml`` asserts exactly this by running the suite in an
20
+ environment where ``yaml`` cannot be imported at all.
21
+ """