rote-cli 0.1.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {rote_cli-0.1.0 → rote_cli-0.4.0}/PKG-INFO +76 -18
  2. {rote_cli-0.1.0 → rote_cli-0.4.0}/README.md +75 -17
  3. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/__init__.py +1 -1
  4. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/adapters/__init__.py +7 -0
  5. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/adapters/_common.py +58 -1
  6. rote_cli-0.4.0/src/rote/adapters/_ts_common.py +338 -0
  7. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/adapters/cloudflare.py +193 -294
  8. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/adapters/dbos.py +37 -17
  9. rote_cli-0.4.0/src/rote/adapters/dbos_ts.py +906 -0
  10. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/adapters/temporal.py +6 -5
  11. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/cli.py +17 -5
  12. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/ir.py +85 -0
  13. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/serve/backends.py +34 -9
  14. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/serve/registry.py +10 -6
  15. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/serve/server.py +28 -3
  16. {rote_cli-0.1.0 → rote_cli-0.4.0}/.gitignore +0 -0
  17. {rote_cli-0.1.0 → rote_cli-0.4.0}/LICENSE +0 -0
  18. {rote_cli-0.1.0 → rote_cli-0.4.0}/pyproject.toml +0 -0
  19. {rote_cli-0.1.0 → rote_cli-0.4.0}/skills/rote-graduate/SKILL.md +0 -0
  20. {rote_cli-0.1.0 → rote_cli-0.4.0}/skills/rote-graduate/references/crystallization-heuristics.md +0 -0
  21. {rote_cli-0.1.0 → rote_cli-0.4.0}/skills/rote-graduate/references/ir-schema.md +0 -0
  22. {rote_cli-0.1.0 → rote_cli-0.4.0}/skills/rote-graduate/references/llm-judge-extraction.md +0 -0
  23. {rote_cli-0.1.0 → rote_cli-0.4.0}/skills/rote-graduate/references/node-kinds.md +0 -0
  24. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/graduator/__init__.py +0 -0
  25. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/graduator/drivers/__init__.py +0 -0
  26. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/graduator/drivers/anthropic_api.py +0 -0
  27. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/graduator/drivers/claude.py +0 -0
  28. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/graduator/drivers/codex.py +0 -0
  29. {rote_cli-0.1.0 → rote_cli-0.4.0}/src/rote/serve/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rote-cli
3
- Version: 0.1.0
3
+ Version: 0.4.0
4
4
  Summary: Graduate fuzzy AI skills into deterministic, reliable workflows
5
5
  Project-URL: Homepage, https://github.com/trevhud/rote
6
6
  Project-URL: Repository, https://github.com/trevhud/rote
@@ -59,8 +59,13 @@ applies a structured graduation rubric, and emits:
59
59
  - and runtime code for your durable execution engine of choice.
60
60
 
61
61
  ```sh
62
- # Two runtimes shipped today emit Python for Temporal, or TypeScript
63
- # for Cloudflare Workflows:
62
+ pip install rote-cli # or zero-install: uvx --from rote-cli rote ...
63
+
64
+ # Default target is DBOS — durable execution as a plain Python library,
65
+ # no orchestrator to run, SQLite for dev / Postgres for prod:
66
+ rote graduate ./examples/bdr-outreach/skill --out ./graduated/
67
+
68
+ # Or target Temporal (Python) or Cloudflare Workflows (TypeScript):
64
69
  rote graduate ./examples/bdr-outreach/skill --runtime temporal --out ./graduated/
65
70
  rote graduate ./examples/bdr-outreach/skill --runtime cloudflare --out ./graduated/
66
71
  ```
@@ -140,6 +145,10 @@ The graduator independently:
140
145
  After the agent finishes, a runtime adapter consumes the IR and emits
141
146
  the target runtime's native code shape:
142
147
 
