feat: generate canonical workflow contract

This commit is contained in:
lda
2026-08-03 07:30:31 +07:00 Verified
parent 55cfddd781
commit 99f0671771
5 changed files with 181 additions and 0 deletions
+14
View File
@@ -1,3 +1,11 @@
from .generate import generate_manifest
from .io import (
DEFAULT_MANIFEST_PATH,
ManifestDriftError,
canonical_manifest_json,
check_manifest,
write_manifest,
)
from .model import ContractManifest, JsonSchema, JsonValue, ManifestError
from .normalize import manifest_from_openrpc
@@ -7,4 +15,10 @@ __all__ = [
"JsonValue",
"ManifestError",
"manifest_from_openrpc",
"generate_manifest",
"DEFAULT_MANIFEST_PATH",
"ManifestDriftError",
"canonical_manifest_json",
"check_manifest",
"write_manifest",
]
+21
View File
@@ -0,0 +1,21 @@
from __future__ import annotations
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import cast
from wf_server import build_local_static_workflow_server
from wf_transport_rpc_http import create_rpc_app
from .model import ContractManifest
from .normalize import manifest_from_openrpc
def generate_manifest() -> ContractManifest:
"""Compose the real server against an isolated store and normalize OpenRPC."""
with TemporaryDirectory(prefix="wf-contract-manifest-") as directory:
server = build_local_static_workflow_server(Path(directory) / "store")
document = cast(dict[str, object], create_rpc_app(server).get_openrpc())
# Normalization deliberately drops framework metadata that could carry
# process-local paths or transport details.
return manifest_from_openrpc(document)
+44
View File
@@ -0,0 +1,44 @@
from __future__ import annotations
import json
from pathlib import Path
from .model import ContractManifest
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_MANIFEST_PATH = REPOSITORY_ROOT / "contracts" / "workflow-api.manifest.json"
class ManifestDriftError(RuntimeError):
"""Indicate that the checked manifest differs from the generated contract."""
def canonical_manifest_json(manifest: ContractManifest) -> str:
try:
return json.dumps(manifest, ensure_ascii=False, indent=2) + "\n"
except (TypeError, ValueError) as error:
raise ValueError(f"manifest is not canonically serializable: {error}") from error
def write_manifest(
manifest: ContractManifest, path: Path = DEFAULT_MANIFEST_PATH
) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(canonical_manifest_json(manifest), encoding="utf-8", newline="\n")
return path
def check_manifest(
manifest: ContractManifest, path: Path = DEFAULT_MANIFEST_PATH
) -> None:
expected = canonical_manifest_json(manifest).encode("utf-8")
try:
actual = path.read_bytes()
except FileNotFoundError as error:
raise ManifestDriftError(
f"{path} is missing; run `python -m wf_contract_manifest write`"
) from error
if actual != expected:
raise ManifestDriftError(
f"{path} is stale; run `python -m wf_contract_manifest write`"
)