docs(thesis): resolve review findings and verify examples

This commit is contained in:
lda
2026-09-14 13:40:11 +07:00 Verified
parent 5495dc5656
commit 396caa63d6
8 changed files with 273 additions and 45 deletions
+20
View File
@@ -0,0 +1,20 @@
# Comparison screenshots
Author-provided captures used in the thesis's related-systems discussion.
These are exact pixel crops; no interface text or results were redrawn.
The originals remain in the author's local `random shit/` folder and are
not needed to build the thesis.
| Asset | Original | Crop (x, y, width, height), pixels |
| --- | --- | --- |
| `n8n-merge-append.png` | `a9fb75.png` | 475, 100, 1125, 630 |
| `zapier-path-condition.png` | `filter.png` | 974, 111, 657, 762 |
Coordinates use the top-left corner of the 1644 × 985 originals. The n8n
crop retains configuration and output. The Zapier crop retains the condition
and test result while excluding the account header and unrelated graph nodes.
The screenshots illustrate configuration and sample results. Execution
ordering and synchronization claims use the documentation cited in the
manuscript. The Edit Fields capture (`fca1d3.png`) was omitted because its
field-assignment example adds a separate topic to this comparison.
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+9 -6
View File
@@ -6,13 +6,16 @@
\usepackage[dvipsnames]{xcolor} \usepackage[dvipsnames]{xcolor}
\usepackage{fancyhdr} \usepackage{fancyhdr}
\usepackage{float} \usepackage{float}
% Let screenshots share a page with their explanation before using float pages.
\floatplacement{figure}{htbp}
\renewcommand{\topfraction}{0.85}
\renewcommand{\textfraction}{0.1}
\renewcommand{\floatpagefraction}{0.75}
\pagestyle{fancy} \pagestyle{fancy}
\usepackage{seqsplit} % Keep inline identifiers intact. Move unusually long paths to a code block
% or shorten their prose label instead of splitting callable names mid-word.
% Pandoc emits inline code as \texttt{...}. This blunt wrapper keeps long % Libertinus Mono permits ordinary word hyphenation, so disable that too.
% paths and commands from overflowing PDF table cells. \DeclareTextFontCommand{\texttt}{\ttfamily\hyphenchar\font=-1\relax}
\let\origtexttt\texttt
\renewcommand{\texttt}[1]{{\origtexttt{\seqsplit{#1}}}}
\usepackage{fvextra} \usepackage{fvextra}
\fvset{breaklines=true, breaknonspaceingroup=true, breakanywhere=true} \fvset{breaklines=true, breaknonspaceingroup=true, breakanywhere=true}
+154 -37
View File
@@ -67,10 +67,9 @@ diagram:
Preparing reports, transforming documents, and collecting workspace information Preparing reports, transforming documents, and collecting workspace information
often involve procedures that must be repeated with new inputs. An AI assistant often involve procedures that must be repeated with new inputs. An AI assistant
can help perform such work. Repeating it requires preserving the operations can help perform such work. Repeating it requires preserving the operations
and their data connections as an executable procedure. This thesis presents and their data connections as an executable procedure. This thesis develops
`lda.chat`, a the programmable workflow subsystem of `lda.chat` for defining, checking,
programmable workflow platform for defining, checking, running, and inspecting running, and inspecting reusable workspace procedures.
reusable workspace procedures.
A workflow describes the operations to perform, the data they exchange, and A workflow describes the operations to perform, the data they exchange, and
the decisions that select the next step. An **artifact** is an immutable saved the decisions that select the next step. An **artifact** is an immutable saved
@@ -88,10 +87,10 @@ choice to separate a node's data output from its routing outcome.
The implementation includes a workflow runtime, Python authoring objects, a The implementation includes a workflow runtime, Python authoring objects, a
workflow service, and adapters for trusted Python functions and external tools. workflow service, and adapters for trusted Python functions and external tools.
Evidence includes The evaluation focuses on workflow construction and execution through the
a deterministic case study and focused tests of validation, Python client. It uses a deterministic case study and identifies focused
execution, and persistence. Authoring usability remains to be evaluated as tests of validation, execution, and persistence. Agent-driven authoring and
the interaction design develops. authoring usability remain to be evaluated as the interaction design develops.
# Introduction # Introduction
@@ -327,6 +326,13 @@ or produce combinations. The node's configuration and worked examples make
these operations distinguishable. Append waits for connected inputs and emits these operations distinguishable. Append waits for connected inputs and emits
their items in input order [@n8n-merge-2026]. their items in input order [@n8n-merge-2026].
[@fig:n8n-merge-append] shows the Append setting and the resulting two
output records. The fields remain in separate records; selecting Append
does not combine them into one record.
<!-- markdownlint-disable-next-line MD013 -->
![n8n Merge configured to append two inputs, with its output preview. Author's screenshot.](assets/comparison/n8n-merge-append.png){#fig:n8n-merge-append width=100%}
Selecting a merge mode changes which records appear in the output. Selecting a merge mode changes which records appear in the output.
For the report task, joining actions by owner is different from appending two For the report task, joining actions by owner is different from appending two
action lists. action lists.
@@ -356,6 +362,14 @@ distinguish complete records from records needing attention. Multiple paths
can qualify, so exclusivity must follow from the rules rather than the can qualify, so exclusivity must follow from the rules rather than the
branching appearance alone [@zapier-paths-2026]. branching appearance alone [@zapier-paths-2026].
[@fig:zapier-path-condition] shows a rule testing whether the selected
`Hello` field contains `world`. The test record matches, and the interface
reports that this path would have continued. This preview lets the author
check a rule against a concrete value before running the workflow.
<!-- markdownlint-disable-next-line MD013 -->
![A Zapier Paths condition and its matching test record. Author's screenshot.](assets/comparison/zapier-path-condition.png){#fig:zapier-path-condition width=75%}
The same documentation describes sequential execution of qualifying paths. The same documentation describes sequential execution of qualifying paths.
The rules can permit several branches to execute in one run. An exclusive The rules can permit several branches to execute in one run. An exclusive
choice requires conditions that cannot qualify together. choice requires conditions that cannot qualify together.
@@ -367,6 +381,10 @@ The interface therefore needs to communicate both the condition being tested
and the consequence of a match. In lda.chat, an ordinary outcome selects and the consequence of a match. In lda.chat, an ordinary outcome selects
one successor, whereas several Zapier Paths may qualify. one successor, whereas several Zapier Paths may qualify.
```{=latex}
\begin{minipage}{\linewidth}
```
## LangGraph: describing decisions in Python ## LangGraph: describing decisions in Python
LangGraph's Graph API lets an author define state, add node functions and LangGraph's Graph API lets an author define state, add node functions and
@@ -374,6 +392,47 @@ routing, compile the graph, and invoke it. For the report task, a developer can
represent extracted fields in state and write a routing function that selects represent extracted fields in state and write a routing function that selects
what happens next [@langgraph-graph-api-2026]. what happens next [@langgraph-graph-api-2026].
In this small example, `clean` returns an update to the `text` field.
The separate `choose` function reads that updated state and selects either
the rendering step or the end of the graph:
<!-- langgraph-comparison -->
```python
from typing import TypedDict
from langgraph.graph import START, END, StateGraph
class ReportState(TypedDict):
text: str
report: str
def clean(state: ReportState):
return {"text": state["text"].strip()}
def choose(state: ReportState):
return "render" if state["text"] else END
def render(state: ReportState):
return {"report": "# Report\n\n" + state["text"]}
builder = StateGraph(ReportState)
builder.add_node("clean", clean)
builder.add_node("render", render)
builder.add_edge(START, "clean")
builder.add_conditional_edges("clean", choose, ["render", END])
builder.add_edge("render", END)
graph = builder.compile()
```
```{=latex}
\end{minipage}
```
Calling `graph.invoke({"text": " Notes ", "report": ""})` cleans the text,
then produces a report. Whitespace-only input ends after `clean`. The state
update and the routing decision are expressed in separate functions here;
LangGraph also offers `Command` to return both from a node
[@langgraph-graph-api-2026].
This authoring style exposes more behavior as code. Nodes produce state This authoring style exposes more behavior as code. Nodes produce state
updates, reducers determine how updates combine, and conditional routes select updates, reducers determine how updates combine, and conditional routes select
subsequent execution. The documented model supports graph loops and super-step subsequent execution. The documented model supports graph loops and super-step
@@ -417,8 +476,8 @@ mapping selects a configured provider that must satisfy the saved source
requirements. An app connection or credential is therefore only a partial requirements. An app connection or credential is therefore only a partial
analogy: the source also supplies operations and their contracts. analogy: the source also supplies operations and their contracts.
The design contribution is the composition of these established lifecycle The system combines these lifecycle responsibilities with its typed graph
responsibilities with the typed graph and client interface. Each system must and Python client. Each system must
distinguish an edit to future work from the recorded distinguish an edit to future work from the recorded
definition and data of a past execution. definition and data of a past execution.
@@ -485,7 +544,7 @@ flowchart LR
Read["Read<br/>notes"] -->|ok| Extract["Extract<br/>report"] Read["Read<br/>notes"] -->|ok| Extract["Extract<br/>report"]
Extract -->|ok| Check{Complete?} Extract -->|ok| Check{Complete?}
Check -->|ready| Render["Render<br/>report"] Check -->|ready| Render["Render<br/>report"]
Check -->|needs_information| Ask["Request<br/>information"] Check -->|needs_information| Ask["Request information<br/>(interrupt)"]
Ask -->|submitted| Render Ask -->|submitted| Render
Render -->|ok| Finish([End]) Render -->|ok| Finish([End])
``` ```
@@ -611,7 +670,7 @@ classDiagram
Deployment "1" <-- "0..*" Run : started from Deployment "1" <-- "0..*" Run : started from
``` ```
## Available operations and environment binding ## Available operations and deployment binding
A **capability** describes functionality available from a source. The report A **capability** describes functionality available from a source. The report
example uses operation capabilities, such as extraction and rendering. A example uses operation capabilities, such as extraction and rendering. A
@@ -622,7 +681,7 @@ A **source** groups capabilities under a configured identity. The extraction
operation might come from trusted Python code, while another operation or operation might come from trusted Python code, while another operation or
document is supplied by an external service. document is supplied by an external service.
An **environment binding**, also called a **deployment binding**, connects a A **deployment binding** connects a
logical source requirement in the workflow logical source requirement in the workflow
to a concrete source in the environment. Validation checks whether that source to a concrete source in the environment. Validation checks whether that source
exists and matches the saved requirements. **Source drift** means those exists and matches the saved requirements. **Source drift** means those
@@ -767,6 +826,15 @@ Refreshing it requests a new snapshot. This makes network
activity explicit, although applications must decide when to refresh and how activity explicit, although applications must decide when to refresh and how
to present progress. to present progress.
A local/static server can also start deployment runs on a schedule. The
optional scheduler supports one-time and recurring starts and records each
scheduled occurrence for inspection. The Python client's `App.create_schedule()`
registers a schedule for an existing deployment; `App.schedule()` retrieves
its configuration, and `App.schedule_occurrences()` retrieves its history.
The server must have scheduling enabled to start runs without a connected
client. The Limitations chapter discusses scheduling's storage and
concurrency constraints.
The draft API and CLI also support persisted editing workspaces, which retain The draft API and CLI also support persisted editing workspaces, which retain
unfinished definitions between requests. Python authoring can save directly unfinished definitions between requests. Python authoring can save directly
from its editable object. Both routes produce a saved definition for deployment from its editable object. Both routes produce a saved definition for deployment
@@ -775,7 +843,8 @@ and execution.
## Executing the graph ## Executing the graph
The execution core is the runtime that runs the saved graph. It holds the The execution core is the runtime that runs the saved graph. It holds the
workflow input, working state, and current execution positions. To execute workflow input, working state, and the active node for each in-flight item
or call. To execute
an ordinary operation node, it resolves the node's input bindings, calls the an ordinary operation node, it resolves the node's input bindings, calls the
selected operation, checks its result, applies output bindings, and follows selected operation, checks its result, applies output bindings, and follows
the edge selected by the outcome. Repeating that cycle advances the run until the edge selected by the outcome. Repeating that cycle advances the run until
@@ -818,8 +887,8 @@ flowchart TB
A declared outcome such as `needs_information` is a workflow decision. A A declared outcome such as `needs_information` is a workflow decision. A
handler exception or exhausted node-execution allowance is an execution handler exception or exhausted node-execution allowance is an execution
failure. A workflow can therefore complete with a non-success business outcome failure. A workflow can complete with an outcome such as `needs_information`
without being a failed runtime execution. Where iteration supports collecting item without failing the run. Where iteration supports collecting item
errors, that policy must be explicit; errors do not automatically become errors, that policy must be explicit; errors do not automatically become
ordinary outcome edges. ordinary outcome edges.
@@ -841,7 +910,8 @@ a time. An output binding appends each rendered report to a `reports`
state field, which the assembly step reads. state field, which the assembly step reads.
[@fig:foreach-graph] shows the authored graph. The `loop` edge enters the [@fig:foreach-graph] shows the authored graph. The `loop` edge enters the
item body. Its return edge, `render.ok -> each`, ends that item's work. item body. Its return edge (a back-edge), `render.ok -> each`, ends that
item's work.
The `done` edge leads to the continuation after the collection is processed. The `done` edge leads to the continuation after the collection is processed.
<!-- markdownlint-disable-next-line MD013 --> <!-- markdownlint-disable-next-line MD013 -->
@@ -862,7 +932,9 @@ controller from `Prepare documents` starts an invocation over the collection.
[@fig:foreach-region] follows one invocation from start to finish. Read [@fig:foreach-region] follows one invocation from start to finish. Read
downward for time. Solid arrows request work or apply a data binding; downward for time. Solid arrows request work or apply a data binding;
dashed arrows report completion. The labels identify the data and routes. dashed arrows report completion. Here line style distinguishes requests from
completion; in [@fig:report-state], it distinguishes execution from data
bindings. The labels identify the data and routes.
<!-- markdownlint-disable-next-line MD013 --> <!-- markdownlint-disable-next-line MD013 -->
```{.mermaid #fig:foreach-region width=95% caption="Serial iteration finishes both documents before assembly."} ```{.mermaid #fig:foreach-region width=95% caption="Serial iteration finishes both documents before assembly."}
@@ -888,7 +960,7 @@ The same authored render node executes twice. The runtime distinguishes
those executions so that finishing A advances to B, while finishing B permits those executions so that finishing A advances to B, while finishing B permits
assembly. Each item returns to the foreach invocation that started it. assembly. Each item returns to the foreach invocation that started it.
The back-edge in [@fig:foreach-graph] records the item's return. Its output The return edge in [@fig:foreach-graph] records the item's return. Its output
binding appends the report before that return. A node in the body belongs to binding appends the report before that return. A node in the body belongs to
that loop's control region. The validator rejects using the same node both that loop's control region. The validator rejects using the same node both
inside and outside the body, or returning past the immediate enclosing loop. inside and outside the body, or returning past the immediate enclosing loop.
@@ -915,7 +987,7 @@ The parent item then continues to its own return edge.
sequenceDiagram sequenceDiagram
participant Item as Parent item A participant Item as Parent item A
participant Child as Child workflow participant Child as Child workflow
participant Result as Item A reports writes participant Result as A's pending report writes
participant Each as Foreach participant Each as Foreach
Item->>Child: Input binding: document = A.md Item->>Child: Input binding: document = A.md
activate Child activate Child
@@ -941,7 +1013,7 @@ its child does not permit assembly while B is still running. The foreach
waits for its required item completions before following `done`. waits for its required item completions before following `done`.
Concurrent items retain separate pending writes until their results combine. Concurrent items retain separate pending writes until their results combine.
In [@fig:scope-boundaries], “Item A reports writes” represents these pending In [@fig:scope-boundaries], “A's pending report writes” represents these pending
writes. In serial mode, the output binding updates enclosing state so the next writes. In serial mode, the output binding updates enclosing state so the next
item can read it. item can read it.
@@ -1005,9 +1077,9 @@ re-inspects the exact artifact version. The resulting artifact object is
therefore reconstructed from the stored definition. Identity checks reject therefore reconstructed from the stored definition. Identity checks reject
responses identifying a different artifact or deployment. responses identifying a different artifact or deployment.
The service's `inspect_run` operation retrieves a stored run by its identifier. The Python client's `Run.refresh()` method retrieves a stored run by its
The Python client's `Run.refresh()` method calls that operation, verifies the identifier through the service's `inspect_run` operation. It verifies the
run and deployment identities, and returns a new run snapshot. `Run.resume()` run and deployment identities and returns a new run snapshot. `Run.resume()`
submits the requested response for an interrupted run and also returns a new submits the requested response for an interrupted run and also returns a new
snapshot. The caller retains the returned object to see the updated status snapshot. The caller retains the returned object to see the updated status
and result. and result.
@@ -1059,7 +1131,8 @@ document's separate view until the outer foreach combines the documents.
Publishing every serial section's write directly into shared workflow state Publishing every serial section's write directly into shared workflow state
would expose unfinished document results to the other concurrent items. would expose unfinished document results to the other concurrent items.
The runtime records where each execution belongs. A **frame** records one The runtime tracks the active node, the workflow invocation that owns its
data, and the updates held separately by concurrent items. A **frame** records one
execution position, including its current node and owning foreach when it is execution position, including its current node and owning foreach when it is
an iteration item. A **scope** contains the input, working state, and context an iteration item. A **scope** contains the input, working state, and context
of one workflow invocation. A child workflow has its own scope. A **lineage** of one workflow invocation. A child workflow has its own scope. A **lineage**
@@ -1095,8 +1168,14 @@ unnamed current-item value would leave it unclear which one the node receives.
**Execution context** supplies runtime information to expressions and input **Execution context** supplies runtime information to expressions and input
bindings. Its foreach entries are keyed by the authored controller's identity. bindings. Its foreach entries are keyed by the authored controller's identity.
Inside the inner body, `context.foreach.documents.item` selects the enclosing Inside the inner body, two paths distinguish the enclosing document from
document, while `context.foreach.actions.item` selects the current action. the current action:
```text
context.foreach.documents.item # enclosing document
context.foreach.actions.item # current action
```
The runtime constructs these entries from the active frames. Repeated The runtime constructs these entries from the active frames. Repeated
executions of the same node therefore resolve the paths to their own items. executions of the same node therefore resolve the paths to their own items.
@@ -1198,9 +1277,10 @@ Their Pydantic models describe the input and output
contracts. The author consumes those operations from the service inventory; contracts. The author consumes those operations from the service inventory;
the client does not import their implementations to execute them locally. the client does not import their implementations to execute them locally.
The following blocks form one asynchronous Python session. The retained test The Python blocks form one asynchronous session. The retained test runs that
executes these blocks against the real service in process, replacing only session against the real service in process. It substitutes an in-process
the HTTP connection assignment. It does not verify remote server startup. connection for the HTTP connection assignment; remote server startup is not
tested.
For HTTP use, the blocks assume a For HTTP use, the blocks assume a
server using `examples/report_workflow/wf.config.json`, reachable at its server using `examples/report_workflow/wf.config.json`, reachable at its
configured address, and a fresh artifact name or unused version. The fixture configured address, and a fresh artifact name or unused version. The fixture
@@ -1444,9 +1524,9 @@ sequenceDiagram
Client->>API: Bind deployment to version Client->>API: Bind deployment to version
API-->>Client: Deployment and readiness API-->>Client: Deployment and readiness
Client->>API: Run with input Client->>API: Run with input
API->>Provider: Invoke graph steps API->>Provider: Invoke bound operation
Provider-->>API: Outputs and outcomes Provider-->>API: Output and outcome
API-->>Client: Stopped run snapshot API-->>Client: Run snapshot
Client->>API: Inspect run and bounded trace Client->>API: Inspect run and bounded trace
API-->>Client: Stored execution evidence API-->>Client: Stored execution evidence
Client-->>Author: Result or diagnostic Client-->>Author: Result or diagnostic
@@ -1506,6 +1586,42 @@ recorded risks and followups, and a Markdown report headed
example's data reached the intended outputs. The extraction contract requires example's data reached the intended outputs. The extraction contract requires
the sectioned notes format described at the start of this chapter. the sectioned notes format described at the start of this chapter.
## Registering a scheduled run
The same deployment can be invoked later by the server's scheduler. With
scheduling enabled on the local/static server, the client can register a
one-time start and inspect its stored configuration and occurrence history:
```python
from datetime import UTC, datetime, timedelta
schedule = await app.create_schedule(
schedule_id="report-once",
deployment_id=deployment.deployment_id,
trigger={
"kind": "oneshot",
"at": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
},
input_bindings=[{
"target": "text",
"expression": {"kind": "literal", "value": notes},
}],
max_steps=100,
)
schedule = await app.schedule(schedule.schedule_id)
history = await app.schedule_occurrences(schedule.schedule_id, limit=10)
```
The input binding stores the current contents of `notes`. The scheduled run
receives that text even if the original file changes. A recurring workflow
that needs fresh notes would need an operation that retrieves them when it
runs. Occurrence history records what happened to scheduled starts, including
admission and completion. Immediately after registration, it can be empty.
The session check verifies registration and inspection without waiting for
the scheduled time. Scheduler execution is exercised separately in
[`test_scheduled_deployment_example.py`](../../tests/examples/test_scheduled_deployment_example.py).
## What this case demonstrates ## What this case demonstrates
The executable check in The executable check in
@@ -1520,7 +1636,7 @@ discovery and invocation, and the artifact/deployment/run lifecycle using
the raw-plan fixture. Those tests are evidence for the report operations the raw-plan fixture. Those tests are evidence for the report operations
and lifecycle, complementing the Python session check. and lifecycle, complementing the Python session check.
The Python presentation makes the current authoring experience concrete: The Python session shows the current authoring steps:
inspect operations, declare contracts, connect data and outcomes, save, inspect operations, declare contracts, connect data and outcomes, save,
select bindings, and inspect an execution. The CLI and draft surface offer select bindings, and inspect an execution. The CLI and draft surface offer
alternative interfaces to related lifecycle operations. alternative interfaces to related lifecycle operations.
@@ -1540,8 +1656,9 @@ are needed to evaluate the interaction beyond this scripted session.
Evaluation distinguishes three questions: whether the runtime follows its Evaluation distinguishes three questions: whether the runtime follows its
contract, whether the public lifecycle composes correctly, and whether an contract, whether the public lifecycle composes correctly, and whether an
author can use that lifecycle effectively. The current evidence addresses author can use that lifecycle effectively. The reported in-process walkthrough
the first two through controlled tests and an adapted in-process walkthrough. exercises the public lifecycle. Targeted tests for runtime contracts are
mapped in the Evidence Index.
The authoring assessment identifies the operations available through the The authoring assessment identifies the operations available through the
interface. Usability remains a separate evaluation question. interface. Usability remains a separate evaluation question.
@@ -1845,7 +1962,7 @@ different from promising that an earlier action can be undone.
This report examined how a useful procedure can become a reusable workflow This report examined how a useful procedure can become a reusable workflow
that an author can define and an operator can inspect. The implemented that an author can define and an operator can inspect. The implemented
system separates the saved definition, its environment bindings, and each system separates the saved definition, its deployment bindings, and each
execution into artifacts, deployments, and runs. Those distinctions give execution into artifacts, deployments, and runs. Those distinctions give
workflow use a record beyond the lifetime of an editing session or a single workflow use a record beyond the lifetime of an editing session or a single
script invocation. script invocation.
+43
View File
@@ -58,3 +58,46 @@ def test_abbreviations_compile_without_a_dummy_float(tmp_path: Path) -> None:
assert "API" in rendered assert "API" in rendered
assert "Application Programming Interface" in rendered assert "Application Programming Interface" in rendered
assert "None 1" not in rendered assert "None 1" not in rendered
@pytest.mark.skipif(
any(shutil.which(tool) is None for tool in ("pandoc", "xelatex", "pdftotext")),
reason="Pandoc, XeLaTeX, and pdftotext are needed for the inline-code build",
)
def test_inline_identifier_moves_to_next_line_intact(tmp_path: Path) -> None:
"""A prose line ending must not split a callable's name into fragments."""
output = tmp_path / "inline.pdf"
identifier = "local.report.render_markdown_report"
result = subprocess.run(
[
"pandoc",
"--standalone",
"--pdf-engine=xelatex",
"--variable=geometry:textwidth=10cm",
"--variable=monofont:Libertinus Mono",
"--include-in-header",
str(THESIS / "header-includes.tex"),
"--output",
str(output),
],
# Shift the identifier along the line to expose both forced splitting
# and font-dependent automatic hyphenation near the right margin.
input="\n\n".join(
f"{'word ' * count}`{identifier}`." for count in range(1, 16)
),
text=True,
encoding="utf-8",
capture_output=True,
check=False,
timeout=90,
)
assert result.returncode == 0, result.stderr
rendered = subprocess.run(
["pdftotext", "-layout", str(output), "-"],
capture_output=True,
text=True,
encoding="utf-8",
check=True,
timeout=15,
).stdout
assert rendered.count(identifier) == 15
@@ -0,0 +1,28 @@
"""Exercise the comparison snippet when the optional LangGraph library exists."""
from pathlib import Path
import pytest
@pytest.mark.parametrize(
("text", "expected_report"),
[(" Notes ", "# Report\n\nNotes"), (" ", "")],
)
def test_langgraph_comparison_routes_updated_state(
text: str, expected_report: str
) -> None:
"""Check the rendered example without making LangGraph a product dependency."""
pytest.importorskip("langgraph.graph")
manuscript = (
Path(__file__).resolve().parents[2]
/ "docs/thesis/system-design-implementation.md"
).read_text(encoding="utf-8")
section = manuscript.split("<!-- langgraph-comparison -->", 1)[1]
snippet = section.split("```python\n", 1)[1].split("```", 1)[0]
namespace = {}
# This is trusted repository prose, executed exactly as displayed.
exec(compile(snippet, "thesis-langgraph-example", "exec"), namespace)
result = namespace["graph"].invoke({"text": text, "report": ""})
assert result["text"] == text.strip()
assert result["report"] == expected_report
@@ -10,9 +10,11 @@ from typing import Any, cast
import pytest import pytest
from wf_client import App, Deployment, Run from wf_api.durable_context import durable_workflow_api
from wf_client import App, Deployment, Run, Schedule
from wf_client.protocols import WorkflowClientPort from wf_client.protocols import WorkflowClientPort
from wf_config import load_workflow_config from wf_config import load_workflow_config
from wf_scheduling.store import FileScheduleStore
from wf_server.config import build_workflow_server_from_workflow_config from wf_server.config import build_workflow_server_from_workflow_config
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
@@ -32,7 +34,11 @@ async def test_thesis_python_session_preserves_report_and_repair(
config = load_workflow_config(ROOT / "examples/report_workflow/wf.config.json") config = load_workflow_config(ROOT / "examples/report_workflow/wf.config.json")
config.server.store.root = tmp_path / "store" config.server.store.root = tmp_path / "store"
server = build_workflow_server_from_workflow_config(config) server = build_workflow_server_from_workflow_config(config)
app = App._from_port(cast(WorkflowClientPort, server.api)) # Enable real schedule persistence without starting a background scheduler:
# this session verifies registration, while another test exercises dispatch.
schedule_store = FileScheduleStore(config.server.store.root)
api = durable_workflow_api(server.context, schedule_store=schedule_store)
app = App._from_port(cast(WorkflowClientPort, api))
monkeypatch.chdir(ROOT) monkeypatch.chdir(ROOT)
manuscript = THESIS.read_text(encoding="utf-8") manuscript = THESIS.read_text(encoding="utf-8")
@@ -84,3 +90,14 @@ async def test_thesis_python_session_preserves_report_and_repair(
for displayed in displayed_outputs: for displayed in displayed_outputs:
assert displayed in diagnostic, "Displayed output differs from the session" assert displayed in diagnostic, "Displayed output differs from the session"
assert namespace["trace"].frames assert namespace["trace"].frames
schedule = namespace["schedule"]
assert isinstance(schedule, Schedule)
assert schedule.deployment_id == deployment.deployment_id
assert schedule.max_steps == 100
assert schedule.trigger["kind"] == "oneshot"
persisted = schedule_store.get_schedule(schedule.schedule_id)
assert persisted is not None
expression = persisted.input_bindings[0].expression
assert expression.kind == "literal"
assert expression.value == namespace["notes"]
assert namespace["history"]["occurrences"] == []