roadmap-core 0.2.2__tar.gz → 0.3.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.
Files changed (26) hide show
  1. {roadmap_core-0.2.2/roadmap_core.egg-info → roadmap_core-0.3.0}/PKG-INFO +47 -1
  2. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/README.md +46 -0
  3. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/pyproject.toml +6 -1
  4. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core/cli.py +53 -12
  5. roadmap_core-0.3.0/roadmap_core/mcp_server.py +662 -0
  6. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core/stores.py +35 -5
  7. {roadmap_core-0.2.2 → roadmap_core-0.3.0/roadmap_core.egg-info}/PKG-INFO +47 -1
  8. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core.egg-info/SOURCES.txt +2 -0
  9. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core.egg-info/entry_points.txt +1 -0
  10. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/tests/test_adoption.py +121 -0
  11. roadmap_core-0.3.0/tests/test_mcp_server.py +402 -0
  12. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/tests/test_stores.py +82 -0
  13. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/LICENSE +0 -0
  14. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core/__init__.py +0 -0
  15. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core/graph.py +0 -0
  16. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core/impact.py +0 -0
  17. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core/store.py +0 -0
  18. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core.egg-info/dependency_links.txt +0 -0
  19. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core.egg-info/requires.txt +0 -0
  20. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/roadmap_core.egg-info/top_level.txt +0 -0
  21. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/setup.cfg +0 -0
  22. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/templates/roadmap.yml +0 -0
  23. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/tests/test_arcs.py +0 -0
  24. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/tests/test_graph.py +0 -0
  25. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/tests/test_impact.py +0 -0
  26. {roadmap_core-0.2.2 → roadmap_core-0.3.0}/tests/test_store.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: roadmap-core
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
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
5
  License: MIT
6
6
  Project-URL: Source, https://github.com/gald33/roadmap-core
@@ -129,6 +129,17 @@ FAIL store roadmap/roadmap.db holds 0 items while roadmap/items/ holds 7.
129
129
  Seed it: `roadmap push`
