feat(standards): add versioning + changelog standard, release templates, and Drone version guard

Add lib page 11 (SemVer contract, pre-1.0 rule, tag==version invariant, Keep a Changelog), the releasing wiki runbook, and canonical release templates (CHANGELOG.md, VERSION). Vendor them into the MCP bundle as templates/release + release.json and expose registry://release with list/get tools. Add a changelog component to the app template. Enforce the tag<->version contract in both Drone shapes: Type A check-version and the backfill loop verify pyproject version; Type B adds a VERSION guard step.
This commit is contained in:
2026-10-15 01:24:25 +05:30
parent a754366901
commit 8cc9631fe7
25 changed files with 625 additions and 10 deletions

View File

@@ -11,6 +11,7 @@ Resources:
registry://libraries (the structured catalog)
registry://components (the template component index)
registry://drone (the CI template index)
registry://release (the release template index)
Tools:
ping (from the base server)
@@ -20,6 +21,8 @@ Tools:
get_app_template (the whole app skeleton as a path -> content map)
list_drone_templates (the two CI shapes: PyPI package / Docker deployment)
get_drone_template (kind -> exact .drone.yml template)
list_release_templates (the changelog + version-file artefacts)
get_release_template (kind -> exact release template file)
Prompts:
intake (the questions to gather before building)
@@ -136,6 +139,32 @@ class StandardsMCPServer(MCPServer):
return {"error": "not_found", "path": str(path), "components": []}
return json.loads(path.read_bytes())
def _release(self) -> dict[str, Any]:
"""Load the release (versioning + changelog) template manifest."""
path = Path(self.mcp_root) / "release.json"
if not path.exists():
return {"error": "not_found", "path": str(path), "components": []}
return json.loads(path.read_bytes())
def _find_release_component(self, kind: str) -> dict[str, Any] | None:
for entry in self._release().get("components", []):
if entry.get("kind") == kind:
return entry
return None
def _read_release_component(self, entry: dict[str, Any]) -> dict[str, Any]:
"""Read a vendored release standard file."""
rel = entry["path"]
path = Path(self.mcp_root) / "templates" / "release" / rel
if not path.exists():
return {"error": "not_found", "path": str(path)}
return {
"kind": entry["kind"],
"path": rel,
"placeholder": entry.get("placeholder"),
"content": path.read_text(encoding="utf-8"),
}
def _find_drone_component(self, kind: str) -> dict[str, Any] | None:
for entry in self._drone().get("components", []):
if entry.get("kind") == kind:
@@ -228,6 +257,10 @@ class StandardsMCPServer(MCPServer):
def drone() -> dict[str, Any]:
return self._drone()
@self.app.resource("registry://release")
def release() -> dict[str, Any]:
return self._release()
@self.app.resource("docs://spec")
def spec() -> dict[str, Any]:
"""Return the pinned auth-server OpenAPI contract.
@@ -380,6 +413,45 @@ class StandardsMCPServer(MCPServer):
}
return self._read_drone_component(entry)
@self.app.tool()
def list_release_templates() -> list[dict[str, Any]]:
"""List the release/versioning template artefacts.
These are the files every repo carries for releases: a Keep a
Changelog ``CHANGELOG.md`` and, for services, a root ``VERSION``
file. See standards "11_versioning_and_changelog".
Returns:
list[dict[str, Any]]:
Entries with ``kind``, ``path``, ``placeholder`` and
``description``. Fetch one with :func:`get_release_template`.
"""
return self._release().get("components", [])
@self.app.tool()
def get_release_template(kind: str) -> dict[str, Any]:
"""Return the exact release standard file for a component kind.
Use this to drop the canonical changelog / version file into a repo
instead of inventing one. See :func:`list_release_templates` for
valid kinds ("changelog", "version-file").
Args:
kind (str): Component kind, e.g. "changelog" or "version-file".
Returns:
dict[str, Any]: ``{kind, path, placeholder, content}`` or an
error entry when the kind is unknown.
"""
entry = self._find_release_component(kind)
if entry is None:
return {
"error": "unknown_kind",
"kind": kind,
"available": [c["kind"] for c in self._release().get("components", [])],
}
return self._read_release_component(entry)
@self.app.prompt()
def intake() -> str:
"""Return the questions to gather before building an app.