148
+ - The **DBOS** adapter (the default) emits a single `main.py` — one
149
+ `@DBOS.workflow` DAG plus a `@DBOS.step` per node, checkpointing to
150
+ SQLite or Postgres. `python main.py` *is* the runtime; there is no
151
+ orchestrator to deploy.
143
152
  - The **Temporal** adapter emits `workflow.py` (the orchestration
144
153
  class with `@workflow.defn` and signal handlers for the HITL gates)
145
154
  and `activities.py` (one `@activity.defn` per node, lazy-importing
@@ -150,6 +159,14 @@ the target runtime's native code shape:
150
159
  `signatures/*.ts` (Zod schemas + Anthropic SDK calls) and the
151
160
  supporting `wrangler.jsonc` / `package.json` / `tsconfig.json`. The
152
161
  output is `wrangler deploy`-ready.
162
+ - The **DBOS (TypeScript)** adapter (`--runtime dbos-ts`) emits a
163
+ Node.js app for DBOS Transact: `src/main.ts` registers one durable
164
+ workflow (`DBOS.registerWorkflow`) running the DAG waves and one
165
+ `DBOS.registerStep` per node, with `DBOS.recv(...)` parking each
166
+ HITL gate durably in Postgres until `DBOS.send(...)` resumes it.
167
+ Zero-orchestrator like the Python DBOS target — `node dist/main.js`
168
+ *is* the runtime — but note the TS SDK is Postgres-only (no SQLite
169
+ mode; `npx dbos postgres start` covers local dev).
153
170
 
154
171
  None of the emitted code references an MCP runtime, in either
155
172
  language — the agent's crystallization step replaces tool calls with
@@ -242,9 +259,43 @@ agent-driven — so the same IR always produces byte-identical output.
242
259
 
243
260
  ## Quickstart
244
261
 
245
- ### Install
262
+ ### Use from Claude Code (recommended)
263
+
264
+ `rote` ships as a Claude Code plugin, so you can graduate a skill
265
+ without leaving Claude or touching Python tooling:
266
+
267
+ ```
268
+ /plugin marketplace add trevhud/rote
269
+ /plugin install rote@rote
270
+ ```
246
271
 
247
- `rote` is not on PyPI yet. Clone and install in editable mode:
272
+ Then say "graduate this skill" (or run `/rote:graduate` directly).
273
+ The plugin confirms the source skill directory, asks which runtime you
274
+ want (Temporal, Cloudflare Workflows, or DBOS), runs the CLI via
275
+ [uv](https://docs.astral.sh/uv/) in the background, and reports the
276
+ emitted pipeline. A second skill, `/rote:serve`, wires graduated
277
+ pipelines up as MCP tools so Claude can trigger the deployed workflows.
278
+
279
+ Prefer a terminal? The same thing is one `uvx` command:
280
+
281
+ ```sh
282
+ uvx --from rote-cli rote graduate ./my-skill --runtime dbos --out ./graduated
283
+
284
+ # or straight from GitHub for unreleased changes:
285
+ uvx --from git+https://github.com/trevhud/rote rote graduate \
286
+ ./my-skill --runtime dbos --out ./graduated
287
+ ```
288
+
289
+ > **Naming note:** the `rote` package on PyPI is an unrelated
290
+ > memoization library that also installs `import rote`, so the two
291
+ > can't share an environment. This project's distribution is
292
+ > `rote-cli` while the CLI command and import name stay `rote` —
293
+ > hence `uvx --from rote-cli rote ...`. See
294
+ > [docs/releasing.md](docs/releasing.md).
295
+
296
+ ### Install from source (development)
297
+
298
+ Clone and install in editable mode:
248
299
 
249
300
  ```sh
250
301
  git clone https://github.com/trevhud/rote.git
@@ -263,11 +314,12 @@ The repository includes a real BDR outreach skill in
263
314
  `examples/bdr-outreach/skill/`. Graduate it:
264
315
 
265
316
  ```sh
266
- rote graduate examples/bdr-outreach/skill \
267
- --runtime temporal \
268
- --out /tmp/bdr-graduated
317
+ rote graduate examples/bdr-outreach/skill --out /tmp/bdr-graduated
269
318
  ```
270
319
 
320
+ This targets DBOS by default; pass `--runtime temporal` or
321
+ `--runtime cloudflare` for the other adapters.
322
+
271
323
  By default `rote` auto-detects an available agent driver in this
272
324
  order: `claude` (Claude Code CLI) → `codex` (Codex CLI) → `api`
273
325
  (Anthropic SDK with `ANTHROPIC_API_KEY`). Override with `--agent`:
@@ -286,10 +338,12 @@ The output directory is structured as:
286
338
  │ ├── signatures/*.py # typed LLM-judge signatures
287
339
  │ ├── evals/*.jsonl # seed eval examples
288
340
  │ └── graduation-report.md # human-readable summary
289
- └── runtime/temporal/ # produced by the adapter
290
- ├── workflow.py
291
- ├── activities.py
292
- └── __init__.py
341
+ └── runtime/dbos/ # produced by the adapter
342
+ ├── main.py # @DBOS.workflow + one @DBOS.step per node
343
+ ├── extracted/*.py # copied deterministic functions
344
+ ├── signatures/*.py # generated Pydantic + vendor-SDK judges
345
+ ├── dbos-config.yaml
346
+ └── README.md # how to run, signal HITL gates, deploy
293
347
  ```
294
348
 
295
349
  ### Render an IR without re-running the agent
@@ -298,6 +352,7 @@ If you already have a `pipeline.yaml` (hand-written or from a previous
298
352
  graduation), `rote emit` runs just the adapter step:
299
353
 
300
354
  ```sh
355
+ rote emit /path/to/pipeline.yaml --out /tmp/emitted/ # dbos (default)
301
356
  rote emit /path/to/pipeline.yaml --runtime temporal --out /tmp/emitted/
302
357
  ```
303
358
 
@@ -359,10 +414,12 @@ Opus's extra reasoning earns its cost.
359
414
  ## Status
360
415
 
361
416
  `rote` is **pre-1.0**. The end-to-end flow works on the BDR example
362
- and the test suite covers each layer (133 tests in the fast suite,
363
- plus 3 slow tests that compile the emitted Cloudflare TypeScript
364
- against the real `@cloudflare/workers-types` definitions and drive a
365
- real workflow instance through both HITL gates via `wrangler dev`).
417
+ and the test suite covers each layer (231 tests in the fast suite,
418
+ plus 5 slow tests that run the emitted code against real runtimes:
419
+ a DBOS runtime over SQLite, Temporal's time-skipping test server, the
420
+ emitted Cloudflare TypeScript compiled against real
421
+ `@cloudflare/workers-types` and driven through both HITL gates via
422
+ `wrangler dev`, and the MCP server over a real stdio transport).
366
423
  Run `pytest tests/` for the fast suite; `pytest tests/ -m slow` for
367
424
  the toolchain-dependent integration tests.
368
425
 
@@ -372,6 +429,7 @@ the toolchain-dependent integration tests.
372
429
  | Temporal adapter | working (validated with mocked-activities e2e test) |
373
430
  | Cloudflare Workflows adapter | working (validated with `tsc --noEmit` over the real emitted output) |
374
431
  | DBOS adapter | working (validated against a real DBOS runtime over SQLite in the e2e test) |
432
+ | DBOS TypeScript adapter (`dbos-ts`) | working (validated with `tsc --noEmit` over the real emitted output and a live run on the DBOS TS runtime against Docker Postgres) |
375
433
  | Graduator orchestrator | working |
376
434
  | `rote graduate` / `rote emit` CLI commands | working |
377
435
  | `claude` driver | working |
@@ -379,8 +437,8 @@ the toolchain-dependent integration tests.
379
437
  | `codex` driver | stub (`is_available` works; `run` not implemented) |
380
438
  | Inngest / Restate adapters | planned |
381
439
  | Real implementations of the extracted modules | the agent produces stubs that raise `NotImplementedError`; humans fill them in with real API client code |
382
- | Workflow data flow between activities | working — nodes declare `inputs:` bindings and all three adapters (Temporal, Cloudflare, DBOS) thread real payloads through the DAG, validated in the runtime e2e tests |
383
- | Distribution via PyPI | not yet published install from source. The release pipeline (tag-driven, Trusted Publishing) is in place; see [docs/releasing.md](docs/releasing.md) |
440
+ | Workflow data flow between activities | working — nodes declare `inputs:` bindings and all four adapters (Temporal, Cloudflare, DBOS, DBOS-TS) thread real payloads through the DAG, validated in the runtime e2e tests |
441
+ | Distribution via PyPI | published as [`rote-cli`](https://pypi.org/project/rote-cli/) (`pip install rote-cli`); tag-driven Trusted Publishing releases see [docs/releasing.md](docs/releasing.md) |
384
442
 
385
443
  The project explicitly **does not** depend on `claude-agent-sdk`.
386
444
  Anthropic's terms of service forbid third-party agents built on the
@@ -13,8 +13,13 @@ applies a structured graduation rubric, and emits:
13
13
  - and runtime code for your durable execution engine of choice.
14
14
 
15
15
  ```sh
16
- # Two runtimes shipped today emit Python for Temporal, or TypeScript
17
- # for Cloudflare Workflows:
16
+ pip install rote-cli # or zero-install: uvx --from rote-cli rote ...
17
+
18
+ # Default target is DBOS — durable execution as a plain Python library,
19
+ # no orchestrator to run, SQLite for dev / Postgres for prod:
20
+ rote graduate ./examples/bdr-outreach/skill --out ./graduated/
21
+
22
+ # Or target Temporal (Python) or Cloudflare Workflows (TypeScript):
18
23
  rote graduate ./examples/bdr-outreach/skill --runtime temporal --out ./graduated/
19
24
  rote graduate ./examples/bdr-outreach/skill --runtime cloudflare --out ./graduated/
20
25
  ```
@@ -94,6 +99,10 @@ The graduator independently:
94
99
  After the agent finishes, a runtime adapter consumes the IR and emits
95
100
  the target runtime's native code shape:
96
101
 
102
+ - The **DBOS** adapter (the default) emits a single `main.py` — one
103
+ `@DBOS.workflow` DAG plus a `@DBOS.step` per node, checkpointing to
104
+ SQLite or Postgres. `python main.py` *is* the runtime; there is no
105
+ orchestrator to deploy.
97
106
  - The **Temporal** adapter emits `workflow.py` (the orchestration
98
107
  class with `@workflow.defn` and signal handlers for the HITL gates)
99
108
  and `activities.py` (one `@activity.defn` per node, lazy-importing
@@ -104,6 +113,14 @@ the target runtime's native code shape:
104
113
  `signatures/*.ts` (Zod schemas + Anthropic SDK calls) and the
105
114
  supporting `wrangler.jsonc` / `package.json` / `tsconfig.json`. The
106
115
  output is `wrangler deploy`-ready.
116
+ - The **DBOS (TypeScript)** adapter (`--runtime dbos-ts`) emits a
117
+ Node.js app for DBOS Transact: `src/main.ts` registers one durable
118
+ workflow (`DBOS.registerWorkflow`) running the DAG waves and one
119
+ `DBOS.registerStep` per node, with `DBOS.recv(...)` parking each
120
+ HITL gate durably in Postgres until `DBOS.send(...)` resumes it.
121
+ Zero-orchestrator like the Python DBOS target — `node dist/main.js`
122
+ *is* the runtime — but note the TS SDK is Postgres-only (no SQLite
123
+ mode; `npx dbos postgres start` covers local dev).
107
124
 
108
125
  None of the emitted code references an MCP runtime, in either
109
126
  language — the agent's crystallization step replaces tool calls with
@@ -196,9 +213,43 @@ agent-driven — so the same IR always produces byte-identical output.
196
213
 
197
214
  ## Quickstart
198
215
 
199
- ### Install
216
+ ### Use from Claude Code (recommended)
217
+
218
+ `rote` ships as a Claude Code plugin, so you can graduate a skill
219
+ without leaving Claude or touching Python tooling:
220
+
221
+ ```
222
+ /plugin marketplace add trevhud/rote
223
+ /plugin install rote@rote
224
+ ```
200
225
 
201
- `rote` is not on PyPI yet. Clone and install in editable mode:
226
+ Then say "graduate this skill" (or run `/rote:graduate` directly).
227
+ The plugin confirms the source skill directory, asks which runtime you
228
+ want (Temporal, Cloudflare Workflows, or DBOS), runs the CLI via
229
+ [uv](https://docs.astral.sh/uv/) in the background, and reports the
230
+ emitted pipeline. A second skill, `/rote:serve`, wires graduated
231
+ pipelines up as MCP tools so Claude can trigger the deployed workflows.
232
+
233
+ Prefer a terminal? The same thing is one `uvx` command:
234
+
235
+ ```sh
236
+ uvx --from rote-cli rote graduate ./my-skill --runtime dbos --out ./graduated
237
+
238
+ # or straight from GitHub for unreleased changes:
239
+ uvx --from git+https://github.com/trevhud/rote rote graduate \
240
+ ./my-skill --runtime dbos --out ./graduated
241
+ ```
242
+
243
+ > **Naming note:** the `rote` package on PyPI is an unrelated
244
+ > memoization library that also installs `import rote`, so the two
245
+ > can't share an environment. This project's distribution is
246
+ > `rote-cli` while the CLI command and import name stay `rote` —
247
+ > hence `uvx --from rote-cli rote ...`. See
248
+ > [docs/releasing.md](docs/releasing.md).
249
+
250
+ ### Install from source (development)
251
+
252
+ Clone and install in editable mode:
202
253
 
203
254
  ```sh
204
255
  git clone https://github.com/trevhud/rote.git
@@ -217,11 +268,12 @@ The repository includes a real BDR outreach skill in
217
268
  `examples/bdr-outreach/skill/`. Graduate it:
218
269
 
219
270
  ```sh
220
- rote graduate examples/bdr-outreach/skill \
221
- --runtime temporal \
222
- --out /tmp/bdr-graduated
271
+ rote graduate examples/bdr-outreach/skill --out /tmp/bdr-graduated
223
272
  ```
224
273
 
274
+ This targets DBOS by default; pass `--runtime temporal` or
275
+ `--runtime cloudflare` for the other adapters.
276
+
225
277
  By default `rote` auto-detects an available agent driver in this
226
278
  order: `claude` (Claude Code CLI) → `codex` (Codex CLI) → `api`
227
279
  (Anthropic SDK with `ANTHROPIC_API_KEY`). Override with `--agent`:
@@ -240,10 +292,12 @@ The output directory is structured as:
240
292
  │ ├── signatures/*.py # typed LLM-judge signatures
241
293
  │ ├── evals/*.jsonl # seed eval examples
242
294
  │ └── graduation-report.md # human-readable summary
243
- └── runtime/temporal/ # produced by the adapter
244
- ├── workflow.py
245
- ├── activities.py
246
- └── __init__.py
295
+ └── runtime/dbos/ # produced by the adapter
296
+ ├── main.py # @DBOS.workflow + one @DBOS.step per node
297
+ ├── extracted/*.py # copied deterministic functions
298
+ ├── signatures/*.py # generated Pydantic + vendor-SDK judges
299
+ ├── dbos-config.yaml
300
+ └── README.md # how to run, signal HITL gates, deploy
247
301
  ```
248
302
 
249
303
  ### Render an IR without re-running the agent
@@ -252,6 +306,7 @@ If you already have a `pipeline.yaml` (hand-written or from a previous
252
306
  graduation), `rote emit` runs just the adapter step:
253
307
 
254
308
  ```sh
309
+ rote emit /path/to/pipeline.yaml --out /tmp/emitted/ # dbos (default)
255
310
  rote emit /path/to/pipeline.yaml --runtime temporal --out /tmp/emitted/
256
311
  ```
257
312
 
@@ -313,10 +368,12 @@ Opus's extra reasoning earns its cost.
313
368
  ## Status
314
369
 
315
370
  `rote` is **pre-1.0**. The end-to-end flow works on the BDR example
316
- and the test suite covers each layer (133 tests in the fast suite,
317
- plus 3 slow tests that compile the emitted Cloudflare TypeScript
318
- against the real `@cloudflare/workers-types` definitions and drive a
319
- real workflow instance through both HITL gates via `wrangler dev`).
371
+ and the test suite covers each layer (231 tests in the fast suite,
372
+ plus 5 slow tests that run the emitted code against real runtimes:
373
+ a DBOS runtime over SQLite, Temporal's time-skipping test server, the
374
+ emitted Cloudflare TypeScript compiled against real
375
+ `@cloudflare/workers-types` and driven through both HITL gates via
376
+ `wrangler dev`, and the MCP server over a real stdio transport).
320
377
  Run `pytest tests/` for the fast suite; `pytest tests/ -m slow` for
321
378
  the toolchain-dependent integration tests.
322
379
 
@@ -326,6 +383,7 @@ the toolchain-dependent integration tests.
326
383
  | Temporal adapter | working (validated with mocked-activities e2e test) |
327
384
  | Cloudflare Workflows adapter | working (validated with `tsc --noEmit` over the real emitted output) |
328
385
  | DBOS adapter | working (validated against a real DBOS runtime over SQLite in the e2e test) |
386
+ | DBOS TypeScript adapter (`dbos-ts`) | working (validated with `tsc --noEmit` over the real emitted output and a live run on the DBOS TS runtime against Docker Postgres) |
329
387
  | Graduator orchestrator | working |
330
388
  | `rote graduate` / `rote emit` CLI commands | working |
331
389
  | `claude` driver | working |
@@ -333,8 +391,8 @@ the toolchain-dependent integration tests.
333
391
  | `codex` driver | stub (`is_available` works; `run` not implemented) |
334
392
  | Inngest / Restate adapters | planned |
335
393
  | Real implementations of the extracted modules | the agent produces stubs that raise `NotImplementedError`; humans fill them in with real API client code |
336
- | Workflow data flow between activities | working — nodes declare `inputs:` bindings and all three adapters (Temporal, Cloudflare, DBOS) thread real payloads through the DAG, validated in the runtime e2e tests |
337
- | Distribution via PyPI | not yet published install from source. The release pipeline (tag-driven, Trusted Publishing) is in place; see [docs/releasing.md](docs/releasing.md) |
394
+ | Workflow data flow between activities | working — nodes declare `inputs:` bindings and all four adapters (Temporal, Cloudflare, DBOS, DBOS-TS) thread real payloads through the DAG, validated in the runtime e2e tests |
395
+ | Distribution via PyPI | published as [`rote-cli`](https://pypi.org/project/rote-cli/) (`pip install rote-cli`); tag-driven Trusted Publishing releases see [docs/releasing.md](docs/releasing.md) |
338
396
 
339
397
  The project explicitly **does not** depend on `claude-agent-sdk`.
340
398
  Anthropic's terms of service forbid third-party agents built on the
@@ -1,3 +1,3 @@
1
1
  """rote — graduate fuzzy AI skills into deterministic workflows."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.4.0"
@@ -48,12 +48,19 @@ def _dbos_adapter_factory() -> Adapter:
48
48
  return DbosAdapter()
49
49
 
50
50
 
51
+ def _dbos_ts_adapter_factory() -> Adapter:
52
+ from rote.adapters.dbos_ts import DbosTsAdapter
53
+
54
+ return DbosTsAdapter()
55
+
56
+
51
57
  #: Name → factory. Keep the values as zero-arg callables so adapters can
52
58
  #: lazy-import their heavy dependencies.
53
59
  ADAPTERS: dict[str, Callable[[], Adapter]] = {
54
60
  "temporal": _temporal_adapter_factory,
55
61
  "cloudflare": _cloudflare_adapter_factory,
56
62
  "dbos": _dbos_adapter_factory,
63
+ "dbos-ts": _dbos_ts_adapter_factory,
57
64
  }
58
65
 
59
66
 
@@ -13,9 +13,63 @@ from __future__ import annotations
13
13
 
14
14
  import hashlib
15
15
  import re
16
+ from pathlib import Path
16
17
 
17
18
  from rote.ir import Node, Pipeline, parse_input_ref
18
19
 
20
+ # ───────── Prose → safe code comment/docstring ─────────
21
+
22
+
23
+ def safe_docstring_line(text: str, fallback: str = "") -> str:
24
+ """First line of ``text``, escaped so it cannot break out of a docstring.
25
+
26
+ Node ``description`` is prose (it legitimately contains quotes and
27
+ backslashes), yet adapters splice its first line straight into a
28
+ ``\"\"\"…\"\"\"`` Python docstring / block comment. An unescaped
29
+ ``\"\"\"`` — or a trailing backslash that escapes the closing quote —
30
+ lets a crafted pipeline.yaml close the docstring early and inject
31
+ code. Escaping backslashes and double-quotes neutralizes both without
32
+ charset-restricting prose at the IR layer.
33
+ """
34
+ stripped = text.strip() if text else ""
35
+ first = stripped.splitlines()[0] if stripped else fallback
36
+ return first.replace("\\", "\\\\").replace('"', '\\"')
37
+
38
+
39
+ def safe_block_comment_line(text: str, fallback: str = "") -> str:
40
+ """First line of ``text``, safe to splice into a ``/* … */`` block comment.
41
+
42
+ The TypeScript adapter emits ``description`` first-lines into JSDoc
43
+ comments. An unescaped ``*/`` would close the comment early and let a
44
+ crafted pipeline.yaml inject code after it; neutralize the sequence by
45
+ inserting a space (``*/`` → ``* /``). Newlines are already dropped by
46
+ taking the first line.
47
+ """
48
+ stripped = text.strip() if text else ""
49
+ first = stripped.splitlines()[0] if stripped else fallback
50
+ return first.replace("*/", "* /")
51
+
52
+
53
+ # ───────── Output-path containment ─────────
54
+
55
+
56
+ def resolve_within(base: Path, *parts: str) -> Path:
57
+ """Join ``parts`` onto ``base`` and assert the result stays inside it.
58
+
59
+ Defense-in-depth for emitted-file paths. IR validation already pins
60
+ ``node.id`` to an identifier, but this guard means any future field
61
+ that reaches a filename cannot escape the output directory via an
62
+ absolute segment or ``..`` traversal — it fails loudly at emit time.
63
+ """
64
+ base_resolved = base.resolve()
65
+ target = base_resolved.joinpath(*parts).resolve()
66
+ if target != base_resolved and base_resolved not in target.parents:
67
+ raise ValueError(
68
+ f"Refusing to write outside output directory: {target} is not within {base_resolved}"
69
+ )
70
+ return target
71
+
72
+
19
73
  # ───────── Case conversion ─────────
20
74
 
21
75
 
@@ -42,7 +96,10 @@ def _pipeline_hash(pipeline: Pipeline) -> str:
42
96
  name) so a regenerated pipeline becomes a new type. Old in-flight
43
97
  workflows continue on the old code; new workflows use the new code.
44
98
  """
45
- payload = f"{pipeline.name}|{pipeline.version}|{len(pipeline.nodes)}|{len(pipeline.edges)}"
99
+ # Hash the full validated contents: node wiring, inputs, retries, and
100
+ # edges all participate, so any regeneration that changes behavior gets
101
+ # a new workflow type — name/version/counts alone miss rewires.
102
+ payload = pipeline.model_dump_json(by_alias=True, exclude_none=True)
46
103
  return hashlib.sha256(payload.encode()).hexdigest()[:8]
47
104
 
48
105