docs: make lda report plan authoring-first

This commit is contained in:
lda
2026-07-01 06:44:48 +07:00 Verified
parent 51b77b0dd6
commit 9e313cd5d0
@@ -4,9 +4,9 @@
**Goal:** Add a deterministic `examples/lda_report_workflow/` case study that exercises document reading, report synthesis, a typed human approval interrupt, issue creation, resume, trace, and final output. **Goal:** Add a deterministic `examples/lda_report_workflow/` case study that exercises document reading, report synthesis, a typed human approval interrupt, issue creation, resume, trace, and final output.
**Architecture:** Implement three trusted Python sources in one example package: `local.lda_docs`, `local.lda_report`, and `local.issue_board`. Ship a raw workflow plan that uses the new self-describing interrupt contract, plus tests that run artifact, deployment, interrupt inspection, resume, issue creation, and final output through the product API. **Architecture:** Implement three trusted Python sources in one example package: `local.lda_docs`, `local.lda_report`, and `local.issue_board`. Author the graph with `wf_authoring.WorkflowBuilder`, generate a committed raw workflow plan from the compiled `Workflow`, and test artifact, deployment, interrupt inspection, resume, issue creation, and final output through the product API.
**Tech Stack:** Python 3.14, Pydantic models, static Python workflow sources, raw workflow plans, `wf_config`, `wf_server`, pytest. **Tech Stack:** Python 3.14, Pydantic models, `wf_authoring.WorkflowBuilder`, static Python workflow sources, generated raw workflow plans, `wf_config`, `wf_server`, pytest.
--- ---
@@ -36,9 +36,10 @@ examples/lda_report_workflow/
document_source.py document_source.py
report_source.py report_source.py
issue_board_source.py issue_board_source.py
build_workflow.py
run-input.json run-input.json
wf.config.json wf.config.json
workflow.plan.json workflow.plan.json # generated by build_workflow.py and committed
``` ```
Create tests: Create tests:
@@ -98,7 +99,8 @@ The interrupt:
- request payload contains rendered report markdown and proposed issues - request payload contains rendered report markdown and proposed issues
- resume payload contains `approved`, `selected_issue_ids`, and optional - resume payload contains `approved`, `selected_issue_ids`, and optional
`comment` `comment`
- request/resume schemas are explicit JSON Schema objects in the raw plan - request/resume schemas are explicit JSON Schema objects authored in
`build_workflow.py` and generated into the raw plan
--- ---
@@ -981,11 +983,12 @@ git commit -m "feat: add local issue board source example"
--- ---
### Task 5: Add Config, Run Input, And Raw Workflow Plan ### Task 5: Add Config, Run Input, And Workflow Builder
**Files:** **Files:**
- Create: `examples/lda_report_workflow/wf.config.json` - Create: `examples/lda_report_workflow/wf.config.json`
- Create: `examples/lda_report_workflow/run-input.json` - Create: `examples/lda_report_workflow/run-input.json`
- Create: `examples/lda_report_workflow/build_workflow.py`
- Create: `examples/lda_report_workflow/workflow.plan.json` - Create: `examples/lda_report_workflow/workflow.plan.json`
- Modify: `tests/examples/test_lda_report_workflow_example.py` - Modify: `tests/examples/test_lda_report_workflow_example.py`
@@ -1052,12 +1055,17 @@ Create `examples/lda_report_workflow/run-input.json`:
- [ ] **Step 3: Add config loading test** - [ ] **Step 3: Add config loading test**
Append to `tests/examples/test_lda_report_workflow_example.py`: Add these imports to the top import block in
`tests/examples/test_lda_report_workflow_example.py`:
```python ```python
from wf_config import load_workflow_config from wf_config import load_workflow_config
from wf_server.config import build_workflow_server_from_workflow_config from wf_server.config import build_workflow_server_from_workflow_config
```
Append this test:
```python
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_lda_report_workflow_config_loads_sources(tmp_path: Path) -> None: async def test_lda_report_workflow_config_loads_sources(tmp_path: Path) -> None:
@@ -1082,203 +1090,276 @@ uv run pytest tests/examples/test_lda_report_workflow_example.py::test_lda_repor
Expected: pass. Expected: pass.
- [ ] **Step 5: Create raw workflow plan** - [ ] **Step 5: Create workflow builder**
Create `examples/lda_report_workflow/workflow.plan.json` with this content: Create `examples/lda_report_workflow/build_workflow.py`:
```json ```python
{ from __future__ import annotations
"name": "lda_report_case_study",
"input_schema": { import json
"type": "object", from pathlib import Path
"properties": { from typing import Any
"selected_documents": {
"type": "array", from wf_api.models import RawWorkflowPlan
"items": {"type": "string"} from wf_authoring import WorkflowBuilder
}, from wf_core import Workflow
"board_path": {"type": "string"}
}, HERE = Path(__file__).resolve().parent
"required": ["selected_documents", "board_path"]
}, WORKFLOW_OUTPUT = [
"state_schema": {
"type": "object",
"properties": {
"documents": {"type": "array"},
"analysis": {"type": "array"},
"report": {"type": "object"},
"report_markdown": {"type": "string"},
"proposed_issues": {"type": "array"},
"selected_issue_ids": {"type": "array"},
"approval_comment": {"type": "string"},
"approved": {"type": "boolean"},
"created_issues": {"type": "array"},
"final_markdown": {"type": "string"}
}
},
"output_schema": {
"type": "object",
"properties": {
"approved": {"type": "boolean"},
"markdown": {"type": "string"},
"created_issues": {"type": "array"},
"selected_issue_ids": {"type": "array"}
}
},
"output": [
{"path": "state.approved", "target": "approved"}, {"path": "state.approved", "target": "approved"},
{"path": "state.final_markdown", "target": "markdown"}, {"path": "state.final_markdown", "target": "markdown"},
{"path": "state.created_issues", "target": "created_issues"}, {"path": "state.created_issues", "target": "created_issues"},
{"path": "state.selected_issue_ids", "target": "selected_issue_ids"} {"path": "state.selected_issue_ids", "target": "selected_issue_ids"},
], ]
"outcomes": ["completed", "cancelled"],
"start": "read_docs",
"nodes": [ def build_workflow() -> Workflow:
{ """Build the demo workflow with the public authoring API.
"id": "read_docs",
"type": "node", `WorkflowBuilder` does not yet expose a workflow-output setter, so this
"node": "local.lda_docs.read_documents", module adds the final output projection in `_with_workflow_output()` after
"input": [ compiling the graph. Keep that seam small and validated.
{"path": "input.selected_documents", "target": "names"} """
], builder = WorkflowBuilder(
"output": [ name="lda_report_case_study",
{"source": "documents", "target": "state.documents"} input_schema={
] "type": "object",
}, "properties": {
{ "selected_documents": {
"id": "analyze", "type": "array",
"type": "node", "items": {"type": "string"},
"node": "local.lda_report.analyze_documents", },
"input": [ "board_path": {"type": "string"},
{"path": "state.documents", "target": "documents"} },
], "required": ["selected_documents", "board_path"],
"output": [
{"source": "analysis", "target": "state.analysis"}
]
},
{
"id": "build_report",
"type": "node",
"node": "local.lda_report.build_report",
"input": [
{"path": "state.analysis", "target": "analysis"}
],
"output": [
{"source": "report", "target": "state.report"},
{"source": "markdown", "target": "state.report_markdown"}
]
},
{
"id": "draft_issues",
"type": "node",
"node": "local.lda_report.create_issue_drafts",
"input": [
{"path": "state.report", "target": "report"}
],
"output": [
{"source": "issues", "target": "state.proposed_issues"}
]
},
{
"id": "review_issues",
"type": "interrupt",
"kind": "issue_review",
"request": [
{"path": "state.report_markdown", "target": "report_markdown"},
{"path": "state.proposed_issues", "target": "proposed_issues"}
],
"resume": [
{"source": "approved", "target": "state.approved"},
{"source": "selected_issue_ids", "target": "state.selected_issue_ids"},
{"source": "comment", "target": "state.approval_comment"}
],
"outcomes": ["submitted", "cancelled"],
"request_schema": {
"type": "object",
"properties": {
"report_markdown": {"type": "string"},
"proposed_issues": {"type": "array"}
}, },
"required": ["report_markdown", "proposed_issues"], state_schema={
"additionalProperties": false "type": "object",
}, "properties": {
"resume_schema": { "documents": {"type": "array"},
"type": "object", "analysis": {"type": "array"},
"properties": { "report": {"type": "object"},
"approved": {"type": "boolean"}, "report_markdown": {"type": "string"},
"selected_issue_ids": { "proposed_issues": {"type": "array"},
"type": "array", "selected_issue_ids": {"type": "array"},
"items": {"type": "string"} "approval_comment": {"type": "string"},
}, "approved": {"type": "boolean"},
"comment": {"type": "string"} "created_issues": {"type": "array"},
"final_markdown": {"type": "string"},
},
}, },
"required": ["approved", "selected_issue_ids"], output_schema={
"additionalProperties": false "type": "object",
} "properties": {
}, "approved": {"type": "boolean"},
{ "markdown": {"type": "string"},
"id": "create_issues", "created_issues": {"type": "array"},
"type": "node", "selected_issue_ids": {"type": "array"},
"node": "local.issue_board.create_issues", },
"input": [ },
{"path": "state.proposed_issues", "target": "issues"}, outcomes=["completed", "cancelled"],
{"path": "state.selected_issue_ids", "target": "selected_issue_ids"}, )
{"path": "input.board_path", "target": "board_path"}
], read_docs = builder.use_ref(
"output": [ "local.lda_docs.read_documents",
{"source": "created_issues", "target": "state.created_issues"} id="read_docs",
] input=[{"path": "input.selected_documents", "target": "names"}],
}, output=[{"source": "documents", "target": "state.documents"}],
{ )
"id": "finalise", analyze = builder.use_ref(
"type": "node", "local.lda_report.analyze_documents",
"node": "local.lda_report.finalise_report", id="analyze",
"input": [ input=[{"path": "state.documents", "target": "documents"}],
{"path": "state.report", "target": "report"}, output=[{"source": "analysis", "target": "state.analysis"}],
{"path": "state.created_issues", "target": "created_issues"}, )
{"path": "state.approved", "target": "approved"}, build_report = builder.use_ref(
{"path": "state.selected_issue_ids", "target": "selected_issue_ids"}, "local.lda_report.build_report",
{"path": "state.approval_comment", "target": "comment"} id="build_report",
], input=[{"path": "state.analysis", "target": "analysis"}],
"output": [ output=[
{"source": "markdown", "target": "state.final_markdown"} {"source": "report", "target": "state.report"},
] {"source": "markdown", "target": "state.report_markdown"},
}, ],
{ )
"id": "revision_requested", draft_issues = builder.use_ref(
"type": "node", "local.lda_report.create_issue_drafts",
"node": "local.lda_report.record_revision_request", id="draft_issues",
"input": [ input=[{"path": "state.report", "target": "report"}],
{"path": "state.approval_comment", "target": "comment"} output=[{"source": "issues", "target": "state.proposed_issues"}],
], )
"output": [ review_issues = builder.interrupt(
{"source": "approved", "target": "state.approved"}, id="review_issues",
{"source": "markdown", "target": "state.final_markdown"}, kind="issue_review",
{"source": "created_issues", "target": "state.created_issues"}, request=[
{"source": "selected_issue_ids", "target": "state.selected_issue_ids"} {"path": "state.report_markdown", "target": "report_markdown"},
] {"path": "state.proposed_issues", "target": "proposed_issues"},
}, ],
{"id": "end_completed", "type": "end", "outcome": "completed"}, resume=[
{"id": "end_cancelled", "type": "end", "outcome": "cancelled"} {"source": "approved", "target": "state.approved"},
], {"source": "selected_issue_ids", "target": "state.selected_issue_ids"},
"edges": [ {"source": "comment", "target": "state.approval_comment"},
{"from": "read_docs", "outcome": "ok", "to": "analyze"}, ],
{"from": "analyze", "outcome": "ok", "to": "build_report"}, outcomes=["submitted", "cancelled"],
{"from": "build_report", "outcome": "ok", "to": "draft_issues"}, request_schema={
{"from": "draft_issues", "outcome": "ok", "to": "review_issues"}, "type": "object",
{"from": "review_issues", "outcome": "submitted", "to": "create_issues"}, "properties": {
{"from": "review_issues", "outcome": "cancelled", "to": "revision_requested"}, "report_markdown": {"type": "string"},
{"from": "create_issues", "outcome": "ok", "to": "finalise"}, "proposed_issues": {"type": "array"},
{"from": "finalise", "outcome": "ok", "to": "end_completed"}, },
{"from": "revision_requested", "outcome": "ok", "to": "end_cancelled"} "required": ["report_markdown", "proposed_issues"],
] "additionalProperties": False,
} },
resume_schema={
"type": "object",
"properties": {
"approved": {"type": "boolean"},
"selected_issue_ids": {
"type": "array",
"items": {"type": "string"},
},
"comment": {"type": "string"},
},
"required": ["approved", "selected_issue_ids"],
"additionalProperties": False,
},
)
create_issues = builder.use_ref(
"local.issue_board.create_issues",
id="create_issues",
input=[
{"path": "state.proposed_issues", "target": "issues"},
{"path": "state.selected_issue_ids", "target": "selected_issue_ids"},
{"path": "input.board_path", "target": "board_path"},
],
output=[{"source": "created_issues", "target": "state.created_issues"}],
)
finalise = builder.use_ref(
"local.lda_report.finalise_report",
id="finalise",
input=[
{"path": "state.report", "target": "report"},
{"path": "state.created_issues", "target": "created_issues"},
{"path": "state.approved", "target": "approved"},
{"path": "state.selected_issue_ids", "target": "selected_issue_ids"},
{"path": "state.approval_comment", "target": "comment"},
],
output=[{"source": "markdown", "target": "state.final_markdown"}],
)
revision_requested = builder.use_ref(
"local.lda_report.record_revision_request",
id="revision_requested",
input=[{"path": "state.approval_comment", "target": "comment"}],
output=[
{"source": "approved", "target": "state.approved"},
{"source": "markdown", "target": "state.final_markdown"},
{"source": "created_issues", "target": "state.created_issues"},
{"source": "selected_issue_ids", "target": "state.selected_issue_ids"},
],
)
end_completed = builder.end("completed", id="end_completed")
end_cancelled = builder.end("cancelled", id="end_cancelled")
builder.set_entry_point(read_docs)
builder.connect(read_docs, "ok", analyze)
builder.connect(analyze, "ok", build_report)
builder.connect(build_report, "ok", draft_issues)
builder.branch(
review_issues,
{
"submitted": create_issues,
"cancelled": revision_requested,
},
)
builder.connect(create_issues, "ok", finalise)
builder.connect(finalise, "ok", end_completed)
builder.connect(revision_requested, "ok", end_cancelled)
return _with_workflow_output(builder.compile())
def _with_workflow_output(workflow: Workflow) -> Workflow:
payload = workflow.model_dump(mode="json", by_alias=True)
payload["output"] = WORKFLOW_OUTPUT
return Workflow.model_validate(payload)
def workflow_plan_payload() -> dict[str, Any]:
payload = build_workflow().model_dump(mode="json", by_alias=True)
payload.pop("node_defs", None)
RawWorkflowPlan.model_validate(payload)
return payload
def write_plan(path: Path = HERE / "workflow.plan.json") -> None:
payload = workflow_plan_payload()
path.write_text(
json.dumps(payload, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
if __name__ == "__main__":
write_plan()
``` ```
- [ ] **Step 6: Commit config and plan** - [ ] **Step 6: Generate raw workflow plan**
Run:
```powershell ```powershell
git add examples/lda_report_workflow/wf.config.json examples/lda_report_workflow/run-input.json examples/lda_report_workflow/workflow.plan.json tests/examples/test_lda_report_workflow_example.py uv run python examples/lda_report_workflow/build_workflow.py
git commit -m "feat: add lda report workflow plan" ```
Expected: `examples/lda_report_workflow/workflow.plan.json` is written.
- [ ] **Step 7: Add builder/generator regression test**
Add these imports to the top import block:
```python
import json
from examples.lda_report_workflow.build_workflow import (
build_workflow,
workflow_plan_payload,
)
from wf_api.models import RawWorkflowPlan
```
Append this test:
```python
def test_lda_report_workflow_builder_generates_committed_raw_plan() -> None:
workflow = build_workflow()
payload = workflow_plan_payload()
committed = json.loads(
(EXAMPLE_DIR / "workflow.plan.json").read_text(encoding="utf-8")
)
validated = RawWorkflowPlan.model_validate(payload)
assert workflow.name == "lda_report_case_study"
assert validated.name == "lda_report_case_study"
assert any(node.id == "review_issues" for node in validated.nodes)
assert payload == committed
```
- [ ] **Step 8: Run builder and config tests**
Run:
```powershell
uv run pytest tests/examples/test_lda_report_workflow_example.py::test_lda_report_workflow_config_loads_sources tests/examples/test_lda_report_workflow_example.py::test_lda_report_workflow_builder_generates_committed_raw_plan -q -n0
```
Expected: pass.
- [ ] **Step 9: Commit config, builder, and plan**
```powershell
git add examples/lda_report_workflow/wf.config.json examples/lda_report_workflow/run-input.json examples/lda_report_workflow/build_workflow.py examples/lda_report_workflow/workflow.plan.json tests/examples/test_lda_report_workflow_example.py
git commit -m "feat: add lda report workflow builder"
``` ```
--- ---
@@ -1290,12 +1371,10 @@ git commit -m "feat: add lda report workflow plan"
- [ ] **Step 1: Add full lifecycle test** - [ ] **Step 1: Add full lifecycle test**
Append to `tests/examples/test_lda_report_workflow_example.py`: Append to `tests/examples/test_lda_report_workflow_example.py`. Do not add a
second `import json`; Task 5 already added it to the top import block.
```python ```python
import json
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_lda_report_workflow_artifact_interrupt_resume_path(tmp_path: Path) -> None: async def test_lda_report_workflow_artifact_interrupt_resume_path(tmp_path: Path) -> None:
config = load_workflow_config(EXAMPLE_DIR / "wf.config.json") config = load_workflow_config(EXAMPLE_DIR / "wf.config.json")
@@ -1486,6 +1565,15 @@ It does not call Google Drive, email, GitHub, or an LLM.
proposed issue drafts, and finalises the report. proposed issue drafts, and finalises the report.
- `local.issue_board`: writes selected issues to a local JSON file. - `local.issue_board`: writes selected issues to a local JSON file.
## Workflow Definition
`workflow.plan.json` is generated from `build_workflow.py`. After changing the
graph, regenerate and review the raw plan:
```powershell
uv run python examples/lda_report_workflow/build_workflow.py
```
## Product Path ## Product Path
From the repository root: From the repository root:
@@ -1579,14 +1667,17 @@ If either `docs/add/...` file was not changed, omit it from `git add`.
Run: Run:
```powershell ```powershell
uv run python examples/lda_report_workflow/build_workflow.py
git diff --exit-code examples/lda_report_workflow/workflow.plan.json
uv run pytest tests/examples/test_lda_report_workflow_example.py tests/wf_sources_python/test_loader.py -q -n0 uv run pytest tests/examples/test_lda_report_workflow_example.py tests/wf_sources_python/test_loader.py -q -n0
uv run ruff check examples/lda_report_workflow tests/examples/test_lda_report_workflow_example.py uv run ruff check examples/lda_report_workflow tests/examples/test_lda_report_workflow_example.py
uv run ruff format --check examples/lda_report_workflow tests/examples/test_lda_report_workflow_example.py uv run ruff format --check examples/lda_report_workflow tests/examples/test_lda_report_workflow_example.py
uv run basedpyright --level error examples/lda_report_workflow tests/examples/test_lda_report_workflow_example.py uv run basedpyright --level error examples/lda_report_workflow tests/examples/test_lda_report_workflow_example.py
``` ```
Expected: all pass. If `basedpyright` reports import-package issues for the Expected: generation exits 0, `git diff --exit-code` reports no generated-plan
example package, add `examples/lda_report_workflow/__init__.py` and rerun. diff, and all checks pass. If `basedpyright` reports import-package issues for
the example package, add `examples/lda_report_workflow/__init__.py` and rerun.
- [ ] **Step 2: Smoke the CLI path** - [ ] **Step 2: Smoke the CLI path**
@@ -1638,7 +1729,7 @@ Spec coverage:
- deterministic documents: Task 1; - deterministic documents: Task 1;
- first-party document/report/issue-board sources: Tasks 2-4; - first-party document/report/issue-board sources: Tasks 2-4;
- raw plan and config: Task 5; - authoring builder, generated raw plan, and config: Task 5;
- typed issue-review interrupt and resume: Task 6; - typed issue-review interrupt and resume: Task 6;
- product runbook and roadmap: Task 7; - product runbook and roadmap: Task 7;
- final verification/archive: Task 8. - final verification/archive: Task 8.