Files
lda-wf/docs/historical/superpowers/plans/2026-08-01-workflow-contract-manifest.md
T

1143 lines
42 KiB
Markdown

# Workflow Contract Manifest Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Generate, validate, check in, and drift-check a deterministic transport-neutral manifest for the complete Python workflow OpenRPC contract.
**Architecture:** A new Python tooling package composes the real local workflow server, extracts its OpenRPC document, and passes it through a pure fail-closed normalizer. Canonical JSON I/O and a small module CLI own the checked artifact; a focused integration test makes Python contract drift visible without changing the TypeScript runtime or browser allowlist.
**Tech Stack:** Python 3.14, `fastapi-jsonrpc` OpenRPC output, `TypedDict`, standard-library `argparse` and `json`, pytest, Ruff, basedpyright
## Global Constraints
- Implement only the checked manifest and deterministic drift gate described in [`workflow contract manifest design`](../specs/2026-08-01-workflow-contract-manifest-design.md).
- Do not modify `web/`, generate TypeScript, translate JSON Schema to Effect Schema, upgrade Effect, add an HTTP endpoint, or expand the browser operation allowlist.
- The manifest must describe every server operation without authorizing any caller to use it.
- The full dotted method name is canonical identity; derived `namespace` and `action` are navigation metadata only.
- Sort operations by method and component dictionaries by key; preserve OpenRPC parameter order.
- Recursively remove only generated JSON Schema `title` keys. Preserve all other schema keywords, `{}`, optionality, nullability, and absent/`true`/`false` `additionalProperties` exactly.
- Preserve local `$ref` values. Reject external references, dangling references, and references into unsupported component namespaces.
- Treat `70` methods, `126` schemas, one declared error, and the five known union result components as initial baseline assertions, not hard generator limits.
- `check` must compare bytes and never rewrite the checked artifact.
- Use a temporary local workflow store. No temporary path, target URL, RPC path, credential, header, or environment-specific value may enter the manifest.
- Add docstrings or comments at the non-obvious recursive-normalization, reference-validation, and temporary-server seams.
- Use `tmp_path` in tests and scoped pytest commands with `-n 0`.
- Do not modify `.serena/` or Serena configuration.
---
### Task 1: Pure Manifest Model And Normalization
**Files:**
- Create: `src/wf_contract_manifest/__init__.py`
- Create: `src/wf_contract_manifest/model.py`
- Create: `src/wf_contract_manifest/normalize.py`
- Create: `tests/wf_contract_manifest/__init__.py`
- Create: `tests/wf_contract_manifest/fixtures.py`
- Create: `tests/wf_contract_manifest/test_normalize.py`
**Interfaces:**
- Consumes: an OpenRPC document as `Mapping[str, object]`.
- Produces: `ManifestError`, `JsonValue`, `JsonSchema`, `ContractManifest`, and `manifest_from_openrpc(document: Mapping[str, object]) -> ContractManifest`.
- Contract: this task validates the manifest envelope and normalizes valid operations; Task 2 adds complete reference-graph validation.
- [x] **Step 1: Add the reusable synthetic OpenRPC fixture**
Create `tests/wf_contract_manifest/fixtures.py` with a fixture that deliberately exercises ordering and schema preservation:
```python
from __future__ import annotations
from typing import Any
def synthetic_openrpc_document() -> dict[str, Any]:
return {
"openrpc": "1.2.6",
"info": {"title": "ignored", "version": "0"},
"methods": [
{
"name": "workflow.zeta.run",
"params": [],
"result": {
"name": "workflow.zeta.run_Result",
"schema": {"$ref": "#/components/schemas/ZetaResult"},
},
"errors": [{"$ref": "#/components/errors/5000"}],
},
{
"name": "workflow.alpha.inspect",
"params": [
{
"name": "optional_nullable",
"required": False,
"schema": {
"title": "Optional Nullable",
"anyOf": [{"type": "string"}, {"type": "null"}],
"x-future-keyword": {"title": "removed recursively", "value": 1},
},
},
{
"name": "required_closed",
"required": True,
"schema": {
"title": "Required Closed",
"type": "object",
"additionalProperties": False,
},
},
],
"result": {
"name": "workflow.alpha.inspect_Result",
"schema": {"$ref": "#/components/schemas/AlphaResult"},
},
"errors": [{"$ref": "#/components/errors/5000"}],
},
],
"components": {
"schemas": {
"ZetaResult": {
"title": "Zeta Result",
"type": "object",
"properties": {"extension": {"additionalProperties": True}},
},
"FreeJson": {},
"AlphaResult": {
"title": "Alpha Result",
"type": "object",
"properties": {
"mode": {"title": "Mode", "const": "alpha"},
"payload": {},
},
"required": ["mode", "payload"],
"if": {"properties": {"mode": {"const": "alpha"}}},
"then": {"required": ["payload"]},
"not": {"required": ["forbidden"]},
},
},
"errors": {
"5000": {
"code": 5000,
"message": "Workflow operation failed",
"data": {
"title": "Error Data",
"type": "object",
"additionalProperties": False,
},
}
},
},
}
```
- [x] **Step 2: Write failing happy-path normalization tests**
Create `tests/wf_contract_manifest/test_normalize.py`:
```python
from __future__ import annotations
from wf_contract_manifest import manifest_from_openrpc
from .fixtures import synthetic_openrpc_document
def test_normalizes_operations_and_components_deterministically() -> None:
manifest = manifest_from_openrpc(synthetic_openrpc_document())
assert manifest["manifest_version"] == 1
assert manifest["source"] == {
"format": "openrpc",
"openrpc_version": "1.2.6",
}
assert [operation["method"] for operation in manifest["operations"]] == [
"workflow.alpha.inspect",
"workflow.zeta.run",
]
assert list(manifest["components"]["schemas"]) == [
"AlphaResult",
"FreeJson",
"ZetaResult",
]
assert list(manifest["components"]["errors"]) == ["5000"]
def test_preserves_parameter_order_optionality_and_nullability() -> None:
operation = manifest_from_openrpc(synthetic_openrpc_document())["operations"][0]
assert [parameter["name"] for parameter in operation["params"]] == [
"optional_nullable",
"required_closed",
]
assert operation["params"][0]["required"] is False
assert operation["params"][0]["schema"]["anyOf"] == [
{"type": "string"},
{"type": "null"},
]
assert operation["params"][1]["required"] is True
assert operation["params"][1]["schema"]["additionalProperties"] is False
def test_removes_only_titles_and_preserves_unknown_schema_keywords() -> None:
manifest = manifest_from_openrpc(synthetic_openrpc_document())
optional_schema = manifest["operations"][0]["params"][0]["schema"]
assert "title" not in optional_schema
assert optional_schema["x-future-keyword"] == {"value": 1}
assert manifest["components"]["schemas"]["FreeJson"] == {}
assert manifest["components"]["schemas"]["ZetaResult"]["properties"] == {
"extension": {"additionalProperties": True}
}
def test_preserves_conditional_schema_keywords() -> None:
alpha = manifest_from_openrpc(synthetic_openrpc_document())["components"]["schemas"][
"AlphaResult"
]
assert alpha["if"] == {"properties": {"mode": {"const": "alpha"}}}
assert alpha["then"] == {"required": ["payload"]}
assert alpha["not"] == {"required": ["forbidden"]}
```
- [x] **Step 3: Run the tests and confirm the package is missing**
Run:
```powershell
New-Item -ItemType Directory -Force -Path '.pytest-tmp\manifest-task1' | Out-Null
.venv\Scripts\python.exe -m pytest tests\wf_contract_manifest\test_normalize.py -n 0 --basetemp '.pytest-tmp\manifest-task1' -q
```
Expected: collection fails with `ModuleNotFoundError: No module named 'wf_contract_manifest'`.
- [x] **Step 4: Add the typed manifest model**
Create `src/wf_contract_manifest/model.py` with these public definitions:
```python
from __future__ import annotations
from typing import TypedDict
type JsonScalar = None | bool | int | float | str
type JsonValue = JsonScalar | list[JsonValue] | dict[str, JsonValue]
type JsonSchema = dict[str, JsonValue]
class ManifestSource(TypedDict):
format: str
openrpc_version: str
class ManifestParameter(TypedDict):
name: str
required: bool
schema: JsonSchema
class ManifestResult(TypedDict):
schema: JsonSchema
class ManifestOperation(TypedDict):
method: str
namespace: list[str]
action: str
params: list[ManifestParameter]
result: ManifestResult
errors: list[JsonSchema]
class ManifestComponents(TypedDict):
schemas: dict[str, JsonSchema]
errors: dict[str, JsonValue]
class ContractManifest(TypedDict):
manifest_version: int
source: ManifestSource
operations: list[ManifestOperation]
components: ManifestComponents
class ManifestError(ValueError):
"""Report an invalid source contract with its exact OpenRPC document path."""
def __init__(self, path: str, message: str) -> None:
self.path = path
self.message = message
super().__init__(f"{path}: {message}")
```
Create `src/wf_contract_manifest/__init__.py` as the public tooling facade:
```python
from .model import ContractManifest, JsonSchema, JsonValue, ManifestError
from .normalize import manifest_from_openrpc
__all__ = [
"ContractManifest",
"JsonSchema",
"JsonValue",
"ManifestError",
"manifest_from_openrpc",
]
```
- [x] **Step 5: Implement the minimal pure normalizer**
Create `src/wf_contract_manifest/normalize.py`. Keep the envelope readers small and path-aware. The implementation must:
```python
from __future__ import annotations
from collections.abc import Mapping
from .model import (
ContractManifest,
JsonSchema,
JsonValue,
ManifestError,
ManifestOperation,
ManifestParameter,
)
def _mapping(value: object, path: str) -> Mapping[str, object]:
if not isinstance(value, Mapping):
raise ManifestError(path, "expected an object")
return value
def _list(value: object, path: str) -> list[object]:
if not isinstance(value, list):
raise ManifestError(path, "expected an array")
return value
def _string(value: object, path: str) -> str:
if not isinstance(value, str) or not value:
raise ManifestError(path, "expected a non-empty string")
return value
def _json_value(value: object, path: str) -> JsonValue:
if value is None or isinstance(value, bool | int | float | str):
return value
if isinstance(value, list):
return [_json_value(item, f"{path}[{index}]") for index, item in enumerate(value)]
if isinstance(value, Mapping):
normalized: dict[str, JsonValue] = {}
for key, item in value.items():
if not isinstance(key, str):
raise ManifestError(path, "expected string object keys")
if key != "title":
normalized[key] = _json_value(item, f"{path}.{key}")
return normalized
raise ManifestError(path, "expected a JSON value")
def _schema(value: object, path: str) -> JsonSchema:
normalized = _json_value(value, path)
if not isinstance(normalized, dict):
raise ManifestError(path, "expected a schema object")
return normalized
```
Then implement `manifest_from_openrpc()` using the helpers above:
- require a non-empty string at `$.openrpc`;
- require arrays/objects at `$.methods`, `$.components`, `$.components.schemas`, and `$.components.errors`;
- require each method name to contain at least one `.` and have no empty segments;
- derive `namespace = segments[:-1]` and `action = segments[-1]`;
- require each parameter's `name`, boolean `required`, and object `schema`;
- retain only normalized `schema` under `result` and each error entry;
- sort operations by `method` and component items by key;
- construct the exact `ContractManifest` shape in the approved spec.
Use a short comment over `_json_value`: generated titles are removed recursively, while every other schema keyword and value is intentionally opaque.
- [x] **Step 6: Run focused tests and static checks**
Run:
```powershell
.venv\Scripts\python.exe -m pytest tests\wf_contract_manifest\test_normalize.py -n 0 --basetemp '.pytest-tmp\manifest-task1' -q
.venv\Scripts\ruff.exe check src\wf_contract_manifest tests\wf_contract_manifest
.venv\Scripts\basedpyright.exe --level error src\wf_contract_manifest tests\wf_contract_manifest
```
Expected: all normalization tests pass; Ruff and basedpyright report no errors.
- [x] **Step 7: Commit Task 1**
```powershell
git add src\wf_contract_manifest tests\wf_contract_manifest
git commit -m "feat: normalize workflow OpenRPC manifests"
```
---
### Task 2: Fail-Closed Contract And Reference Validation
**Files:**
- Modify: `src/wf_contract_manifest/normalize.py`
- Modify: `tests/wf_contract_manifest/test_normalize.py`
**Interfaces:**
- Consumes: `ManifestError`, normalized operation/component values from Task 1.
- Produces: the same `manifest_from_openrpc()` interface, now rejecting malformed methods and invalid `$ref` graphs before returning.
- [x] **Step 1: Add failing malformed-envelope tests**
Append parameterized tests to `tests/wf_contract_manifest/test_normalize.py`:
```python
from copy import deepcopy
import pytest
from wf_contract_manifest import ManifestError
@pytest.mark.parametrize(
("mutate", "path", "message"),
[
(
lambda document: document.update({"openrpc": "2.0.0"}),
"$.openrpc",
"unsupported OpenRPC version '2.0.0'; expected '1.2.6'",
),
(lambda document: document.update({"methods": {}}), "$.methods", "expected an array"),
(
lambda document: document["methods"].append(deepcopy(document["methods"][0])),
"$.methods[2].name",
"duplicate method 'workflow.zeta.run'",
),
(
lambda document: document["methods"][0].update({"name": "workflow..run"}),
"$.methods[0].name",
"malformed dotted method name",
),
(
lambda document: document["methods"][0].update({"params": {}}),
"$.methods[0].params",
"expected an array",
),
(
lambda document: document["methods"][0]["params"].append(
{"name": "value", "required": "yes", "schema": {"type": "string"}}
),
"$.methods[0].params[0].required",
"expected a boolean",
),
(
lambda document: document["methods"][0].update({"result": {}}),
"$.methods[0].result.schema",
"expected an object",
),
(
lambda document: document["methods"][0]["result"].update(
{"schema": {"type": "object", "properties": {"ok": {"type": "boolean"}}}}
),
"$.methods[0].result.schema",
"success result must reference a named schema component",
),
],
)
def test_rejects_malformed_openrpc_contracts(mutate, path: str, message: str) -> None:
document = synthetic_openrpc_document()
mutate(document)
with pytest.raises(ManifestError) as exc_info:
manifest_from_openrpc(document)
assert exc_info.value.path == path
assert exc_info.value.message == message
```
If basedpyright rejects untyped lambdas, define a `Protocol` named `DocumentMutation` and annotate the parameter, or replace the table with named mutation functions. Do not silence it with `Any` casts.
- [x] **Step 2: Add failing reference-graph tests**
Add tests for every rejected reference class:
```python
@pytest.mark.parametrize(
("reference", "message"),
[
("https://example.test/schema.json", "external references are not supported"),
("#/definitions/Result", "unsupported local reference namespace"),
("#/components/parameters/Value", "unsupported component reference namespace"),
("#/components/schemas/Missing", "dangling local reference"),
],
)
def test_rejects_unsupported_or_dangling_references(
reference: str,
message: str,
) -> None:
document = synthetic_openrpc_document()
document["components"]["schemas"]["AlphaResult"]["properties"]["linked"] = {
"$ref": reference
}
with pytest.raises(ManifestError) as exc_info:
manifest_from_openrpc(document)
assert exc_info.value.path.endswith(".properties.linked.$ref")
assert exc_info.value.message == message
def test_accepts_nested_schema_and_error_component_references() -> None:
document = synthetic_openrpc_document()
document["components"]["schemas"]["AlphaResult"]["properties"]["linked"] = {
"$ref": "#/components/schemas/ZetaResult"
}
manifest = manifest_from_openrpc(document)
assert manifest["operations"][0]["errors"] == [
{"$ref": "#/components/errors/5000"}
]
```
- [x] **Step 3: Run tests and verify fail-closed cases are red**
Run:
```powershell
.venv\Scripts\python.exe -m pytest tests\wf_contract_manifest\test_normalize.py -n 0 --basetemp '.pytest-tmp\manifest-task2' -q
```
Expected: new duplicate-method, generic-result, and invalid-reference cases fail because Task 1 does not yet reject them.
- [x] **Step 4: Implement strict method/result validation**
In `normalize.py`:
- require `$.openrpc` to equal `"1.2.6"`; a new OpenRPC format version must be reviewed before manifest v1 accepts it;
- add `_boolean(value: object, path: str) -> bool` that rejects non-`bool` values;
- maintain `seen_methods: set[str]` while reading methods and report duplicates at the later method's `.name` path;
- validate dotted names with `parts = method.split(".")` and reject `len(parts) < 2` or any empty part;
- require every success schema to be exactly a local schema reference object at the top level: `set(schema) == {"$ref"}` and `schema["$ref"]` begins with `#/components/schemas/`.
This top-level result rule is intentionally stricter than nested JSON Schema. Every current successful operation has a named result component, which is the stable seam the TypeScript generator will consume.
- [x] **Step 5: Implement one complete reference-graph walk**
Add these internal interfaces:
```python
type ComponentIndex = dict[str, set[str]]
def _walk_references(value: JsonValue, path: str):
if isinstance(value, dict):
for key, child in value.items():
child_path = f"{path}.{key}"
if key == "$ref":
if not isinstance(child, str):
raise ManifestError(child_path, "expected a reference string")
yield child_path, child
else:
yield from _walk_references(child, child_path)
elif isinstance(value, list):
for index, child in enumerate(value):
yield from _walk_references(child, f"{path}[{index}]")
```
Use a docstring explaining that this walker intentionally treats JSON Schema vocabulary as opaque and inspects only `$ref` values.
Build a component index from normalized keys:
```python
component_index: ComponentIndex = {
"schemas": set(manifest["components"]["schemas"]),
"errors": set(manifest["components"]["errors"]),
}
```
For every operation and component value, walk references and validate:
1. references must start with `#/`;
2. split path must be exactly `components/<schemas|errors>/<key>`;
3. JSON Pointer unescaping is not supported in v1, so reject keys containing `~0` or `~1` with `unsupported escaped component reference`;
4. the component key must exist in the indexed namespace.
Run this validation once after the whole manifest is assembled so forward references are valid.
- [x] **Step 6: Run focused tests and static checks**
```powershell
.venv\Scripts\python.exe -m pytest tests\wf_contract_manifest\test_normalize.py -n 0 --basetemp '.pytest-tmp\manifest-task2' -q
.venv\Scripts\ruff.exe check src\wf_contract_manifest tests\wf_contract_manifest
.venv\Scripts\basedpyright.exe --level error src\wf_contract_manifest tests\wf_contract_manifest
```
Expected: all normalization and validation tests pass; static checks are clean.
- [x] **Step 7: Commit Task 2**
```powershell
git add src\wf_contract_manifest\normalize.py tests\wf_contract_manifest\test_normalize.py
git commit -m "feat: validate workflow contract references"
```
---
### Task 3: Real Contract Generation And Canonical I/O
**Files:**
- Create: `src/wf_contract_manifest/generate.py`
- Create: `src/wf_contract_manifest/io.py`
- Create: `tests/wf_contract_manifest/test_generate.py`
- Create: `tests/wf_contract_manifest/test_io.py`
- Modify: `src/wf_contract_manifest/__init__.py`
**Interfaces:**
- Consumes: `manifest_from_openrpc()` from Tasks 1-2, `wf_server.build_local_static_workflow_server`, and `wf_transport_rpc_http.create_rpc_app`.
- Produces: `generate_manifest() -> ContractManifest`, `canonical_manifest_json(manifest) -> str`, `write_manifest(manifest, path) -> Path`, `check_manifest(manifest, path) -> None`, `ManifestDriftError`, and `DEFAULT_MANIFEST_PATH`.
- [x] **Step 1: Write the real-contract integration test**
Create `tests/wf_contract_manifest/test_generate.py`:
```python
from __future__ import annotations
from wf_contract_manifest import generate_manifest
UNION_RESULTS = {
"InspectCapabilityResult",
"PatchDraftResult",
"ValidateDraftResult",
"CompileDraftWorkspaceResult",
"CreateArtifactFromWorkspaceResult",
}
def test_generates_the_complete_real_workflow_contract() -> None:
manifest = generate_manifest()
schemas = manifest["components"]["schemas"]
assert len(manifest["operations"]) == 70
assert len({operation["method"] for operation in manifest["operations"]}) == 70
assert len(schemas) == 126
assert len(manifest["components"]["errors"]) == 1
assert all(set(operation["result"]["schema"]) == {"$ref"} for operation in manifest["operations"])
assert {name for name in UNION_RESULTS if "anyOf" in schemas[name]} == UNION_RESULTS
def test_generated_contract_preserves_security_and_extension_boundaries() -> None:
schemas = generate_manifest()["components"]["schemas"]
auth_result_names = [name for name in schemas if "Auth" in name and name.endswith("Result")]
assert auth_result_names
for name in auth_result_names:
properties = schemas[name].get("properties", {})
assert isinstance(properties, dict)
assert "payload" not in properties
assert schemas["SourceDiagnosisResult"]["additionalProperties"] is True
assert schemas["RegistryEntryPayload"]["additionalProperties"] is True
def test_generated_contract_contains_no_temporary_or_transport_state() -> None:
serialized = str(generate_manifest())
assert "TemporaryDirectory" not in serialized
assert "\\\\Temp\\\\" not in serialized
assert "127.0.0.1" not in serialized
assert '"/rpc"' not in serialized
```
If current auth result components use a narrower naming convention, replace `auth_result_names` with the exact current component names discovered from the real document and pin them explicitly. Do not weaken the assertion to a no-op.
- [x] **Step 2: Write canonical I/O tests**
Create `tests/wf_contract_manifest/test_io.py`:
```python
from __future__ import annotations
from pathlib import Path
import pytest
from wf_contract_manifest import (
ManifestDriftError,
canonical_manifest_json,
check_manifest,
manifest_from_openrpc,
write_manifest,
)
from .fixtures import synthetic_openrpc_document
def _manifest():
return manifest_from_openrpc(synthetic_openrpc_document())
def test_canonical_json_is_stable_utf8_text_with_trailing_newline() -> None:
first = canonical_manifest_json(_manifest())
second = canonical_manifest_json(_manifest())
assert first == second
assert first.endswith("\n")
assert ' "manifest_version": 1' in first
assert "\\u" not in first
def test_write_and_check_round_trip(tmp_path: Path) -> None:
path = tmp_path / "workflow-api.manifest.json"
assert write_manifest(_manifest(), path) == path
check_manifest(_manifest(), path)
assert path.read_bytes() == canonical_manifest_json(_manifest()).encode("utf-8")
def test_check_reports_drift_without_mutating_the_file(tmp_path: Path) -> None:
path = tmp_path / "workflow-api.manifest.json"
path.write_text("stale\n", encoding="utf-8")
before = path.read_bytes()
with pytest.raises(ManifestDriftError, match="python -m wf_contract_manifest write"):
check_manifest(_manifest(), path)
assert path.read_bytes() == before
```
- [x] **Step 3: Run tests and verify generation/I/O modules are missing**
```powershell
.venv\Scripts\python.exe -m pytest tests\wf_contract_manifest\test_generate.py tests\wf_contract_manifest\test_io.py -n 0 --basetemp '.pytest-tmp\manifest-task3' -q
```
Expected: collection fails because `generate_manifest`, `ManifestDriftError`, and the I/O helpers are not exported.
- [x] **Step 4: Implement real in-process generation**
Create `src/wf_contract_manifest/generate.py`:
```python
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)
```
Do not start Uvicorn, bind a socket, read `.env`, or construct a remote client. The composed in-process app is the authoritative transport registration surface.
- [x] **Step 5: Implement byte-canonical I/O**
Create `src/wf_contract_manifest/io.py`:
```python
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`"
)
```
Byte comparison is intentional: it catches semantic contract drift and non-canonical manual edits with the same deterministic remediation.
- [x] **Step 6: Export the generation and I/O interfaces**
Update `src/wf_contract_manifest/__init__.py` so `__all__` also contains:
```python
from .generate import generate_manifest
from .io import (
DEFAULT_MANIFEST_PATH,
ManifestDriftError,
canonical_manifest_json,
check_manifest,
write_manifest,
)
```
- [x] **Step 7: Run focused tests and static checks**
```powershell
.venv\Scripts\python.exe -m pytest tests\wf_contract_manifest -n 0 --basetemp '.pytest-tmp\manifest-task3' -q
.venv\Scripts\ruff.exe check src\wf_contract_manifest tests\wf_contract_manifest
.venv\Scripts\basedpyright.exe --level error src\wf_contract_manifest tests\wf_contract_manifest
```
Expected: synthetic and real-contract tests pass; static checks are clean.
- [x] **Step 8: Commit Task 3**
```powershell
git add src\wf_contract_manifest tests\wf_contract_manifest
git commit -m "feat: generate canonical workflow contract"
```
---
### Task 4: Module CLI And Checked Manifest Artifact
**Files:**
- Create: `src/wf_contract_manifest/__main__.py`
- Create: `tests/wf_contract_manifest/test_cli.py`
- Create by command: `contracts/workflow-api.manifest.json`
**Interfaces:**
- Consumes: `generate_manifest`, `write_manifest`, `check_manifest`, `DEFAULT_MANIFEST_PATH`, and `ManifestDriftError` from Task 3.
- Produces: `main(argv: Sequence[str] | None = None) -> int` and the supported commands `.venv\Scripts\python.exe -m wf_contract_manifest write|check`.
- [x] **Step 1: Write CLI behavior tests**
Create `tests/wf_contract_manifest/test_cli.py`:
```python
from __future__ import annotations
from pathlib import Path
import pytest
from wf_contract_manifest import ContractManifest, ManifestDriftError, manifest_from_openrpc
from wf_contract_manifest.__main__ import main
from .fixtures import synthetic_openrpc_document
def _manifest() -> ContractManifest:
return manifest_from_openrpc(synthetic_openrpc_document())
def test_write_generates_once_and_writes_requested_contract(monkeypatch, tmp_path: Path) -> None:
manifest = _manifest()
calls: list[tuple[object, Path]] = []
monkeypatch.setattr("wf_contract_manifest.__main__.generate_manifest", lambda: manifest)
monkeypatch.setattr(
"wf_contract_manifest.__main__.write_manifest",
lambda value, path: calls.append((value, path)) or path,
)
monkeypatch.setattr("wf_contract_manifest.__main__.DEFAULT_MANIFEST_PATH", tmp_path / "manifest.json")
assert main(["write"]) == 0
assert calls == [(manifest, tmp_path / "manifest.json")]
def test_check_returns_nonzero_and_prints_drift_guidance(monkeypatch, capsys) -> None:
monkeypatch.setattr("wf_contract_manifest.__main__.generate_manifest", _manifest)
def fail_check(_manifest, _path) -> None:
raise ManifestDriftError("stale; run `python -m wf_contract_manifest write`")
monkeypatch.setattr("wf_contract_manifest.__main__.check_manifest", fail_check)
assert main(["check"]) == 1
assert "python -m wf_contract_manifest write" in capsys.readouterr().err
def test_rejects_unknown_command() -> None:
with pytest.raises(SystemExit) as exc_info:
main(["unknown"])
assert exc_info.value.code == 2
```
Use explicit pytest fixture types already established in the repository if basedpyright requires them; do not replace behavior assertions with subprocess-only smoke tests.
- [x] **Step 2: Run the CLI test and confirm it is red**
```powershell
.venv\Scripts\python.exe -m pytest tests\wf_contract_manifest\test_cli.py -n 0 --basetemp '.pytest-tmp\manifest-task4' -q
```
Expected: collection fails because `wf_contract_manifest.__main__` does not exist.
- [x] **Step 3: Implement the module CLI**
Create `src/wf_contract_manifest/__main__.py`:
```python
from __future__ import annotations
import argparse
import sys
from collections.abc import Sequence
from .generate import generate_manifest
from .io import DEFAULT_MANIFEST_PATH, ManifestDriftError, check_manifest, write_manifest
from .model import ManifestError
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Manage the checked workflow API contract manifest.")
parser.add_argument("command", choices=("write", "check"))
return parser
def main(argv: Sequence[str] | None = None) -> int:
args = _parser().parse_args(argv)
try:
manifest = generate_manifest()
if args.command == "write":
path = write_manifest(manifest, DEFAULT_MANIFEST_PATH)
print(f"wrote {path}")
else:
check_manifest(manifest, DEFAULT_MANIFEST_PATH)
print(f"checked {DEFAULT_MANIFEST_PATH}")
except (ManifestError, ManifestDriftError, ValueError) as error:
print(str(error), file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
```
Do not add a `[project.scripts]` entry. The approved surface is the module command.
- [x] **Step 4: Run the CLI test and static checks**
```powershell
.venv\Scripts\python.exe -m pytest tests\wf_contract_manifest\test_cli.py -n 0 --basetemp '.pytest-tmp\manifest-task4' -q
.venv\Scripts\ruff.exe check src\wf_contract_manifest tests\wf_contract_manifest
.venv\Scripts\basedpyright.exe --level error src\wf_contract_manifest tests\wf_contract_manifest
```
Expected: CLI tests pass and static checks are clean.
- [x] **Step 5: Generate and independently check the committed artifact**
```powershell
.venv\Scripts\python.exe -m wf_contract_manifest write
.venv\Scripts\python.exe -m wf_contract_manifest check
```
Expected: the first command reports `contracts\workflow-api.manifest.json` written; the second reports the same path checked and exits zero.
Inspect the artifact with bounded assertions rather than manually reading thousands of lines:
```powershell
@'
import json
from pathlib import Path
path = Path("contracts/workflow-api.manifest.json")
manifest = json.loads(path.read_text(encoding="utf-8"))
print(len(manifest["operations"]))
print(len(manifest["components"]["schemas"]))
print(len(manifest["components"]["errors"]))
print(manifest["operations"][0]["method"])
print(manifest["operations"][-1]["method"])
'@ | .venv\Scripts\python.exe -
```
Expected: `70`, `126`, `1`, followed by the lexically first and last method names.
- [x] **Step 6: Commit Task 4**
```powershell
git add src\wf_contract_manifest\__main__.py tests\wf_contract_manifest\test_cli.py contracts\workflow-api.manifest.json
git commit -m "feat: check in workflow contract manifest"
```
---
### Task 5: Drift Gate, Documentation, And Final Review
**Files:**
- Create: `tests/wf_contract_manifest/test_committed_manifest.py`
- Modify: `ISSUES.md`
- Modify: `docs/project_map.md`
- Modify: `docs/current_roadmap.md`
- Move after implementation: `docs/superpowers/plans/2026-08-01-workflow-contract-manifest.md` to `docs/historical/superpowers/plans/2026-08-01-workflow-contract-manifest.md`
**Interfaces:**
- Consumes: the real generator, canonical checker, and checked artifact from Tasks 3-4.
- Produces: a deterministic pytest drift gate and current documentation pointing to the manifest seam and the next TypeScript generation slice.
- [x] **Step 1: Write the committed-manifest drift test**
Create `tests/wf_contract_manifest/test_committed_manifest.py`:
```python
from wf_contract_manifest import DEFAULT_MANIFEST_PATH, check_manifest, generate_manifest
def test_committed_manifest_matches_the_python_workflow_contract() -> None:
check_manifest(generate_manifest(), DEFAULT_MANIFEST_PATH)
```
- [x] **Step 2: Prove the drift gate fails without mutating the artifact**
Use a temporary backup and restore in one PowerShell `try/finally` block:
```powershell
$path = Resolve-Path 'contracts\workflow-api.manifest.json'
$before = [System.IO.File]::ReadAllBytes($path)
try {
Add-Content -LiteralPath $path -Value ' '
.venv\Scripts\python.exe -m pytest tests\wf_contract_manifest\test_committed_manifest.py -n 0 --basetemp '.pytest-tmp\manifest-task5-red' -q
if ($LASTEXITCODE -eq 0) { throw 'drift test unexpectedly passed' }
} finally {
[System.IO.File]::WriteAllBytes($path, $before)
}
```
Expected: pytest fails with `ManifestDriftError` and guidance to run `python -m wf_contract_manifest write`; the `finally` block restores the exact original bytes.
- [x] **Step 3: Run the restored drift gate**
```powershell
.venv\Scripts\python.exe -m pytest tests\wf_contract_manifest\test_committed_manifest.py -n 0 --basetemp '.pytest-tmp\manifest-task5-green' -q
```
Expected: one test passes.
- [x] **Step 4: Update current documentation**
Read `docs/AGENTS.md` before editing these files.
In `ISSUES.md`, update the TypeScript JSON-RPC coverage section to record:
- all 70 Python methods now have named OpenRPC success schemas;
- `contracts/workflow-api.manifest.json` is the checked transport-neutral inventory;
- `python -m wf_contract_manifest check` is the drift gate;
- browser authorization, operation metadata, and the 12 current Effect RPC implementations remain authored boundaries; and
- the next slice is generated TypeScript operation names/raw types plus a fail-closed representative JSON Schema-to-Effect translator.
In the package table in `docs/project_map.md`, add:
```markdown
| `wf_contract_manifest` | Tooling that normalizes the composed workflow OpenRPC document into the checked transport-neutral contract manifest and detects drift. | Contract generation, tests, and future TypeScript generators. |
```
Under `Important Entry Points`, add:
```markdown
- `python -m wf_contract_manifest write|check`: regenerate or verify
`contracts/workflow-api.manifest.json` from the real composed workflow server.
```
In `docs/current_roadmap.md` under `Recently Completed Platform Milestones`, add a concise completed bullet linking the approved design spec and checked artifact, then state that generated TypeScript inventory/types and representative Effect translation are the next contract-parity slice. Do not claim TypeScript parity is complete.
- [x] **Step 5: Run the complete scoped verification gate**
```powershell
New-Item -ItemType Directory -Force -Path '.pytest-tmp' | Out-Null
.venv\Scripts\python.exe -m pytest tests\wf_contract_manifest tests\wf_transport_rpc_http\test_openrpc_contract.py -n 0 --basetemp '.pytest-tmp\manifest-final' -q
.venv\Scripts\python.exe -m wf_contract_manifest check
.venv\Scripts\ruff.exe check src\wf_contract_manifest tests\wf_contract_manifest
.venv\Scripts\basedpyright.exe --level error src\wf_contract_manifest tests\wf_contract_manifest
git diff --check
git status --short
```
Expected:
- all manifest and existing OpenRPC contract tests pass;
- module `check` exits zero without modifying the artifact;
- Ruff, basedpyright, and whitespace checks are clean;
- status contains only files intentionally changed by this plan.
- [x] **Step 6: Run independent two-axis review and fix valid findings**
Dispatch a fresh reviewer that did not implement the slice. Give it:
- the approved design spec;
- this implementation plan;
- the commit range beginning immediately before Task 1; and
- explicit instructions to review both repository standards and spec compliance, prioritizing manifest information loss, false authorization coupling, non-determinism, reference validation gaps, and weak drift tests.
Fix every valid Critical or Important finding and add a regression test for behavioral fixes. Re-run Step 5 after fixes. Record Minor deferrals with rationale in the final report rather than silently ignoring them.
- [x] **Step 7: Archive the completed plan and commit documentation**
Only after all code, tests, review fixes, and verification are complete:
```powershell
Move-Item -LiteralPath 'docs\superpowers\plans\2026-08-01-workflow-contract-manifest.md' -Destination 'docs\historical\superpowers\plans\2026-08-01-workflow-contract-manifest.md'
git add ISSUES.md docs\project_map.md docs\current_roadmap.md docs\superpowers\plans\2026-08-01-workflow-contract-manifest.md docs\historical\superpowers\plans\2026-08-01-workflow-contract-manifest.md tests\wf_contract_manifest\test_committed_manifest.py
git commit -m "docs: record workflow contract manifest"
```
- [x] **Step 8: Remove only the plan-owned temporary pytest directory**
```powershell
$temp = (Resolve-Path '.pytest-tmp').Path
$root = (Resolve-Path '.').Path
if (-not $temp.StartsWith($root, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "Refusing to remove pytest temp outside workspace: $temp"
}
Remove-Item -LiteralPath $temp -Recurse -Force
git status --short
```
Expected: the verified workspace-local temporary directory is removed; the worktree is clean after the final commit.
## Completion Criteria
- `contracts/workflow-api.manifest.json` canonically represents all 70 current OpenRPC operations, 126 schema components, and one declared error component.
- Synthetic tests prove ordering, parameter-order preservation, optional/null distinction, title-only stripping, unknown-keyword preservation, all `additionalProperties` states, and empty-schema preservation.
- Invalid envelopes, duplicate/malformed methods, generic success results, external/dangling/unsupported references, and malformed reference values fail with path-bearing `ManifestError` messages.
- Real generation uses the composed in-process server and leaks no local store or transport state.
- `write` changes only the artifact; `check` detects byte drift without mutation.
- The checked-file pytest gate fails on Python/OpenRPC drift.
- No TypeScript runtime, Effect schema, browser allowlist, or presentation behavior changes.
- Current docs identify the manifest as transport inventory, not authorization, and name the next parity slice honestly.