130
130
  ```
131
131
 
132
+ **Who owns a claim depends on which store you have.** On the SQLite floor the
133
+ file is authoritative: CI rebuilds the store from `roadmap/items/*.yaml` every
134
+ run, so `push` takes a file's `claim` when it CREATES the item — otherwise a
135
+ held item renders as `ready` and `sync --check` fails for as long as anybody is
136
+ working. Against a served store the file is a *projection* of a store that
137
+ outlives the checkout, so a claim in a file is never pushed: a stale clone would
138
+ recreate one the store had already released. Either way `push` ignores it on
139
+ UPDATE, because the store is the live record of who holds what.
140
+
141
+ Not guessable from the field's name, which is why it is written down here.
142
+
132
143
  **Two things that are conventions rather than choices**, both found by doing
133
144
  this rather than by reading the code:
134
145
 
@@ -145,6 +156,41 @@ this rather than by reading the code:
145
156
  `ROADMAP_SOURCE=local` selects the SQLite store. Without it the CLI expects the
146
157
  API store, which is how Lucille runs it — see `roadmap_core.stores`.
147
158
 
159
+ ### Agents (MCP)
160
+
161
+ `roadmap-mcp` serves the same graph to a coding agent as MCP tools over stdio —
162
+ `ready`, `list`, `show`, `validate`, `claim`, `release`, `set_status`. Point a
163
+ client at it:
164
+
165
+ ```json
166
+ {
167
+ "mcpServers": {
168
+ "roadmap": {
169
+ "command": "roadmap-mcp",
170
+ "env": { "ROADMAP_SOURCE": "local" }
171
+ }
172
+ }
173
+ }
174
+ ```
175
+
176
+ **There is no extra to install and no SDK underneath.** `mcp_server.py` speaks
177
+ the JSON-RPC protocol in stdlib, so it is present wherever the package is —
178
+ which is the same reason the rest of this package is dependency-free, applied to
179
+ one more caller. An extra would be one an adopter can forget, for a server that
180
+ needs nothing.
181
+
182
+ **Reads default to the files**, so an agent in a fresh clone can ask what to
183
+ work on with no store, no server and no token. Writes need a store to arbitrate
184
+ them: set `ROADMAP_SOURCE=local` as above, or the write tools expect the API
185
+ store. There is deliberately no `files` write target — a claim nothing
186
+ adjudicated is not a claim, and two agents could each hold the same item.
187
+
188
+ **A write tells the agent to commit something, and it means it.** `claim`,
189
+ `release` and `set_status` project into `roadmap/items/<key>.yaml`, and on the
190
+ floor that projection *is* the durable record — an unmerged one is a claim no
191
+ other checkout can see. The tool result carries the CLI's own words for this
192
+ under `notes`.
193
+
148
194
  ### CI
149
195
 
150
196
  Copy `templates/roadmap.yml` to `.github/workflows/roadmap.yml`. That is the
@@ -108,6 +108,17 @@ FAIL store roadmap/roadmap.db holds 0 items while roadmap/items/ holds 7.
108
108
  Seed it: `roadmap push`
109
109
  ```
110
110
 
111
+ **Who owns a claim depends on which store you have.** On the SQLite floor the
112
+ file is authoritative: CI rebuilds the store from `roadmap/items/*.yaml` every
113
+ run, so `push` takes a file's `claim` when it CREATES the item — otherwise a
114
+ held item renders as `ready` and `sync --check` fails for as long as anybody is
115
+ working. Against a served store the file is a *projection* of a store that
116
+ outlives the checkout, so a claim in a file is never pushed: a stale clone would
117
+ recreate one the store had already released. Either way `push` ignores it on
118
+ UPDATE, because the store is the live record of who holds what.
119
+
120
+ Not guessable from the field's name, which is why it is written down here.
121
+
111
122
  **Two things that are conventions rather than choices**, both found by doing
112
123
  this rather than by reading the code:
113
124
 
@@ -124,6 +135,41 @@ this rather than by reading the code:
124
135
  `ROADMAP_SOURCE=local` selects the SQLite store. Without it the CLI expects the
125
136
  API store, which is how Lucille runs it — see `roadmap_core.stores`.
126
137
 
138
+ ### Agents (MCP)
139
+
140
+ `roadmap-mcp` serves the same graph to a coding agent as MCP tools over stdio —
141
+ `ready`, `list`, `show`, `validate`, `claim`, `release`, `set_status`. Point a
142
+ client at it:
143
+
144
+ ```json
145
+ {
146
+ "mcpServers": {
147
+ "roadmap": {
148
+ "command": "roadmap-mcp",
149
+ "env": { "ROADMAP_SOURCE": "local" }
150
+ }
151
+ }
152
+ }
153
+ ```
154
+
155
+ **There is no extra to install and no SDK underneath.** `mcp_server.py` speaks
156
+ the JSON-RPC protocol in stdlib, so it is present wherever the package is —
157
+ which is the same reason the rest of this package is dependency-free, applied to
158
+ one more caller. An extra would be one an adopter can forget, for a server that
159
+ needs nothing.
160
+
161
+ **Reads default to the files**, so an agent in a fresh clone can ask what to
162
+ work on with no store, no server and no token. Writes need a store to arbitrate
163
+ them: set `ROADMAP_SOURCE=local` as above, or the write tools expect the API
164
+ store. There is deliberately no `files` write target — a claim nothing
165
+ adjudicated is not a claim, and two agents could each hold the same item.
166
+
167
+ **A write tells the agent to commit something, and it means it.** `claim`,
168
+ `release` and `set_status` project into `roadmap/items/<key>.yaml`, and on the
169
+ floor that projection *is* the durable record — an unmerged one is a claim no
170
+ other checkout can see. The tool result carries the CLI's own words for this
171
+ under `notes`.
172
+
127
173
  ### CI
128
174
 
129
175
  Copy `templates/roadmap.yml` to `.github/workflows/roadmap.yml`. That is the
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "roadmap-core"
3
- version = "0.2.2"
3
+ version = "0.3.0"
4
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
5
  requires-python = ">=3.11"
6
6
  readme = "README.md"
@@ -63,6 +63,11 @@ dev = ["pytest>=8"]
63
63
  # A dependency-free library whose tool you cannot get is not an adoptable tool.
64
64
  [project.scripts]
65
65
  roadmap = "roadmap_core.cli:main"
66
+ # The MCP bridge, and no extra to install it: `mcp_server.py` speaks the stdio
67
+ # protocol in stdlib rather than taking the SDK, so it is already installed
68
+ # wherever the package is. An extra here would be an extra that is always
69
+ # empty — and one an adopter could forget, for a server that needs nothing.
70
+ roadmap-mcp = "roadmap_core.mcp_server:main"
66
71
 
67
72
  [project.urls]
68
73
  Source = "https://github.com/gald33/roadmap-core"
@@ -899,7 +899,16 @@ def cmd_push(args: argparse.Namespace) -> int:
899
899
  }
900
900
  try:
901
901
  if local is not None:
902
- local.upsert_item(payload)
902
+ # The claim goes to the LOCAL store only. Its store is ephemeral
903
+ # — CI rebuilds it from these files every run — so the file is
904
+ # the only durable record a claim has. The served store outlives
905
+ # the checkout, where the same move would recreate a claim it had
906
+ # already released. See `LocalStore`'s "claim on the floor".
907
+ local.upsert_item(dict(
908
+ payload,
909
+ claimed_by=item.get("claimed_by"),
910
+ claimed_at=item.get("claimed_at"),
911
+ ))
903
912
  else:
904
913
  _api("PUT", f"/admin/roadmap/{key}", payload)
905
914
  except stores.Pruned:
@@ -1000,7 +1009,15 @@ def cmd_status(args: argparse.Namespace) -> int:
1000
1009
  file=sys.stderr,
1001
1010
  )
1002
1011
  if args.status == "done":
1003
- print("done also drops the claim — `roadmap.py prune` when you are ready to clear it")
1012
+ # The store drops the hold; the FILE has to lose it too, or the next
1013
+ # push puts it straight back. On the floor that is not a cosmetic lag —
1014
+ # the file is where a claim durably lives there, so a stale block in it
1015
+ # is the claim, and `sync --check` goes red on an item nobody holds.
1016
+ # Projected here rather than left to `pull`, which is a served-store
1017
+ # command a floor project never runs.
1018
+ with switchboard_write_lock(args.key):
1019
+ _claim_projected(args.key, None)
1020
+ print("done also drops the claim — `roadmap prune` when you are ready to clear it")
1004
1021
  # No lease to drop here any more. This used to release the long-lived
1005
1022
  # mirror, which the store's `done` side effect would otherwise leave
1006
1023
  # reading as "claimed and alive" for up to four hours after the roadmap
@@ -1915,22 +1932,46 @@ def cmd_validate(args: argparse.Namespace) -> int:
1915
1932
 
1916
1933
 
1917
1934
  def installed_version() -> str:
1918
- """The version of the distribution this module was imported from.
1919
-
1920
- A source checkout that was never installed has no distribution metadata,
1921
- which is a legitimate way to run the CLI and must not raise. It is still
1922
- worth distinguishing in the output: "which version is this" and "there is no
1923
- package here, you are running a directory" are different answers to the same
1924
- question, and only one of them can be compared against a pin.
1935
+ """The version of the code that is actually running.
1936
+
1937
+ Not simply `metadata.version("roadmap-core")`, which reports the version of
1938
+ the INSTALLED DISTRIBUTION whether or not that is what got imported. Those
1939
+ diverge whenever a source tree shadows the install — `PYTHONPATH=.` in a
1940
+ checkout, an editable install left pointing at a directory that moved, a
1941
+ `sys.path` entry a wrapper script prepended. In every one of those the
1942
+ distribution's metadata is still perfectly readable and describes a copy of
1943
+ the code nobody is executing.
1944
+
1945
+ That is a small inaccuracy in most commands and a disqualifying one here.
1946
+ This function exists because two installs at different versions could not be
1947
+ told apart; a version string that can name the wrong one reintroduces the
1948
+ problem it was added to solve. Measured 2026-08-22 in this repository:
1949
+ `doctor` reported `0.2.1` while running 0.2.2 from a checkout.
1950
+
1951
+ So the location is compared, and a mismatch is stated rather than resolved —
1952
+ which one is "right" depends on what the caller meant, and printing both is
1953
+ the only answer that cannot be wrong.
1925
1954
  """
1926
1955
  try:
1927
- from importlib.metadata import PackageNotFoundError, version
1956
+ from importlib.metadata import PackageNotFoundError, distribution
1928
1957
  except ImportError: # pragma: no cover - stdlib since 3.8
1929
1958
  return "unknown"
1959
+
1960
+ running_from = Path(__file__).resolve().parent
1930
1961
  try:
1931
- return version("roadmap-core")
1962
+ dist = distribution("roadmap-core")
1932
1963
  except PackageNotFoundError:
1933
- return "not installed (running from a source tree)"
1964
+ return f"not installed (running from {running_from})"
1965
+
1966
+ declared = dist.version
1967
+ try:
1968
+ installed_at = Path(str(dist.locate_file("roadmap_core"))).resolve()
1969
+ except Exception: # noqa: BLE001 - a path we cannot resolve is a mismatch we cannot rule out
1970
+ return declared
1971
+
1972
+ if installed_at == running_from:
1973
+ return declared
1974
+ return f"{declared} installed, but running from {running_from}"
1934
1975
 
1935
1976
 
1936
1977
  class _Check: