From 5e4450adaba680ad1d28c86beeca16b1da5f17e7 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 16 May 2026 17:56:34 +0200 Subject: [PATCH 01/98] AGENTS.md --- AGENTS.md | 94 ++++++++++++++++++++-- bin/{journal-follow.py => journal-tail.py} | 8 +- 2 files changed, 94 insertions(+), 8 deletions(-) rename bin/{journal-follow.py => journal-tail.py} (93%) diff --git a/AGENTS.md b/AGENTS.md index df47c7d7..60bc5792 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,21 +1,91 @@ # Instructions for the AI Agents -This document provides instructions and guidelines for the AI agents working on this project. +This document provides instructions and guidelines for the AI agents working on `donna`. -Every agent MUST follow the rules and guidelines outlined in this document when performing their work. +Every agent MUST follow these instructions. -## Donna tool +## Project Overview -Since this is the repository that contains the Donna project itself, you have direct access to the Donna CLI tool via `./bin/donna.sh` script. I.e. you develop Donna using Donna. +`donna` is a CLI tool that helps agents run predefined workflows in deterministic way. -In all commands that use `donna`, you MUST replace `donna` with `./bin/donna.sh` when you run the command. +Workflow is a state-machine / non-linear graph of operations that guides the agent's work. Each operation can run code, output text, provide instructions for the agent (to execute them and report back to Donna), or do other things. The agent's task is to follow the instructions of the operations and report back to Donna about the next operation to run. -For example, instead of `donna artifacts list` you MUST run `./bin/donna.sh artifacts list`. +Donna maintains the state of the workflow and the stack of operations. + +So, you may look at `donna` as a Virtual Machine for agents, where agent is just one of the possible execution contexts, and workflows are programs that run in this VM. + +## Source Of Truth + +Project behavior and architecture are specified in `./specs/`. + +Agents MUST read the relevant specifications before making changes. + +Start from `./specs/intro.md` to find the relevant specification documents. + +When adding, deleting, or significantly changing a specification, agents MUST update `./specs/intro.md`. + +Agents MUST NOT create new specifications without explicit instructions. + +Agents MUST NOT delete or significantly change existing specifications without explicit instructions. + +## Development Environment + +All development-related operations MUST be performed in Docker containers. + +Agents MUST NOT perform development-related operations directly on the host machine. + +Allowed development commands: + +- `./bin/dev.sh` — run development utilities inside the container, for example `./bin/dev.sh uv run -- pytest`. +- `./bin/dev-build-containers.sh` — build base Docker images for development; use only after approved Docker or dependency changes. + +Searching, reading, and editing repository files MAY be done on the host machine. + +## Restricted Changes And Operations + +Agents MUST NOT perform these operations without explicit permission: + +- Change `docker-compose.yml` or Docker-related configuration. +- Change Docker runtime parameters such as resources or volumes. +- Change running Docker services unrelated to this project. +- Install new dependencies. +- Update lock files. +- Install new tools, utilities, or software on the host machine or in development containers. +- Change project structure by moving files or creating new top-level directories. + +If one of these operations seems necessary, agents MUST ask for explicit permission before doing it. + +## Implementation Guidance + +Follow existing specifications and local project patterns. + +Keep changes scoped to the requested task. + +Do not implement behavior that is only mentioned as future or possible functionality unless explicitly requested. + +When code is added, tests SHOULD follow `./specs/architecture/tests.md`. + +When entities, errors, warnings, or module layout are affected, agents MUST check the corresponding architecture specs. ## Top priority tools These tools MUST have the highest priority when an agent is deciding which tool to use for a given task: +### `donna` + +We use Donna to develop Donna. We use the current development version of Donna. For convinience, use shortcut `./bin/donna.sh` to run current development version of Donna. + +Use Donna to run workflows. + +You may need to read the usage intructions for `donna`: `./bin/donna.sh -p llm skill usage` in these cases: + +- You need to run a workflow first time in the session. +- You need to list available workflows first time in the session. + +You run workflows only when explicitly instructed to do so by a developer or Donna itself. You MUST run workflows in that cases. + +Donna is configured to log significant operation steps via `task` tool. + ### `ast-grep` `ast-grep` — a tool for searching and manipulating Abstract Syntax Trees in code. Use it when you work with particular code patterns, structures, or constructs in the codebase. @@ -81,3 +151,15 @@ You MUST NOT log: - CLI commands you execute. - Elementary or trivial steps. + +You can read the logged journal with: + +```bash +./bin/journal-tail.py --lines 20 +``` + +### `rg` + +Use `rg` for text and file searches unless a structural code query is needed. + +`ast-grep` has a higher priority than `rg` whenever a structural code query is needed. diff --git a/bin/journal-follow.py b/bin/journal-tail.py similarity index 93% rename from bin/journal-follow.py rename to bin/journal-tail.py index 9b1e5c59..b25dafb8 100755 --- a/bin/journal-follow.py +++ b/bin/journal-tail.py @@ -16,9 +16,10 @@ def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Follow project journal records.") + parser = argparse.ArgumentParser(description="Output project journal records.") parser.add_argument("-n", "--lines", type=int, default=20, help="number of existing records to show") - parser.add_argument("-i", "--interval", type=float, default=1.0, help="poll interval in seconds") + parser.add_argument("-f", "--follow", action="store_true", help="follow new records after output") + parser.add_argument("-i", "--interval", type=float, default=1.0, help="poll interval in seconds when following") return parser.parse_args() @@ -115,6 +116,9 @@ def main() -> int: if args.lines > 0: print_records(records[-args.lines :], formatter) + if not args.follow: + return 0 + while True: time.sleep(args.interval) From 147e13e372e1c80f73fa589eeba86b347dfc57eb Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 16 May 2026 17:57:40 +0200 Subject: [PATCH 02/98] specs -> old specs --- {specs => old_specs}/core/error_handling.md | 0 {specs => old_specs}/core/top_level_architecture.md | 0 {specs => old_specs}/intro.md | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {specs => old_specs}/core/error_handling.md (100%) rename {specs => old_specs}/core/top_level_architecture.md (100%) rename {specs => old_specs}/intro.md (100%) diff --git a/specs/core/error_handling.md b/old_specs/core/error_handling.md similarity index 100% rename from specs/core/error_handling.md rename to old_specs/core/error_handling.md diff --git a/specs/core/top_level_architecture.md b/old_specs/core/top_level_architecture.md similarity index 100% rename from specs/core/top_level_architecture.md rename to old_specs/core/top_level_architecture.md diff --git a/specs/intro.md b/old_specs/intro.md similarity index 100% rename from specs/intro.md rename to old_specs/intro.md From c3669aa03772b9a0f7fb1634fbca234f1e939d3b Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 16 May 2026 19:23:15 +0200 Subject: [PATCH 03/98] usage.md --- README.md | 17 +- donna/cli/commands/sessions.py | 12 +- donna/machine/errors.py | 4 +- donna/machine/sessions.py | 27 ++- donna/protocol/formatters/automation.py | 7 +- donna/protocol/utils.py | 5 +- donna/skills/entities.py | 2 +- donna/skills/fixtures.py | 2 +- donna/skills/fixtures/artifacts.md | 142 ------------ donna/skills/fixtures/usage.md | 263 +++++++++++++++++----- donna/skills/fixtures/workflows.md | 131 +++++++++++ workflows/rfc/design.donna.md | 2 +- workflows/rfc/plan.donna.md | 2 +- workflows/rfc/request.donna.md | 2 +- workflows/rfc/specs/design.md | 2 +- workflows/rfc/specs/request_for_change.md | 2 +- 16 files changed, 379 insertions(+), 243 deletions(-) delete mode 100644 donna/skills/fixtures/artifacts.md create mode 100644 donna/skills/fixtures/workflows.md diff --git a/README.md b/README.md index 9c2816a9..039cfd2f 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ Donna will create `donna.toml` in your project root. The configured session dire ## Skills - `donna-do` — use Donna to perform a specific task in the current Donna session. Creates a new session if there is no one. -- `donna-start` — start a new Donna session and tell the agent to use Donna to perform all further work. Removes all content from the previous session. +- `donna-start` — create new Donna session state and tell the agent to use Donna to perform all further work. - `donna-stop` — stop using Donna to perform work — the agent should switch to its own flow control. ## Usage @@ -157,7 +157,7 @@ Donna will create `donna.toml` in your project root. The configured session dire Commands you may need: - `donna init` — Initialize Donna in your project. -- `donna start` — start a new working session, remove everything from the previous session. +- `donna new-session` — create fresh session state. - `donna list` — list workflows with short descriptions. Donna can send internal journal records to a third-party tool. Configure it in `donna.toml`: @@ -187,7 +187,7 @@ Use `donna --help` for a quick reference. You find detailed documentation in the built-in skill documents — they are readable and always accurate: - `donna skill usage` — full list of commands and how to use them. -- `donna skill artifacts` — what Donna artifacts are and how to use them on the filesystem. +- `donna skill workflows` — what Donna workflows are and how to use them on the filesystem. - `donna skill configuration` — how to configure `donna.toml`. - `donna skill initialization` — how to initialize or refresh a Donna workspace. @@ -260,10 +260,11 @@ You can find all workflows with `donna -p llm list`. `/.session/donna/` contains the current state of work performed by Donna: runtime state plus temporary documents and workflows created during the session. -The developer is responsible for starting or resetting sessions with `donna -p human start` and `donna -p human reset`. +Donna initializes empty session state automatically when a session-aware command needs it and no session state exists. -- On session start, Donna removes everything from the previous session and creates a fresh session directory. -- On session reset donna resets the state of the current session (tasks, action requests, etc.), but keeps artifacts. +The developer can explicitly reset the current session state with `donna -p human new-session`. + +- `new-session` resets tasks, work units, and action requests. The agent is encouraged not to manage sessions directly, because it doesn't have enough context to decide when session artifacts may be safely removed. @@ -288,7 +289,7 @@ To execute a workflow, Donna uses a simplified virtual machine (VM) that maintai ### Operations -You can find detailed docs on built-in operations in `donna skill artifacts`. +You can find detailed docs on built-in operations in `donna skill workflows`. Here is a short list of them: @@ -363,7 +364,7 @@ Donna defines a set of built-in Jinja2 functions that provide artifacts with the Directives are used in the next way: `{{ python.import.path() }}`. -You can find detailed documentation of all built-in directives in `donna skill artifacts`. +You can find detailed documentation of all built-in directives in `donna skill workflows`. Here they are: diff --git a/donna/cli/commands/sessions.py b/donna/cli/commands/sessions.py index 6d8eb31a..75d179b8 100644 --- a/donna/cli/commands/sessions.py +++ b/donna/cli/commands/sessions.py @@ -12,16 +12,10 @@ from donna.machine import sessions -@app.command(help="Start a new session, reset session state, remove all session artifacts.") -def start(context: typer.Context) -> None: +@app.command(name="new-session", help="Create fresh session state.") +def new_session(context: typer.Context) -> None: with command_context(context) as command: - command.write_cells(sessions.start().unwrap()) - - -@app.command(help="Reset the current session state, keeps session artifacts.") -def reset(context: typer.Context) -> None: - with command_context(context) as command: - command.write_cells(sessions.reset().unwrap()) + command.write_cells(sessions.new_session().unwrap()) @app.command( diff --git a/donna/machine/errors.py b/donna/machine/errors.py index 68c0d6b0..a47a6413 100644 --- a/donna/machine/errors.py +++ b/donna/machine/errors.py @@ -31,7 +31,7 @@ class EnvironmentError(core_errors.EnvironmentError): class SessionStateNotInitialized(EnvironmentError): code: str = "donna.machine.session_state_not_initialized" message: str = "Session state is not initialized." - ways_to_fix: list[str] = ["Run Donna session start to initialize session state."] + ways_to_fix: list[str] = ["Run `donna new-session` to create fresh session state."] class SessionStateChangedExternally(EnvironmentError): @@ -55,7 +55,7 @@ class JournalMessageContainsNewlines(EnvironmentError): class ActionRequestNotFound(EnvironmentError): code: str = "donna.machine.action_request_not_found" message: str = "Action request `{error.request_id}` was not found in the current session state." - ways_to_fix: list[str] = ["Use an action request id from `sessions details` output."] + ways_to_fix: list[str] = ["Use an action request id from `donna details` output."] request_id: ActionRequestId diff --git a/donna/machine/sessions.py b/donna/machine/sessions.py index 41c08b0c..b62624c4 100644 --- a/donna/machine/sessions.py +++ b/donna/machine/sessions.py @@ -18,7 +18,18 @@ @unwrap_to_error def load_state() -> Result[ConsistentState, ErrorsList]: - return Ok(context().state.load().unwrap()) + loaded = context().state.load() + + if loaded.is_ok(): + return Ok(loaded.unwrap()) + + errors = loaded.unwrap_err() + if not any(isinstance(error, machine_errors.SessionStateNotInitialized) for error in errors): + return Err(errors) + + state = MutableState.build().freeze() + context().state.save(state).unwrap() + return Ok(state) @unwrap_to_error @@ -60,20 +71,12 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> CellsResult: @unwrap_to_error -def start() -> Result[list[Cell], ErrorsList]: - workspace_sessions.reset_dir() - +def new_session() -> Result[list[Cell], ErrorsList]: _save_state(MutableState.build().freeze()).unwrap() - machine_journal.add(message="Started new session.").unwrap() - - return Ok([operation_succeeded("Started new session.")]) - + machine_journal.add(message="Created new session state.").unwrap() -@unwrap_to_error -def reset() -> Result[list[Cell], ErrorsList]: - _save_state(MutableState.build().freeze()).unwrap() - return Ok([operation_succeeded("Session state reset.")]) + return Ok([operation_succeeded("Created new session state.")]) @unwrap_to_error diff --git a/donna/protocol/formatters/automation.py b/donna/protocol/formatters/automation.py index 8f6bb6ff..39ca6406 100644 --- a/donna/protocol/formatters/automation.py +++ b/donna/protocol/formatters/automation.py @@ -7,6 +7,9 @@ class Formatter(BaseFormatter): + def _json_line(self, data: object) -> bytes: + return json.dumps(data, ensure_ascii=False, indent=None, separators=(",", ":"), sort_keys=True).encode() + b"\n" + def format_cell(self, cell: Cell) -> bytes: data: dict[str, str | int | bool | None] = {"id": cell.short_id} @@ -15,7 +18,7 @@ def format_cell(self, cell: Cell) -> bytes: data["content"] = cell.content.strip() if cell.content else None - return json.dumps(data, ensure_ascii=False, indent=None, separators=(",", ":"), sort_keys=True).encode() + return self._json_line(data) def format_journal(self, record: JournalRecord) -> bytes: - return serialize_record(record) + return serialize_record(record) + b"\n" diff --git a/donna/protocol/utils.py b/donna/protocol/utils.py index 648f3da0..6cddb0d1 100644 --- a/donna/protocol/utils.py +++ b/donna/protocol/utils.py @@ -6,7 +6,10 @@ def instant_output(text: bytes) -> None: - sys.stdout.buffer.write(text + b"\n") + if text.endswith(b"\n"): + sys.stdout.buffer.write(text) + else: + sys.stdout.buffer.write(text + b"\n") sys.stdout.buffer.flush() diff --git a/donna/skills/entities.py b/donna/skills/entities.py index beec4356..f8abf8de 100644 --- a/donna/skills/entities.py +++ b/donna/skills/entities.py @@ -5,4 +5,4 @@ class SkillDocument(enum.StrEnum): usage = "usage" configuration = "configuration" initialization = "initialization" - artifacts = "artifacts" + workflows = "workflows" diff --git a/donna/skills/fixtures.py b/donna/skills/fixtures.py index ae89ecc7..c8b89d6a 100644 --- a/donna/skills/fixtures.py +++ b/donna/skills/fixtures.py @@ -6,7 +6,7 @@ SkillDocument.usage: "usage.md", SkillDocument.configuration: "configuration.md", SkillDocument.initialization: "initialization.md", - SkillDocument.artifacts: "artifacts.md", + SkillDocument.workflows: "workflows.md", } diff --git a/donna/skills/fixtures/artifacts.md b/donna/skills/fixtures/artifacts.md deleted file mode 100644 index 00d48531..00000000 --- a/donna/skills/fixtures/artifacts.md +++ /dev/null @@ -1,142 +0,0 @@ -# `donna` Artifacts - -Donna artifacts are project files that Donna can discover, render, validate, and execute as workflow input. They are usually Markdown files with the `.donna.md` extension. - -Donna reads artifacts from the project filesystem. It does not mutate project artifacts through artifact commands. Developers and agents edit files directly, then ask Donna to list workflows or validate artifacts. - -## Artifact Locations - -The common artifact areas are: - -- `/workflows`: project-owned workflows. -- `/.agents/donna`: project-local Donna documentation, when present. -- `/.session/donna`: session artifacts and active workflow state. - -Example: - -```text -workflows/polish.donna.md -workflows/rfc/request.donna.md -.session/donna/current_task.donna.md -``` - -Donna sees only `.donna.md` files under directories listed in `donna.toml:workflow_dirs`. - -## List Workflows - -List workflow artifacts: - -```bash -donna -p llm list -``` - -Read artifact source files directly when you need details beyond the workflow introduction. - -## Validate Artifacts - -Validate one artifact: - -```bash -donna -p llm validate '@/workflows/polish.donna.md' -``` - -Validate all visible artifacts: - -```bash -donna -p llm validate --all -``` - -Run validation after creating or editing Donna artifacts. - -## Artifact IDs - -Use `@/` for project-root artifact ids: - -```bash -donna -p llm validate '@/workflows/polish.donna.md' -``` - -Validate multiple specific artifacts by passing multiple ids: - -```bash -donna -p llm validate '@/workflows/polish.donna.md' './workflows/rfc/request.donna.md' -``` - -Artifact path arguments accept root-anchored paths such as `@/workflows/polish.donna.md`, relative paths such as `./workflows/polish.donna.md`, and absolute paths inside the project root. - -## Creating Artifacts - -Create normal documentation as plain Markdown files. A minimal documentation file: - -````markdown -# Example Specification - -This document describes one stable project rule. -```` - -Documentation files are not Donna artifacts and do not need artifact validation. - -## Creating Workflows - -A workflow artifact defines a finite-state machine. The H1 section is a workflow by default. Each H2 section declares one operation. When the H1 config omits `start_operation_id`, Donna starts from the first H2 section. - -Minimal workflow: - -````markdown -# Example Workflow - -This workflow asks the agent to do one thing and finish. - -## Do The Work - -```toml donna -id = "ask_agent" -kind = "donna.lib.request_action" -``` - -Perform the requested change. - -When done, continue with `{{ donna.lib.goto("finish") }}`. - -## Finish - -```toml donna -id = "finish" -kind = "donna.lib.finish" -``` - -The workflow is complete. -```` - -Validate the workflow before running it: - -```bash -donna -p llm validate '@/workflows/example.donna.md' -``` - -Run it: - -```bash -donna -p llm run '@/workflows/example.donna.md' -``` - -## Managing Artifacts - -Use direct file edits to create, update, move, or delete artifact files. Then use Donna to inspect the result. - -Recommended loop: - -1. Edit the artifact source file. -2. Validate the artifact: - -```bash -donna -p llm validate '@/specs/example.donna.md' -``` - -3. If it is a workflow, list it: - -```bash -donna -p llm list -``` - -Keep artifact files concise. Put project-wide explanations in specifications and operational step-by-step instructions in workflows. diff --git a/donna/skills/fixtures/usage.md b/donna/skills/fixtures/usage.md index 1146b976..6c3c0bfc 100644 --- a/donna/skills/fixtures/usage.md +++ b/donna/skills/fixtures/usage.md @@ -1,169 +1,310 @@ # `donna` Usage -Donna is a CLI tool for orchestrating AI-agent work with project-local workflows, artifacts, and session state. +`donna` is a command line tool that helps agents run predefined workflows in a deterministic way. -Use this document as the first reference for command usage. For narrower topics, use: +Treat Donna as a small workflow virtual machine for agents. A workflow is a state machine stored in project-local Donna artifacts. Donna maintains the active session state, runs workflow operations, asks the agent to apply judgment or perform work when fully deterministic execution is not enough, and waits for the agent to report which operation to run next. -- `donna skill configuration` for `donna.toml`. -- `donna skill initialization` for creating or checking Donna project files. -- `donna skill artifacts` for artifact layout, discovery, and authoring rules. -- `donna skill usage` for this command overview. +Donna does not replace the agent. The agent is still responsible for reading project instructions, doing the requested work, running tools, making code changes, and reporting results. Donna serves the control-flow role: it keeps the workflow path explicit, validated, resumable, and less dependent on the agent remembering every process step. -## Project Root - -Donna works inside a project root. If `--root/-r` is omitted, commands that load a project discover the project root by searching upward from the current directory for `donna.toml`. +## This Documentation -Use `--root PATH` when running Donna from outside the project tree or when targeting a specific project: +This output is built-in skill-style documentation printed by: ```bash -donna -p llm --root /path/to/project status +donna -p llm skill usage ``` -`donna skill ...` does not load a project config and can run from any directory. +Use it as the first reference for agent-side command usage in a session. Use the other built-in skill documents for narrower tasks: + +- `donna -p llm skill configuration` explains `donna.toml`. +- `donna -p llm skill initialization` explains project initialization. +- `donna -p llm skill workflows` explains Donna workflow format, layout, execution and creation best practices. +- `donna -p llm skill usage` prints this document. ## Output Protocols Donna supports three protocol modes: -- `llm`: structured cells optimized for agents. -- `human`: compact terminal output for people. -- `automation`: output intended for programs. +- `llm`: structured cell output intended for agents. +- `human`: compact terminal output intended for people. +- `automation`: JSON Lines output intended for programs. + +Use `llm` when invoking `donna` as a coding agent. It is the normal choice for this documentation's examples. + +Use `human` for compact terminal inspection by a person. + +Use `automation` when an agent or another program needs automatic processing of Donna output. Automation output is JSON Lines: each stdout line is one JSON object representing one Donna output cell or journal record. -Agents should use `-p llm` for normal Donna workflow commands: +Example automation command: ```bash -donna -p llm status +donna -p automation list +``` + +Example automation output: + +```jsonl +{"artifact_id":"@/workflows/polish.donna.md","artifact_kind":"donna.lib.workflow","artifact_title":"Polishing Workflow","content":"Initiate operations to polish and refine the codebase.","id":"WxVlGyfwTs-vnaTh6c46ww"} +{"artifact_id":"@/workflows/rfc/request.donna.md","artifact_kind":"donna.lib.workflow","artifact_title":"Create a Request for Change","content":"This workflow creates a Request for Change document.","id":"gPY_FHlISKu7HOphsyj-kQ"} +``` + +Global options go before the subcommand: + +```bash +donna -p llm --root /path/to/project status ``` -The root option goes before the command: +## Project Root + +Most commands need a Donna project. If `--root/-r` is omitted, Donna discovers the project root by searching upward from the current working directory for `donna.toml`. + +Use `--root PATH` when running from outside the intended project tree or when there is any ambiguity: ```bash donna -p llm --root /path/to/project list ``` -## Skill Documents +Donna uses project-root anchored ids for workflow artifacts. The `@/` prefix means "from the Donna project root": + +```bash +donna -p llm run @/workflows/some-workflow.donna.md +``` -The `skill` command prints built-in agent documentation as plain Markdown. It does not require an initialized Donna project. +Relative artifact paths passed to CLI commands are resolved from the process current working directory, then normalized to project-root anchored ids. Absolute artifact paths are accepted only when they point inside the Donna project root. Prefer `@/` paths in agent notes and workflow instructions because they are independent of the current working directory. -Examples: +Artifact section ids append `:section_id` to an artifact id: ```bash -donna skill usage -donna skill configuration -donna skill initialization -donna skill artifacts +donna -p llm complete-action-request AR-12-x @/.session/donna/workflow.donna.md:next_step ``` -Use these documents when an agent needs stable instructions before `donna.toml` exists or when synced artifacts are not available. +`donna skill ...` and `donna init` do not require an existing Donna project config. -## Project Commands +## Agent Safety Rules -Workspace commands create or check Donna project configuration. +1. Read the project's own agent instructions before using Donna. +2. Use `-p llm` unless a human explicitly asks for human output or a program needs automation output. +3. Run `status` before deciding what to do with a session. +4. If Donna says the session is awaiting your action, address the pending action requests before unrelated work. +5. If the developer asks for new work while Donna has pending work units or action requests, ask whether to continue the current session or start a new one. +6. Run workflows only when explicitly instructed by the developer, project instructions, or Donna. +7. Use the action request id and next operation id exactly as Donna provides them. Do not invent operation ids. +8. Do not run `new-session` unless you understand the consequence and the developer or project instructions allow it. -Initialize Donna in the current directory: +## Usage Patterns + +### Workflow Execution + +Workflow execution means Donna drives operations until the workflow finishes, reports an error, or asks the agent to perform work. The command that starts workflow execution is: ```bash -donna -p llm init +donna -p llm run @/path/to/workflows/.donna.md ``` -Initialize Donna in an explicit existing directory: +`run` starts the workflow in the current session. Donna then executes workflow operations until it needs agent work, finishes, or reports an error. Some operations may take time because they run scripts, validators, formatters, tests, or other tools. Wait for the command to finish and read all emitted cells before deciding what to do next. + +When Donna emits an action request, perform the requested work exactly. Action requests include an id and allowed next operation choices. After finishing the requested work, report the action request completion with the id and next operation that matches the result: ```bash -donna -p llm --root /path/to/project init +donna -p llm complete-action-request AR-12-x @/.session/donna/workflow.donna.md:next_step +``` + +`complete-action-request` removes the action request, queues the selected next operation, and continues workflow execution immediately. It may finish the workflow, emit another action request, or run more deterministic operations before returning. + +The workflow is finished when Donna emits the workflow's finish message, when `status` says the session is idle with no active tasks, or when Donna explicitly tells you to report back: + +```bash +donna -p llm status +``` + +When the workflow is finished, report the result to the developer before starting unrelated work. + +### Start Workflow + +A developer or project instruction explicitly asks you to run a Donna workflow, and Donna has no active work that must be continued first. + +Read project instructions and this usage document if you have not already: + +```bash +donna -p llm skill usage +``` + +Inspect the current session before starting anything: + +```bash +donna -p llm status +``` + +If Donna reports pending work units or action requests, do not start unrelated work silently. Ask whether to continue the current session or start a new one. + +List available workflows if you have not been given a specific workflow to run: + +```bash +donna -p llm list +``` + +Run the selected workflow, then follow the workflow execution rules above: + +```bash +donna -p llm run @/path/to/workflows/.donna.md +``` + +### Continue Workflow + +Donna has queued work units, pending action requests, a workflow command tells you to continue, or the developer asks you to continue the current Donna workflow. + +Inspect the current session: + +```bash +donna -p llm status +``` + +Continue queued workflow execution: + +```bash +donna -p llm continue +``` + +Strictly follow every instruction Donna gives you after that. If Donna finishes the workflow or tells you to report back, do so before doing anything else. + +### Creating New Workflow + +A developer or an active Donna workflow asks you to create or modify a workflow. + +Read the workflow authoring instructions if you have not already: + +```bash +donna -p llm skill workflows +``` + +Create or edit the workflow source file directly. Choose the target location by this priority: + +1. Use the explicitly specified filepath when the developer, project instructions, or parent workflow provides one. +2. For a temporary workflow, use a directory configured for temporary files or session artifacts. +3. For a temporary workflow if no directory for temporary files or session artifacts is configured, use the donna session directory, you can find it in `donna.toml`. +4. For a permanent workflow, use one of the directories where project-owned workflows are stored. Check `donna.toml` when the project-owned workflow directories are not obvious. + +Validate the workflow: + +```bash +donna -p llm validate @/workflows/example.donna.md +``` + +If the workflow should be discoverable, list workflows and confirm it appears with the expected summary: + +```bash +donna -p llm list ``` ## Session Commands -All workflow execution happens in the active session. Session state lives under the configured session directory, `.session/donna` by default. +All workflow execution happens in the active session. Session state and session-created artifacts live under the configured session directory. + +Donna automatically creates empty session state when a session-aware command needs it and no session state exists. -Start a new session: +Create a fresh session: ```bash -donna -p llm start +donna -p llm new-session ``` -Starting a session resets session state and removes session artifacts. Only start a new session when the developer asks for it or when no active session exists. +`new-session` creates or **resets** Donna session state. Use it only when starting fresh work is intended. -Show concise status: +Show concise session status, including whether Donna is idle, has queued work units, or is awaiting action: ```bash donna -p llm status ``` -Show detailed session state and action requests: +Show detailed session state and pending action requests: ```bash donna -p llm details ``` -Continue queued workflow execution: +Continue queued workflow execution and emit the next action request if a workflow reaches one: ```bash donna -p llm continue ``` -Run a workflow artifact: +Run a workflow artifact in the current session: ```bash donna -p llm run @/workflows/polish.donna.md ``` -Complete an action request by passing its id and the next operation id exactly as Donna instructed: +Complete an action request: ```bash donna -p llm complete-action-request AR-12-x @/.session/donna/workflow.donna.md:next_step ``` -## Artifact Commands +The first argument is the action request id. The second argument is an artifact section id in `artifact:section` form. Prefer copying the exact completion command or next operation id from Donna's action request output. + +## Workflow Commands -Artifacts are `*.donna.md` project files under Donna's configured `workflow_dirs`. Agents use artifacts to discover workflows, read documentation, and validate Donna-readable files. +Donna workflows are `*.donna.md` files discovered under the configured `workflow_dirs`. Workflow ids are project-root anchored paths such as `@/workflows/polish.donna.md`. Section ids append `:section_id`, for example `@/workflows/polish.donna.md:finish`. -List workflows: +List available workflows and their summaries: ```bash donna -p llm list ``` -Validate all visible artifacts: +Render a workflow to debug how Donna sees it: + +```bash +donna -p llm render @/workflows/polish.donna.md --mode view +``` + +Use `render` when a workflow does not validate, an operation displays unexpected instructions, or a directive does not behave as expected. + +Render modes: + +- `view`: Use to see how Donna will render workflow instructions to the agent. This is the safest mode for inspecting the workflow text and generated agent-facing instructions. +- `execute`: Use when debugging the exact operation text Donna would execute in the current session context. This mode may fail when the workflow references data that is expected to exist on the current session stack. +- `analysis`: Use when inspecting machine-readable directive behavior, workflow transitions, and validation-related rendering. + +Validate all discovered workflows: ```bash donna -p llm validate --all ``` -Validate specific artifacts by project-root or relative path: +Validate specific workflows: ```bash -donna -p llm validate '@/workflows/polish.donna.md' +donna -p llm validate @/workflows/polish.donna.md ``` -## Normal Agent Flow +Workflow arguments accept root-anchored ids, relative paths, and absolute paths inside the project root. Prefer root-anchored ids in notes and agent instructions because they remain stable when the current directory changes. -1. Read project instructions and `donna skill usage`. -2. Check session state: +## Project Commands + +Initialize Donna in the current directory: ```bash -donna -p llm status +donna -p llm init ``` -3. If there is no active work and a workflow is needed, list workflows: +Initialize Donna in an explicit directory: ```bash -donna -p llm list +donna -p llm --root /path/to/project init ``` -4. Start the selected workflow: +`init` creates or refreshes Donna project configuration. Read `donna -p llm skill initialization` and project instructions before using it in an existing repository. + +Print the installed Donna package version: ```bash -donna -p llm run @/workflows/polish.donna.md +donna version ``` -5. Execute Donna action requests exactly. -6. Report completion with `complete-action-request`. -7. Continue until Donna finishes the workflow. - ## Journal Forwarding -Donna creates internal journal records for workflow events. To forward them to another tool, configure `[journal].cmd` in `/donna.toml`. +Donna creates internal journal records for significant workflow events. Projects can forward those records to another tool by configuring `journal.cmd` in `donna.toml`. Example: @@ -173,3 +314,5 @@ cmd = ["./bin/taskwarior.sh", "log", "+journal", "+donna", "{message}"] ``` Supported placeholders are `timestamp`, `actor_id`, `message`, `current_task_id`, `current_work_unit_id`, and `current_operation_id`. + +When no journal command is configured, Donna still prints newly created internal journal records through the selected protocol formatter. diff --git a/donna/skills/fixtures/workflows.md b/donna/skills/fixtures/workflows.md new file mode 100644 index 00000000..1aab35a4 --- /dev/null +++ b/donna/skills/fixtures/workflows.md @@ -0,0 +1,131 @@ +# `donna` Workflows + +Donna workflows are project files that Donna can discover, render, validate, and execute. They are usually Markdown files with the `.donna.md` extension. + +Donna reads workflows from the project filesystem. It does not mutate project workflow files through workflow commands. Developers and agents edit files directly, then ask Donna to list workflows or validate them. + +## Workflow Locations + +The common workflow areas are: + +- `/workflows`: project-owned workflows. +- `/.session/donna`: session workflows and active workflow state. + +Project-local Donna documentation may also live under `/.agents/donna` when present. + +Example: + +```text +workflows/polish.donna.md +workflows/rfc/request.donna.md +.session/donna/current_task.donna.md +``` + +Donna sees only `.donna.md` files under directories listed in `donna.toml:workflow_dirs`. + +## List Workflows + +List workflows: + +```bash +donna -p llm list +``` + +Read workflow source files directly when you need details beyond the workflow introduction. + +## Validate Workflows + +Validate one workflow: + +```bash +donna -p llm validate '@/workflows/polish.donna.md' +``` + +Validate all visible workflows: + +```bash +donna -p llm validate --all +``` + +Run validation after creating or editing Donna workflows. + +## Workflow IDs + +Use `@/` for project-root workflow ids: + +```bash +donna -p llm validate '@/workflows/polish.donna.md' +``` + +Validate multiple specific workflows by passing multiple ids: + +```bash +donna -p llm validate '@/workflows/polish.donna.md' './workflows/rfc/request.donna.md' +``` + +Workflow path arguments accept root-anchored paths such as `@/workflows/polish.donna.md`, relative paths such as `./workflows/polish.donna.md`, and absolute paths inside the project root. + +## Creating Workflows + +A workflow defines a finite-state machine. The H1 section is a workflow by default. Each H2 section declares one operation. When the H1 config omits `start_operation_id`, Donna starts from the first H2 section. + +Minimal workflow: + +````markdown +# Example Workflow + +This workflow asks the agent to do one thing and finish. + +## Do The Work + +```toml donna +id = "ask_agent" +kind = "donna.lib.request_action" +``` + +Perform the requested change. + +When done, continue with `{{ donna.lib.goto("finish") }}`. + +## Finish + +```toml donna +id = "finish" +kind = "donna.lib.finish" +``` + +The workflow is complete. +```` + +Validate the workflow before running it: + +```bash +donna -p llm validate '@/workflows/example.donna.md' +``` + +Run it: + +```bash +donna -p llm run '@/workflows/example.donna.md' +``` + +## Managing Workflows + +Use direct file edits to create, update, move, or delete workflow files. Then use Donna to inspect the result. + +Recommended loop: + +1. Edit the workflow source file. +2. Validate the workflow: + +```bash +donna -p llm validate '@/workflows/example.donna.md' +``` + +3. List it: + +```bash +donna -p llm list +``` + +Keep workflow files concise. Put project-wide explanations in specifications and operational step-by-step instructions in workflows. diff --git a/workflows/rfc/design.donna.md b/workflows/rfc/design.donna.md index 40370f0b..55ef9996 100644 --- a/workflows/rfc/design.donna.md +++ b/workflows/rfc/design.donna.md @@ -11,7 +11,7 @@ fsm_mode = "start" ``` 1. Read the `workflows/rfc/specs/design.md` file if you haven't done it yet. -2. Read the artifact instructions by running `donna skill artifacts` if you haven't done it yet. +2. Read the workflow instructions by running `donna skill workflows` if you haven't done it yet. 3. `{{ donna.lib.goto("ensure_rfc_artifact_exists") }}` ## Ensure RFC artifact exists diff --git a/workflows/rfc/plan.donna.md b/workflows/rfc/plan.donna.md index d5eb3e39..59fbfae4 100644 --- a/workflows/rfc/plan.donna.md +++ b/workflows/rfc/plan.donna.md @@ -13,7 +13,7 @@ fsm_mode = "start" 1. Read the Design document that the developer or parent workflow wants you to implement. 2. Read the RFC document that the developer or parent workflow wants you to implement, if it exists. -3. Read the artifact instructions by running `donna skill artifacts` if you haven't done it yet. +3. Read the workflow instructions by running `donna skill workflows` if you haven't done it yet. 4. `{{ donna.lib.goto("prepare_workflow_artifact") }}` ## Prepare workflow artifact diff --git a/workflows/rfc/request.donna.md b/workflows/rfc/request.donna.md index 26409b8f..8c1caba0 100644 --- a/workflows/rfc/request.donna.md +++ b/workflows/rfc/request.donna.md @@ -12,7 +12,7 @@ fsm_mode = "start" ``` 1. Read the `workflows/rfc/specs/request_for_change.md` file if you haven't done it yet. -2. Read the artifact instructions by running `donna skill artifacts` if you haven't done it yet. +2. Read the workflow instructions by running `donna skill workflows` if you haven't done it yet. 3. `{{ donna.lib.goto("ensure_work_description_exists") }}` ## Ensure work description exists diff --git a/workflows/rfc/specs/design.md b/workflows/rfc/specs/design.md index 56ba5d97..4075b415 100644 --- a/workflows/rfc/specs/design.md +++ b/workflows/rfc/specs/design.md @@ -36,7 +36,7 @@ The RFC document is a Markdown file with the next structure: ## General language and format - You MUST follow [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119.txt) for keywords like MUST, SHOULD, MAY, etc. -- You MUST follow `donna skill artifacts`. +- You MUST follow `donna skill workflows`. - You MUST follow the structure specified in this document. ### List format diff --git a/workflows/rfc/specs/request_for_change.md b/workflows/rfc/specs/request_for_change.md index f5f93355..4e34fac5 100644 --- a/workflows/rfc/specs/request_for_change.md +++ b/workflows/rfc/specs/request_for_change.md @@ -30,7 +30,7 @@ The RFC document is a Markdown file with the next structure: ## General language and format - You MUST follow [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119.txt) for keywords like MUST, SHOULD, MAY, etc. -- You MUST follow `donna skill artifacts`. +- You MUST follow `donna skill workflows`. - You MUST follow the structure specified in this document. ### List format From bd09d6d3c4d404838a6d85a8f2171a60f728d51a Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 16 May 2026 19:38:23 +0200 Subject: [PATCH 04/98] config version --- donna.toml | 2 + donna/skills/fixtures/configuration.md | 227 ++++++++++++++++----- donna/skills/fixtures/initialization.md | 2 +- donna/workspaces/config.py | 3 +- donna/workspaces/fixtures/base_config.toml | 1 + 5 files changed, 184 insertions(+), 51 deletions(-) diff --git a/donna.toml b/donna.toml index ddc6ee44..92256919 100644 --- a/donna.toml +++ b/donna.toml @@ -1,3 +1,5 @@ +version = 1 + session_dir = ".session/donna" default_section_kind = "donna.lib.text" default_primary_section_kind = "donna.lib.workflow" diff --git a/donna/skills/fixtures/configuration.md b/donna/skills/fixtures/configuration.md index 9a2b47f4..b22e8f82 100644 --- a/donna/skills/fixtures/configuration.md +++ b/donna/skills/fixtures/configuration.md @@ -1,18 +1,43 @@ # `donna` Configuration -Donna project configuration lives at: +`donna.toml` tells `donna` where a project's workflow virtual machine stores state, where it discovers workflow artifacts, which default section primitives to use, and how to forward workflow journal records. -```text -/donna.toml +Donna is a CLI tool for agents. It keeps high-level control flow in explicit workflows so an agent can focus on the concrete work requested by each operation. A workflow is a state machine stored in a project-local `.donna.md` artifact. Donna maintains the active session state, the workflow stack, queued work units, and action requests. + +The configuration file has two main parts: + +- Top-level workspace settings configure session storage, artifact discovery, and default artifact section behavior. +- `journal` configures optional forwarding of Donna journal records to an external command. + +The configuration file is TOML with schema version `1`. The presence of `donna.toml` marks a Donna project root. + +Minimal file: + +```toml +version = 1 ``` -The file is created by `donna -p llm init`. Edit it when the project needs workflow source directories, default section settings, or journal forwarding. +If `version` is omitted, Donna treats the configuration as schema version `1`. -## Minimal Configuration +The generated file includes comments around this shape: -A default project can use the generated configuration without manual edits. The effective defaults are: +```toml +version = 1 + +session_dir = ".session/donna" + +workflow_dirs = [ + "./workflows", + "./.session/donna", +] + +[journal] +``` + +Effective defaults: ```toml +version = 1 session_dir = ".session/donna" default_section_kind = "donna.lib.text" default_primary_section_kind = "donna.lib.workflow" @@ -22,75 +47,128 @@ workflow_dirs = ["./workflows", "./.session/donna"] [journal] ``` -## Session Directory +Top-level fields: -`session_dir` points to Donna's temporary session directory. -Donna stores runtime state, action requests, and session-created artifacts there. +- `version`: optional integer, currently `1`. +- `session_dir`: optional relative path to Donna's session directory. +- `default_section_kind`: optional Python path to the primitive used for non-primary sections without an explicit `kind`. +- `default_primary_section_kind`: optional Python path to the primitive used for the primary H1 section without an explicit `kind`. +- `default_primary_section_id`: optional section id assigned to the primary H1 section when it omits `id`. +- `workflow_dirs`: optional list of relative directories scanned for `.donna.md` workflow artifacts. +- `journal`: optional journal forwarding config. -Default: +Unknown top-level fields are invalid. + +## Version + +`version` declares the Donna configuration schema version. ```toml -session_dir = ".session/donna" +version = 1 ``` -Relative paths are resolved from the project root; absolute paths are used as configured. Use a directory ignored by version control unless a project intentionally tracks session artifacts. +Fields: -## Default Sections +- `version`: optional integer, currently `1`. + +If `version` is omitted, Donna treats the configuration as schema version `1`. -Donna loads artifacts only from `*.donna.md` Markdown files. +If `version` is present, it must be an integer. + +If `version` is not supported, configuration loading fails. + +## Session Directory -When a non-primary section omits `kind`, Donna uses `default_section_kind`. When the primary section omits `kind`, Donna uses `default_primary_section_kind`. When the primary section omits `id`, Donna uses `default_primary_section_id`. +`session_dir` points to Donna's temporary session directory. Donna stores runtime state, action requests, and session-created artifacts there. ```toml -default_section_kind = "donna.lib.text" -default_primary_section_kind = "donna.lib.workflow" -default_primary_section_id = "primary" +session_dir = ".session/donna" ``` Fields: -- `default_section_kind`: full Python path to the primitive used for sections without an explicit `kind`. -- `default_primary_section_kind`: full Python path to the primitive used for the primary H1 section without an explicit `kind`. -- `default_primary_section_id`: section id assigned to the primary H1 section when it omits `id`. +- `session_dir`: optional relative project path, default `.session/donna`. + +The path is resolved from the Donna project root. It must be relative and must not contain `..`. + +Use a directory ignored by version control unless the project intentionally tracks session artifacts. ## Workflow Directories `workflow_dirs` controls where Donna searches for workflow artifacts. Donna recursively scans each configured directory and recognizes only files ending with `.donna.md`. -Default: - ```toml -workflow_dirs = ["./workflows", "./.session/donna"] +workflow_dirs = [ + "./workflows", + "./.session/donna", +] ``` -Paths are relative to the Donna project root. Missing directories are ignored, so a project can keep the default list before all of those directories exist. +Fields: -Example: +- `workflow_dirs`: optional list of relative project paths, default `["./workflows", "./.session/donna"]`. + +Each path is resolved from the Donna project root. Paths must be relative and must not contain `..`. + +Missing directories are ignored. Duplicate directories are deduplicated when configuration is loaded. + +Use narrower directories when you want Donna to ignore unrelated `.donna.md` files elsewhere in the project. + +## Default Sections + +Donna artifacts are Markdown files ending with `.donna.md`. Each artifact is split into sections. A fenced `toml donna` block configures the artifact or section that contains it. + +The default section fields let a project omit repetitive config in common artifacts: ```toml -workflow_dirs = [ - "./workflows", - "./.session/donna", - "./team-workflows", -] +default_section_kind = "donna.lib.text" +default_primary_section_kind = "donna.lib.workflow" +default_primary_section_id = "primary" ``` -Use narrower directories when you want Donna to ignore unrelated `.donna.md` files elsewhere in the project. +Fields: + +- `default_section_kind`: optional Python import path, default `donna.lib.text`. +- `default_primary_section_kind`: optional Python import path, default `donna.lib.workflow`. +- `default_primary_section_id`: optional section id, default `primary`. -## Journal Forwarding +When a non-primary section omits `kind`, Donna uses `default_section_kind`. -`journal.cmd` forwards Donna journal records to an external command. Omit it or set it to `null` to disable forwarding. +When the primary H1 section omits `kind`, Donna uses `default_primary_section_kind`. -Example: +When the primary H1 section omits `id`, Donna uses `default_primary_section_id`. + +Most projects should not change these fields. Change them only when a project intentionally uses custom Donna primitives or a different artifact convention. + +## Journal + +`journal.cmd` forwards Donna journal records to an external command. Omit `cmd` to disable forwarding. ```toml [journal] cmd = ["./bin/taskwarior.sh", "log", "+journal", "+donna", "{message}"] ``` -The command is configured as a list of arguments. Donna does not run a shell for this command. +Fields: + +- `cmd`: optional non-empty list of command arguments. + +Donna does not run a shell implicitly. If shell behavior is required, call a shell explicitly: + +```toml +[journal] +cmd = [ + "/bin/sh", + "-c", + "printf '%s [%s] %s\n' \"$1\" \"$2\" \"$3\" >> donna.log", + "journal", + "{timestamp}", + "{actor_id}", + "{message}", +] +``` -Supported placeholders: +Supported whole-argument placeholders: - `{timestamp}`: ISO-8601 record timestamp. - `{actor_id}`: actor that created the record. @@ -101,22 +179,61 @@ Supported placeholders: Invalid placeholder names make configuration loading fail. -Example with explicit fields: +Placeholders are recognized only when the whole argument starts with `{` and ends with `}`. For example, `{message}` is a placeholder, but `message:{message}` is a literal argument. + +Donna still prints newly created journal records through the selected output protocol even when `journal.cmd` is omitted. + +## Paths + +Paths in `donna.toml` are stable from the Donna project root, which is the directory containing the active `donna.toml`. + +Configuration paths should use `/`. + +Project paths in `session_dir` and `workflow_dirs` must be relative: ```toml -[journal] -cmd = [ - "./bin/taskwarior.sh", - "log", - "+journal", - "+donna", - "actor:{actor_id}", - "operation:{current_operation_id}", - "{message}", -] +session_dir = ".session/donna" +workflow_dirs = ["./workflows", "./.session/donna"] +``` + +Absolute paths are invalid: + +```toml +session_dir = "/tmp/donna" ``` -## Validation Workflow +Parent-directory references are invalid: + +```toml +workflow_dirs = ["../workflows"] +``` + +Workflow artifact ids use canonical root-anchored paths: + +```text +@/workflows/polish.donna.md +@/.session/donna/execute_rfc.donna.md +``` + +Artifact section ids append `:section_id`: + +```text +@/workflows/polish.donna.md:finish +``` + +## Recommendations + +Keep project-owned workflows in a dedicated workflow directory such as `./workflows`. + +Keep temporary or generated workflows under the configured `session_dir` and include that directory in `workflow_dirs` when those workflows should be executable. + +Do not broaden `workflow_dirs` to the whole repository unless the project intentionally treats every `.donna.md` file as an executable Donna artifact. + +Keep default section settings unchanged unless the project has custom primitives and a clear convention for using them. + +Configure `journal.cmd` only when the project has a stable journal tool. A failing journal command can make Donna report environment errors during workflow execution. + +## Validation After editing `donna.toml`, run: @@ -126,3 +243,15 @@ donna -p llm validate --all ``` If Donna cannot load the project config, inspect the reported configuration error and fix the TOML before continuing workflow work. + +After creating or editing a workflow, validate that workflow directly: + +```bash +donna -p llm validate @/workflows/example.donna.md +``` + +If the workflow should be discoverable, confirm it appears in: + +```bash +donna -p llm list +``` diff --git a/donna/skills/fixtures/initialization.md b/donna/skills/fixtures/initialization.md index 32981dd9..55732f5a 100644 --- a/donna/skills/fixtures/initialization.md +++ b/donna/skills/fixtures/initialization.md @@ -12,7 +12,7 @@ Use this document when a project has no `donna.toml`. /donna.toml ``` -`donna.toml` stores configuration. The configured session directory stores Donna runtime state and session artifacts after runtime commands create it. +`donna.toml` stores configuration. The generated file declares schema version `1`. The configured session directory stores Donna runtime state and session artifacts after runtime commands create it. ## Initialize The Current Directory diff --git a/donna/workspaces/config.py b/donna/workspaces/config.py index 1a7e32cf..1502ba3a 100644 --- a/donna/workspaces/config.py +++ b/donna/workspaces/config.py @@ -1,7 +1,7 @@ from __future__ import annotations import enum -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal import pydantic @@ -82,6 +82,7 @@ def _validate_relative_project_path(path: RelativeProjectPath) -> RelativeProjec class Config(BaseEntity): + version: Literal[1] = 1 session_dir: RelativeProjectPath = RelativeProjectPath(DONNA_DEFAULT_SESSION_DIR) default_section_kind: PythonPath = PythonPath(NormalizedRawIdPath("donna.lib.text")) default_primary_section_kind: PythonPath = PythonPath(NormalizedRawIdPath("donna.lib.workflow")) diff --git a/donna/workspaces/fixtures/base_config.toml b/donna/workspaces/fixtures/base_config.toml index 2fec7bb6..74d0b09a 100644 --- a/donna/workspaces/fixtures/base_config.toml +++ b/donna/workspaces/fixtures/base_config.toml @@ -1,3 +1,4 @@ +version = 1 # Directory where Donna will keep its state. session_dir = ".session/donna" From aa335ab775793ed609aff202a0e3f2f09f76326a Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 16 May 2026 19:39:47 +0200 Subject: [PATCH 05/98] wip --- donna/skills/fixtures/configuration.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/donna/skills/fixtures/configuration.md b/donna/skills/fixtures/configuration.md index b22e8f82..24834471 100644 --- a/donna/skills/fixtures/configuration.md +++ b/donna/skills/fixtures/configuration.md @@ -2,8 +2,6 @@ `donna.toml` tells `donna` where a project's workflow virtual machine stores state, where it discovers workflow artifacts, which default section primitives to use, and how to forward workflow journal records. -Donna is a CLI tool for agents. It keeps high-level control flow in explicit workflows so an agent can focus on the concrete work requested by each operation. A workflow is a state machine stored in a project-local `.donna.md` artifact. Donna maintains the active session state, the workflow stack, queued work units, and action requests. - The configuration file has two main parts: - Top-level workspace settings configure session storage, artifact discovery, and default artifact section behavior. From 45758470ee247ec404bbab5b3cb215bed90f55d8 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 16 May 2026 19:43:19 +0200 Subject: [PATCH 06/98] defaults config section --- donna.toml | 8 +++-- donna/skills/fixtures/configuration.md | 36 ++++++++++++---------- donna/workspaces/artifacts.py | 7 +++-- donna/workspaces/config.py | 10 ++++-- donna/workspaces/fixtures/base_config.toml | 7 +++-- 5 files changed, 39 insertions(+), 29 deletions(-) diff --git a/donna.toml b/donna.toml index 92256919..2076863f 100644 --- a/donna.toml +++ b/donna.toml @@ -1,10 +1,12 @@ version = 1 session_dir = ".session/donna" -default_section_kind = "donna.lib.text" -default_primary_section_kind = "donna.lib.workflow" -default_primary_section_id = "primary" workflow_dirs = ["./workflows", "./.session/donna"] +[defaults] +section_kind = "donna.lib.text" +primary_section_kind = "donna.lib.workflow" +primary_section_id = "primary" + [journal] cmd = ["./bin/taskwarior.sh", "log", "+journal", "+donna", "kind:event", "{message}"] diff --git a/donna/skills/fixtures/configuration.md b/donna/skills/fixtures/configuration.md index 24834471..a732d1c6 100644 --- a/donna/skills/fixtures/configuration.md +++ b/donna/skills/fixtures/configuration.md @@ -2,9 +2,10 @@ `donna.toml` tells `donna` where a project's workflow virtual machine stores state, where it discovers workflow artifacts, which default section primitives to use, and how to forward workflow journal records. -The configuration file has two main parts: +The configuration file has three main parts: -- Top-level workspace settings configure session storage, artifact discovery, and default artifact section behavior. +- Top-level workspace settings configure schema version, session storage, and artifact discovery. +- `defaults` configures fallback artifact section behavior. - `journal` configures optional forwarding of Donna journal records to an external command. The configuration file is TOML with schema version `1`. The presence of `donna.toml` marks a Donna project root. @@ -37,11 +38,13 @@ Effective defaults: ```toml version = 1 session_dir = ".session/donna" -default_section_kind = "donna.lib.text" -default_primary_section_kind = "donna.lib.workflow" -default_primary_section_id = "primary" workflow_dirs = ["./workflows", "./.session/donna"] +[defaults] +section_kind = "donna.lib.text" +primary_section_kind = "donna.lib.workflow" +primary_section_id = "primary" + [journal] ``` @@ -49,10 +52,8 @@ Top-level fields: - `version`: optional integer, currently `1`. - `session_dir`: optional relative path to Donna's session directory. -- `default_section_kind`: optional Python path to the primitive used for non-primary sections without an explicit `kind`. -- `default_primary_section_kind`: optional Python path to the primitive used for the primary H1 section without an explicit `kind`. -- `default_primary_section_id`: optional section id assigned to the primary H1 section when it omits `id`. - `workflow_dirs`: optional list of relative directories scanned for `.donna.md` workflow artifacts. +- `defaults`: optional default artifact section config. - `journal`: optional journal forwarding config. Unknown top-level fields are invalid. @@ -119,22 +120,23 @@ Donna artifacts are Markdown files ending with `.donna.md`. Each artifact is spl The default section fields let a project omit repetitive config in common artifacts: ```toml -default_section_kind = "donna.lib.text" -default_primary_section_kind = "donna.lib.workflow" -default_primary_section_id = "primary" +[defaults] +section_kind = "donna.lib.text" +primary_section_kind = "donna.lib.workflow" +primary_section_id = "primary" ``` Fields: -- `default_section_kind`: optional Python import path, default `donna.lib.text`. -- `default_primary_section_kind`: optional Python import path, default `donna.lib.workflow`. -- `default_primary_section_id`: optional section id, default `primary`. +- `section_kind`: optional Python import path, default `donna.lib.text`. +- `primary_section_kind`: optional Python import path, default `donna.lib.workflow`. +- `primary_section_id`: optional section id, default `primary`. -When a non-primary section omits `kind`, Donna uses `default_section_kind`. +When a non-primary section omits `kind`, Donna uses `defaults.section_kind`. -When the primary H1 section omits `kind`, Donna uses `default_primary_section_kind`. +When the primary H1 section omits `kind`, Donna uses `defaults.primary_section_kind`. -When the primary H1 section omits `id`, Donna uses `default_primary_section_id`. +When the primary H1 section omits `id`, Donna uses `defaults.primary_section_id`. Most projects should not change these fields. Change them only when a project intentionally uses custom Donna primitives or a different artifact convention. diff --git a/donna/workspaces/artifacts.py b/donna/workspaces/artifacts.py index bd0b529f..be2855ab 100644 --- a/donna/workspaces/artifacts.py +++ b/donna/workspaces/artifacts.py @@ -186,14 +186,15 @@ def render_markdown_artifact( from donna.workspaces.markdown_parser import construct_artifact_from_bytes workspace_config = config() + defaults = workspace_config.defaults return Ok( construct_artifact_from_bytes( artifact_id, content, render_context, - default_section_kind=workspace_config.default_section_kind, - default_primary_section_kind=workspace_config.default_primary_section_kind, - default_primary_section_id=workspace_config.default_primary_section_id, + default_section_kind=defaults.section_kind, + default_primary_section_kind=defaults.primary_section_kind, + default_primary_section_id=defaults.primary_section_id, ).unwrap() ) diff --git a/donna/workspaces/config.py b/donna/workspaces/config.py index 1502ba3a..ab6a42f2 100644 --- a/donna/workspaces/config.py +++ b/donna/workspaces/config.py @@ -60,6 +60,12 @@ def validate_cmd(cls, value: list[str] | None) -> list[str] | None: return value +class DefaultsConfig(BaseEntity): + section_kind: PythonPath = PythonPath(NormalizedRawIdPath("donna.lib.text")) + primary_section_kind: PythonPath = PythonPath(NormalizedRawIdPath("donna.lib.workflow")) + primary_section_id: SectionId = SectionId("primary") + + def _default_workflow_dirs() -> list[RelativeProjectPath]: return [ RelativeProjectPath(DONNA_DEFAULT_WORKFLOW_DIR), @@ -84,9 +90,7 @@ def _validate_relative_project_path(path: RelativeProjectPath) -> RelativeProjec class Config(BaseEntity): version: Literal[1] = 1 session_dir: RelativeProjectPath = RelativeProjectPath(DONNA_DEFAULT_SESSION_DIR) - default_section_kind: PythonPath = PythonPath(NormalizedRawIdPath("donna.lib.text")) - default_primary_section_kind: PythonPath = PythonPath(NormalizedRawIdPath("donna.lib.workflow")) - default_primary_section_id: SectionId = SectionId("primary") + defaults: DefaultsConfig = pydantic.Field(default_factory=DefaultsConfig) workflow_dirs: list[RelativeProjectPath] = pydantic.Field(default_factory=_default_workflow_dirs) journal: JournalConfig = pydantic.Field(default_factory=JournalConfig) diff --git a/donna/workspaces/fixtures/base_config.toml b/donna/workspaces/fixtures/base_config.toml index 74d0b09a..440726a5 100644 --- a/donna/workspaces/fixtures/base_config.toml +++ b/donna/workspaces/fixtures/base_config.toml @@ -6,9 +6,10 @@ session_dir = ".session/donna" # Defaults to simplify initialization of a workflow. # Most likely you don't need to change these unless you implement a custom operation or workflow. # -# default_section_kind = "donna.lib.text" -# default_primary_section_kind = "donna.lib.workflow" -# default_primary_section_id = "primary" +# [defaults] +# section_kind = "donna.lib.text" +# primary_section_kind = "donna.lib.workflow" +# primary_section_id = "primary" # Directories where Donna will look for workflow files. workflow_dirs = [ From e636e748610dd1740997836780b1e23cd063d60a Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 16 May 2026 19:49:57 +0200 Subject: [PATCH 07/98] wip --- donna/skills/fixtures/configuration.md | 47 +++------------------- donna/workspaces/fixtures/base_config.toml | 3 +- 2 files changed, 8 insertions(+), 42 deletions(-) diff --git a/donna/skills/fixtures/configuration.md b/donna/skills/fixtures/configuration.md index a732d1c6..2e975e41 100644 --- a/donna/skills/fixtures/configuration.md +++ b/donna/skills/fixtures/configuration.md @@ -5,7 +5,7 @@ The configuration file has three main parts: - Top-level workspace settings configure schema version, session storage, and artifact discovery. -- `defaults` configures fallback artifact section behavior. +- `defaults` configures usable defaults. Most projects can omit this section. - `journal` configures optional forwarding of Donna journal records to an external command. The configuration file is TOML with schema version `1`. The presence of `donna.toml` marks a Donna project root. @@ -18,7 +18,7 @@ version = 1 If `version` is omitted, Donna treats the configuration as schema version `1`. -The generated file includes comments around this shape: +Minimal file (will be generated by `donna init`): ```toml version = 1 @@ -29,23 +29,6 @@ workflow_dirs = [ "./workflows", "./.session/donna", ] - -[journal] -``` - -Effective defaults: - -```toml -version = 1 -session_dir = ".session/donna" -workflow_dirs = ["./workflows", "./.session/donna"] - -[defaults] -section_kind = "donna.lib.text" -primary_section_kind = "donna.lib.workflow" -primary_section_id = "primary" - -[journal] ``` Top-level fields: @@ -58,27 +41,9 @@ Top-level fields: Unknown top-level fields are invalid. -## Version - -`version` declares the Donna configuration schema version. - -```toml -version = 1 -``` - -Fields: - -- `version`: optional integer, currently `1`. - -If `version` is omitted, Donna treats the configuration as schema version `1`. - -If `version` is present, it must be an integer. - -If `version` is not supported, configuration loading fails. - ## Session Directory -`session_dir` points to Donna's temporary session directory. Donna stores runtime state, action requests, and session-created artifacts there. +`session_dir` points to Donna's temporary session directory. Donna stores runtime state there. ```toml session_dir = ".session/donna" @@ -88,7 +53,7 @@ Fields: - `session_dir`: optional relative project path, default `.session/donna`. -The path is resolved from the Donna project root. It must be relative and must not contain `..`. +The path is resolved from the Donna project root. Use a directory ignored by version control unless the project intentionally tracks session artifacts. @@ -107,9 +72,9 @@ Fields: - `workflow_dirs`: optional list of relative project paths, default `["./workflows", "./.session/donna"]`. -Each path is resolved from the Donna project root. Paths must be relative and must not contain `..`. +Each path is resolved from the Donna project root. -Missing directories are ignored. Duplicate directories are deduplicated when configuration is loaded. +Missing directories are ignored. Use narrower directories when you want Donna to ignore unrelated `.donna.md` files elsewhere in the project. diff --git a/donna/workspaces/fixtures/base_config.toml b/donna/workspaces/fixtures/base_config.toml index 440726a5..55acc59b 100644 --- a/donna/workspaces/fixtures/base_config.toml +++ b/donna/workspaces/fixtures/base_config.toml @@ -16,12 +16,13 @@ workflow_dirs = [ "./workflows", "./.session/donna", ] -[journal] + # Command Donna will execute to log a workflow execution progress. # If the command is not specified, Donna will not log anything. # # Example: append journal records to a project-local text file. # Uses only /bin/sh and printf, which are available on standard Linux systems. +# [journal] # cmd = [ # "/bin/sh", # "-c", From 4186f5215eefdf62dafbdaf5fb55f5d851eac4f6 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 16 May 2026 19:54:33 +0200 Subject: [PATCH 08/98] better default names --- donna.toml | 2 +- donna/skills/fixtures/configuration.md | 30 ++++++++++++---------- donna/workspaces/artifacts.py | 2 +- donna/workspaces/config.py | 2 +- donna/workspaces/fixtures/base_config.toml | 2 +- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/donna.toml b/donna.toml index 2076863f..147af148 100644 --- a/donna.toml +++ b/donna.toml @@ -4,7 +4,7 @@ session_dir = ".session/donna" workflow_dirs = ["./workflows", "./.session/donna"] [defaults] -section_kind = "donna.lib.text" +tail_section_kind = "donna.lib.text" primary_section_kind = "donna.lib.workflow" primary_section_id = "primary" diff --git a/donna/skills/fixtures/configuration.md b/donna/skills/fixtures/configuration.md index 2e975e41..b81e6fae 100644 --- a/donna/skills/fixtures/configuration.md +++ b/donna/skills/fixtures/configuration.md @@ -1,11 +1,11 @@ # `donna` Configuration -`donna.toml` tells `donna` where a project's workflow virtual machine stores state, where it discovers workflow artifacts, which default section primitives to use, and how to forward workflow journal records. +`donna.toml` tells `donna` where a project's workflow virtual machine stores state, where it discovers workflow artifacts, which fallback Markdown section config values to use, and how to forward workflow journal records. The configuration file has three main parts: - Top-level workspace settings configure schema version, session storage, and artifact discovery. -- `defaults` configures usable defaults. Most projects can omit this section. +- `defaults` configures fallback config for Markdown artifact sections. Most projects can omit this section. - `journal` configures optional forwarding of Donna journal records to an external command. The configuration file is TOML with schema version `1`. The presence of `donna.toml` marks a Donna project root. @@ -36,7 +36,7 @@ Top-level fields: - `version`: optional integer, currently `1`. - `session_dir`: optional relative path to Donna's session directory. - `workflow_dirs`: optional list of relative directories scanned for `.donna.md` workflow artifacts. -- `defaults`: optional default artifact section config. +- `defaults`: optional fallback config for Markdown artifact sections. - `journal`: optional journal forwarding config. Unknown top-level fields are invalid. @@ -78,32 +78,34 @@ Missing directories are ignored. Use narrower directories when you want Donna to ignore unrelated `.donna.md` files elsewhere in the project. -## Default Sections +## Markdown Section Defaults -Donna artifacts are Markdown files ending with `.donna.md`. Each artifact is split into sections. A fenced `toml donna` block configures the artifact or section that contains it. +Donna artifacts are Markdown files ending with `.donna.md`. Each artifact is split into a primary H1 section and zero or more tail H2 sections. A fenced `toml donna` block configures the section that contains it. -The default section fields let a project omit repetitive config in common artifacts: +`defaults` supplies missing config values during Markdown artifact construction: ```toml [defaults] -section_kind = "donna.lib.text" +tail_section_kind = "donna.lib.text" primary_section_kind = "donna.lib.workflow" primary_section_id = "primary" ``` Fields: -- `section_kind`: optional Python import path, default `donna.lib.text`. -- `primary_section_kind`: optional Python import path, default `donna.lib.workflow`. -- `primary_section_id`: optional section id, default `primary`. +- `tail_section_kind`: optional Python import path used for H2 sections without explicit `kind`, default `donna.lib.text`. +- `primary_section_kind`: optional Python import path used for the H1 section without explicit `kind`, default `donna.lib.workflow`. +- `primary_section_id`: optional section id used for the H1 section without explicit `id`, default `primary`. -When a non-primary section omits `kind`, Donna uses `defaults.section_kind`. +Explicit section config always wins over these defaults. -When the primary H1 section omits `kind`, Donna uses `defaults.primary_section_kind`. +The primary H1 section is the artifact-level section. In normal workflow artifacts, it can omit `id` and `kind` because Donna fills them from `defaults.primary_section_id` and `defaults.primary_section_kind`. -When the primary H1 section omits `id`, Donna uses `defaults.primary_section_id`. +Tail H2 sections are child sections. When a tail section omits `kind`, Donna fills it from `defaults.tail_section_kind`. When a tail section omits `id`, Donna generates a temporary Markdown section id; there is no configurable default tail id. -Most projects should not change these fields. Change them only when a project intentionally uses custom Donna primitives or a different artifact convention. +The default `tail_section_kind` is `donna.lib.text`, so unconfigured H2 sections are treated as text/documentation sections. Workflow operation sections usually need explicit `kind` values such as `donna.lib.request_action`, `donna.lib.run_script`, `donna.lib.output`, or `donna.lib.finish`. + +Most projects should not change `defaults`. Change these fields only when a project intentionally uses custom Donna primitives or a different artifact convention. ## Journal diff --git a/donna/workspaces/artifacts.py b/donna/workspaces/artifacts.py index be2855ab..29475c77 100644 --- a/donna/workspaces/artifacts.py +++ b/donna/workspaces/artifacts.py @@ -192,7 +192,7 @@ def render_markdown_artifact( artifact_id, content, render_context, - default_section_kind=defaults.section_kind, + default_section_kind=defaults.tail_section_kind, default_primary_section_kind=defaults.primary_section_kind, default_primary_section_id=defaults.primary_section_id, ).unwrap() diff --git a/donna/workspaces/config.py b/donna/workspaces/config.py index ab6a42f2..76f48639 100644 --- a/donna/workspaces/config.py +++ b/donna/workspaces/config.py @@ -61,7 +61,7 @@ def validate_cmd(cls, value: list[str] | None) -> list[str] | None: class DefaultsConfig(BaseEntity): - section_kind: PythonPath = PythonPath(NormalizedRawIdPath("donna.lib.text")) + tail_section_kind: PythonPath = PythonPath(NormalizedRawIdPath("donna.lib.text")) primary_section_kind: PythonPath = PythonPath(NormalizedRawIdPath("donna.lib.workflow")) primary_section_id: SectionId = SectionId("primary") diff --git a/donna/workspaces/fixtures/base_config.toml b/donna/workspaces/fixtures/base_config.toml index 55acc59b..94da4391 100644 --- a/donna/workspaces/fixtures/base_config.toml +++ b/donna/workspaces/fixtures/base_config.toml @@ -7,7 +7,7 @@ session_dir = ".session/donna" # Most likely you don't need to change these unless you implement a custom operation or workflow. # # [defaults] -# section_kind = "donna.lib.text" +# tail_section_kind = "donna.lib.text" # primary_section_kind = "donna.lib.workflow" # primary_section_id = "primary" From 99f2b8ad41aa005ff94857cb5578ffa173def307 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 16 May 2026 19:59:11 +0200 Subject: [PATCH 09/98] wip --- donna/skills/fixtures/configuration.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/donna/skills/fixtures/configuration.md b/donna/skills/fixtures/configuration.md index b81e6fae..ec7fd89d 100644 --- a/donna/skills/fixtures/configuration.md +++ b/donna/skills/fixtures/configuration.md @@ -93,17 +93,15 @@ primary_section_id = "primary" Fields: -- `tail_section_kind`: optional Python import path used for H2 sections without explicit `kind`, default `donna.lib.text`. -- `primary_section_kind`: optional Python import path used for the H1 section without explicit `kind`, default `donna.lib.workflow`. -- `primary_section_id`: optional section id used for the H1 section without explicit `id`, default `primary`. +- `tail_section_kind`: default primitive path used for H2 sections without explicit `kind`. +- `primary_section_kind`: default primitive path used for the H1 section without explicit `kind`. +- `primary_section_id`: default section id used for the H1 section without explicit `id`. -Explicit section config always wins over these defaults. - -The primary H1 section is the artifact-level section. In normal workflow artifacts, it can omit `id` and `kind` because Donna fills them from `defaults.primary_section_id` and `defaults.primary_section_kind`. +Primitive is a Python object that is used to interpret the content of an artifact section. -Tail H2 sections are child sections. When a tail section omits `kind`, Donna fills it from `defaults.tail_section_kind`. When a tail section omits `id`, Donna generates a temporary Markdown section id; there is no configurable default tail id. +Read `donna -p llm skill workflows` for a deeper explanation. -The default `tail_section_kind` is `donna.lib.text`, so unconfigured H2 sections are treated as text/documentation sections. Workflow operation sections usually need explicit `kind` values such as `donna.lib.request_action`, `donna.lib.run_script`, `donna.lib.output`, or `donna.lib.finish`. +Explicit section config always wins over these defaults. Most projects should not change `defaults`. Change these fields only when a project intentionally uses custom Donna primitives or a different artifact convention. From 1946b3d158fdcf55c44c928327c5e0b44771a128 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 16 May 2026 20:03:52 +0200 Subject: [PATCH 10/98] wip --- donna/skills/fixtures/configuration.md | 23 ++++++++-------------- donna/workspaces/fixtures/base_config.toml | 11 +++++------ 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/donna/skills/fixtures/configuration.md b/donna/skills/fixtures/configuration.md index ec7fd89d..c1a6ea45 100644 --- a/donna/skills/fixtures/configuration.md +++ b/donna/skills/fixtures/configuration.md @@ -109,30 +109,23 @@ Most projects should not change `defaults`. Change these fields only when a proj `journal.cmd` forwards Donna journal records to an external command. Omit `cmd` to disable forwarding. -```toml -[journal] -cmd = ["./bin/taskwarior.sh", "log", "+journal", "+donna", "{message}"] -``` - -Fields: - -- `cmd`: optional non-empty list of command arguments. - -Donna does not run a shell implicitly. If shell behavior is required, call a shell explicitly: - ```toml [journal] cmd = [ - "/bin/sh", - "-c", - "printf '%s [%s] %s\n' \"$1\" \"$2\" \"$3\" >> donna.log", - "journal", + "./bin/journal-tool.sh", + "record", "{timestamp}", "{actor_id}", + "{current_task_id}", + "{current_operation_id}", "{message}", ] ``` +Fields: + +- `cmd`: optional non-empty list of command arguments. + Supported whole-argument placeholders: - `{timestamp}`: ISO-8601 record timestamp. diff --git a/donna/workspaces/fixtures/base_config.toml b/donna/workspaces/fixtures/base_config.toml index 94da4391..fdeb4f15 100644 --- a/donna/workspaces/fixtures/base_config.toml +++ b/donna/workspaces/fixtures/base_config.toml @@ -20,15 +20,14 @@ workflow_dirs = [ # Command Donna will execute to log a workflow execution progress. # If the command is not specified, Donna will not log anything. # -# Example: append journal records to a project-local text file. -# Uses only /bin/sh and printf, which are available on standard Linux systems. +# Example: forward journal records to a project-specific journal tool. # [journal] # cmd = [ -# "/bin/sh", -# "-c", -# "printf '%s [%s] %s\\n' \"$1\" \"$2\" \"$3\" >> donna.log", -# "journal", +# "./bin/journal-tool.sh", +# "record", # "{timestamp}", # "{actor_id}", +# "{current_task_id}", +# "{current_operation_id}", # "{message}", # ] From 3b64ac124ef3ada95c3955c0f2bb9d51bea22e08 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 16 May 2026 20:07:20 +0200 Subject: [PATCH 11/98] wip --- donna/skills/fixtures/configuration.md | 38 -------------------------- 1 file changed, 38 deletions(-) diff --git a/donna/skills/fixtures/configuration.md b/donna/skills/fixtures/configuration.md index c1a6ea45..1433191d 100644 --- a/donna/skills/fixtures/configuration.md +++ b/donna/skills/fixtures/configuration.md @@ -141,44 +141,6 @@ Placeholders are recognized only when the whole argument starts with `{` and end Donna still prints newly created journal records through the selected output protocol even when `journal.cmd` is omitted. -## Paths - -Paths in `donna.toml` are stable from the Donna project root, which is the directory containing the active `donna.toml`. - -Configuration paths should use `/`. - -Project paths in `session_dir` and `workflow_dirs` must be relative: - -```toml -session_dir = ".session/donna" -workflow_dirs = ["./workflows", "./.session/donna"] -``` - -Absolute paths are invalid: - -```toml -session_dir = "/tmp/donna" -``` - -Parent-directory references are invalid: - -```toml -workflow_dirs = ["../workflows"] -``` - -Workflow artifact ids use canonical root-anchored paths: - -```text -@/workflows/polish.donna.md -@/.session/donna/execute_rfc.donna.md -``` - -Artifact section ids append `:section_id`: - -```text -@/workflows/polish.donna.md:finish -``` - ## Recommendations Keep project-owned workflows in a dedicated workflow directory such as `./workflows`. From 1aa1245834f08f1ce444101acd07160d9ea37aee Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 16 May 2026 20:09:54 +0200 Subject: [PATCH 12/98] configuration.md finished --- donna/skills/fixtures/configuration.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/donna/skills/fixtures/configuration.md b/donna/skills/fixtures/configuration.md index 1433191d..1b78e210 100644 --- a/donna/skills/fixtures/configuration.md +++ b/donna/skills/fixtures/configuration.md @@ -147,7 +147,7 @@ Keep project-owned workflows in a dedicated workflow directory such as `./workfl Keep temporary or generated workflows under the configured `session_dir` and include that directory in `workflow_dirs` when those workflows should be executable. -Do not broaden `workflow_dirs` to the whole repository unless the project intentionally treats every `.donna.md` file as an executable Donna artifact. +Do not broaden `workflow_dirs` to the whole repository unless you plan to do some rocket science. In most cases a single specialized directory will enough. Keep default section settings unchanged unless the project has custom primitives and a clear convention for using them. @@ -163,15 +163,3 @@ donna -p llm validate --all ``` If Donna cannot load the project config, inspect the reported configuration error and fix the TOML before continuing workflow work. - -After creating or editing a workflow, validate that workflow directly: - -```bash -donna -p llm validate @/workflows/example.donna.md -``` - -If the workflow should be discoverable, confirm it appears in: - -```bash -donna -p llm list -``` From 144b29c6d3cf67c51386f039de1b5b9840dcf759 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 16 May 2026 20:15:36 +0200 Subject: [PATCH 13/98] removed deprecated journal demo gif --- docs/images/journal-demo.gif | Bin 797357 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/images/journal-demo.gif diff --git a/docs/images/journal-demo.gif b/docs/images/journal-demo.gif deleted file mode 100644 index a31e57456d84afee00012429f6e96b3877c3b1fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 797357 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%0A&d{Jik@oJuv^ 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 U}9bVgxd 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ෞmb+^KfOtj&~RIT3nN^U6Ptl15)m1L_eLN0lV}F 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%0AWFirGM~sY{t)cCvI)4W3wCzW(y=d8VeSdsM_f?5 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

_r-fjtw%S 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 From 344b3cb5a0dc8f6d5796adfce4685bd35b2b3ea4 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 16 May 2026 20:35:51 +0200 Subject: [PATCH 14/98] initialization.md --- donna/skills/fixtures/initialization.md | 95 ++++++++++++++++++------- 1 file changed, 71 insertions(+), 24 deletions(-) diff --git a/donna/skills/fixtures/initialization.md b/donna/skills/fixtures/initialization.md index 55732f5a..d2bc60ec 100644 --- a/donna/skills/fixtures/initialization.md +++ b/donna/skills/fixtures/initialization.md @@ -1,61 +1,108 @@ # `donna` Initialization -Initialization creates the Donna project config. Runtime commands create the configured session directory lazily. +`donna init` creates a starter `donna.toml` for a project that does not have one yet. -Use this document when a project has no `donna.toml`. +The generated file is intentionally small. It gives the project a valid schema version, a session directory, workflow discovery directories, and commented examples for optional defaults and journal forwarding. -## What Initialization Creates +Use this document when a configuration file is missing, when adding Donna to a new project, or when checking what initialization should create. -`donna -p llm init` creates: +For configuration syntax details, use: -```text -/donna.toml +```bash +donna -p llm skill configuration +``` + +For command usage after initialization, use: + +```bash +donna -p llm skill usage ``` -`donna.toml` stores configuration. The generated file declares schema version `1`. The configured session directory stores Donna runtime state and session artifacts after runtime commands create it. +## Project Root + +Run initialization from the directory that should contain `donna.toml`. + +Without `--root`, `donna init` creates `donna.toml` in the current working directory. With `--root PATH`, it creates `donna.toml` in that existing directory and treats it as the project root for later commands. + +The project root matters because Donna discovers it by locating `donna.toml`. `session_dir` and `workflow_dirs` are relative to that directory, and Donna reports workflow artifacts as `@/` anchored at that directory. Relative artifact paths passed to commands are resolved from the command's current working directory and accepted only when they point inside the project root. -## Initialize The Current Directory +## Create The Starter File -Run from the directory that should become the project root: +Create `donna.toml` in the current directory: ```bash donna -p llm init ``` -This command fails if `donna.toml` already exists. - -## Initialize Another Directory - -Pass an explicit root directory: +Create it in an explicit project root: ```bash donna -p llm --root /path/to/project init ``` -The target directory must already exist. Donna creates `donna.toml` inside it. +`init` does not overwrite an existing file. + +Right after creating the starter file, check whether the default workflow discovery directories match the project layout. Ask the developer before changing project structure or introducing new workflow directories. + +## Filling The Configuration + +Start from the smallest configuration that matches the project. + +General workflow: + +1. Check whether the project already has workflow files. +2. Keep `session_dir` unchanged unless the project has an established runtime-state or temporary files location. +3. Keep `workflow_dirs` unchanged when project workflows live in `./workflows` or temporary workflows should live in `./.session/donna`. +4. Add or narrow `workflow_dirs` only when the real workflow layout requires it. +5. Configure `journal.cmd` only when project instructions provide a reliable journal command. -## First Checks After Initialization +Do not create workflow files or new project directories during initialization unless the developer explicitly asks for that. `donna init` only creates `donna.toml`; runtime commands create the session directory lazily. -Verify the project config can load: +## First Workflow Files + +Donna discovers Markdown workflow files ending with `.donna.md` under configured workflow directories. + +After initialization, offer the developer to create the first project workflows automatically. Base the proposal on the project configuration, existing scripts, package manifests, CI definitions, test commands, formatter and linter commands, changelog or release files, and specifications or documentation templates. + +Good starter workflows: + +1. Polish code by running autoformatters and linters in order, capturing command output, asking the agent to fix issues, and looping until the code is clean. +2. Run tests, ask the agent to fix failures, and loop until all configured test suites pass. +3. Analyze branch changes and update the changelog or release notes. +4. Prepare a project-specific documentation or specification artifact from an existing template, then review it against its specification until it is complete. +5. Reproduce the project's CI pipeline locally by running the same checks in a deterministic order and routing failures to focused agent repair steps. +6. Verify changed deliverables against a specification, design document, or acceptance criteria, then route missing or incorrect deliverables back to fix steps. +7. Prepare implementation plans from existing RFC, design, issue, or specification documents and save the resulting workflow as a session artifact. +8. Run dependency, generated-file, schema, migration, or documentation-build checks when the project already has stable commands for them. + +Use direct file edits to create or change workflow files. Donna commands inspect, validate, render, and run workflow artifacts; they do not edit project-owned workflow files for you. + +Before adding or changing workflows, use: + +```bash +donna -p llm skill workflows +``` + +## Validation Loop + +After creating or editing `donna.toml`, verify that Donna can load the project config and initialize session state: ```bash donna -p llm status ``` -List available workflows: +`status` is the right first check because a newly initialized project may have no workflow files yet. + +If the project already has workflows, or if you created workflows during initialization, list visible workflows: ```bash donna -p llm list ``` -Validate artifacts: +Then validate the discovered workflow artifacts: ```bash donna -p llm validate --all ``` -## Agent Guidance - -Initialize Donna only when the developer asks for it or when the task explicitly requires Donna and no `donna.toml` exists. - -Edit project-owned artifacts directly when the developer asks for project-specific behavior changes. +If `list` returns no workflows, that can be valid for a newly initialized project. Check `workflow_dirs`, directory existence, and `.donna.md` suffixes only when workflows were expected. When configuration loading fails, fix `donna.toml` before continuing with workflow work. When validation fails, fix the workflow source files before running them. From 33e0b472bfcbe1aee054841625b645646acb538a Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 11:43:38 +0200 Subject: [PATCH 15/98] wip --- donna/skills/fixtures/workflows.md | 541 +++++++++++++++++++++++++---- 1 file changed, 482 insertions(+), 59 deletions(-) diff --git a/donna/skills/fixtures/workflows.md b/donna/skills/fixtures/workflows.md index 1aab35a4..be078883 100644 --- a/donna/skills/fixtures/workflows.md +++ b/donna/skills/fixtures/workflows.md @@ -1,131 +1,554 @@ # `donna` Workflows -Donna workflows are project files that Donna can discover, render, validate, and execute. They are usually Markdown files with the `.donna.md` extension. +Donna workflows are Markdown artifacts that describe finite-state machines for agent work. -Donna reads workflows from the project filesystem. It does not mutate project workflow files through workflow commands. Developers and agents edit files directly, then ask Donna to list workflows or validate them. +Use this document when you need to create, review, debug, or change a `*.donna.md` workflow file. -## Workflow Locations +A workflow does not do the agent's job. It defines control flow: -The common workflow areas are: +- which operation starts the work; +- which deterministic steps Donna can run by itself; +- where Donna must stop and ask the agent to act; +- which next operations are valid after each stop; +- when the workflow is complete. -- `/workflows`: project-owned workflows. -- `/.session/donna`: session workflows and active workflow state. +The agent still reads project instructions, edits files, runs tools, uses judgment, and reports results. Donna keeps the process explicit, resumable, and validated. -Project-local Donna documentation may also live under `/.agents/donna` when present. +## Workflow Files -Example: +Donna discovers workflow artifacts under the directories configured in `donna.toml:workflow_dirs`. For configuration details, defaults, and directory selection guidance, read `donna -p llm skill configuration`. + +Only files ending with `.donna.md` are workflow artifacts. + +Relative and absolute paths are accepted by Donna CLI commands when they resolve inside the Donna project root, but `@/` paths are stable from any current working directory. + +## File Shape + +A workflow file is one Markdown document with exactly one H1 section followed by zero or more H2 sections. + +The H1 section is the workflow section. Each H2 section is usually one workflow operation. + +## IDs + +Donna uses three related ids when working with workflows. + +A workflow artifact id identifies the workflow file. Use project-root anchored artifact ids in workflow instructions and notes: ```text -workflows/polish.donna.md -workflows/rfc/request.donna.md -.session/donna/current_task.donna.md +@/workflows/do-work.donna.md +@/.session/donna/plans/implement-feature.donna.md ``` -Donna sees only `.donna.md` files under directories listed in `donna.toml:workflow_dirs`. +A section id identifies one section inside a workflow artifact. For workflow operations, it is the `id` field in the operation's config block: -## List Workflows +```toml +id = "finish" +``` -List workflows: +A full artifact section id identifies one section in one artifact by appending `:` to the artifact id: -```bash -donna -p llm list +```text +@/workflows/do-work.donna.md:finish ``` -Read workflow source files directly when you need details beyond the workflow introduction. +Use full artifact section ids when completing action requests. Use local section ids in workflow config fields and `goto` directives inside the same workflow. -## Validate Workflows +## Minimal Workflow -Validate one workflow: +````markdown +# Example Workflow -```bash -donna -p llm validate '@/workflows/polish.donna.md' +This workflow checks the current time, asks the agent whether it is tea time, and branches on the answer. + +## Get Current Time + +```toml donna +id = "get_current_time" +kind = "donna.lib.run_script" +save_stdout_to = "current_time" +goto_on_success = "ask_about_tea" +goto_on_failure = "finish" ``` -Validate all visible workflows: +```bash donna script +#!/usr/bin/env bash +date +%H:%M +``` -```bash -donna -p llm validate --all +## Ask About Tea + +```toml donna +id = "ask_about_tea" +kind = "donna.lib.request_action" ``` -Run validation after creating or editing Donna workflows. +The current time is: -## Workflow IDs +```text +{{ donna.lib.task_variable("current_time") }} +``` -Use `@/` for project-root workflow ids: +Is it time to drink tea? -```bash -donna -p llm validate '@/workflows/polish.donna.md' +1. If yes, `{{ donna.lib.goto("turn_on_kettle") }}`. +2. If no, `{{ donna.lib.goto("finish") }}`. + +## Turn On Kettle + +```toml donna +id = "turn_on_kettle" +kind = "donna.lib.request_action" ``` -Validate multiple specific workflows by passing multiple ids: +Turn on the kettle, then `{{ donna.lib.goto("finish") }}`. -```bash -donna -p llm validate '@/workflows/polish.donna.md' './workflows/rfc/request.donna.md' +## Finish + +```toml donna +id = "finish" +kind = "donna.lib.finish" ``` -Workflow path arguments accept root-anchored paths such as `@/workflows/polish.donna.md`, relative paths such as `./workflows/polish.donna.md`, and absolute paths inside the project root. +The workflow is complete. Report the result to the developer. +```` -## Creating Workflows +## Markdown Parsing Rules -A workflow defines a finite-state machine. The H1 section is a workflow by default. Each H2 section declares one operation. When the H1 config omits `start_operation_id`, Donna starts from the first H2 section. +Donna's Markdown parser uses only H1 and H2 headings as section boundaries. -Minimal workflow: +- The first heading must be H1. +- There can be only one H1. +- H2 starts a tail section. +- H3 and deeper headings stay inside the current H1 or H2 section body. +- Text before the first H1 is invalid. + +Donna removes `donna` config and script code fences from the rendered section description. Ordinary code fences remain visible to the agent. + +Each section may have at most one config block. A config block is a fenced code block marked with `donna`; `toml donna` is treated as `toml donna config`. ````markdown -# Example Workflow +```toml donna +id = "operation_id" +kind = "donna.lib.request_action" +``` +```` + +The parser supports TOML, JSON, YAML, and YML config formats, but write workflow config as TOML unless the project has a clear reason to do otherwise. + +Script blocks are fenced code blocks marked with both `donna` and `script`: -This workflow asks the agent to do one thing and finish. +````markdown +```bash donna script +#!/usr/bin/env bash +echo "hello" +``` +```` -## Do The Work +A `donna.lib.run_script` operation must have exactly one script block. +## Section Config + +All section config is strict. Unknown fields are invalid. + +Common fields: + +- `id`: local section id. The H1 defaults to `primary`; H2 sections without `id` get generated unstable ids. Set explicit ids for all operation sections. +- `kind`: primitive path that tells Donna how to interpret the section. The H1 defaults to `donna.lib.workflow`; H2 sections default to `donna.lib.text`. +- `fsm_mode`: optional operation mode. Valid values are `start`, `normal`, and `final`. The default is `normal`. + +Section ids may contain ASCII letters, digits, underscores, hyphens, and dots. Use lowercase snake case for readability: + +```toml +id = "review_changes" +``` + +Do not rely on `fsm_mode = "start"` to choose the start operation. Donna starts from the H1 `start_operation_id` when it is set, otherwise from the first H2 section. `fsm_mode = "start"` is metadata. + +## Workflow Section + +The H1 section is normally a `donna.lib.workflow` section. It describes the whole workflow and optionally chooses the first operation. + +````markdown +```toml donna +id = "primary" +kind = "donna.lib.workflow" +start_operation_id = "start" +``` +```` + +Fields: + +- `id`: optional, defaults to `primary`. +- `kind`: optional, defaults to `donna.lib.workflow`. +- `start_operation_id`: optional local id of the operation to run first. + +If `start_operation_id` is omitted, Donna uses the first H2 section. If the workflow has no H2 section and no explicit start operation, validation fails. + +Write the H1 body as a concise summary. It is what `donna -p llm list` shows for the workflow. + +## Operation Sections + +Each operation is an H2 section with a config block. The H2 title becomes the operation title. The body becomes the operation instructions, output text, or script host depending on `kind`. + +Standard operation kinds are: + +- `donna.lib.request_action`: stop and ask the agent to do work. +- `donna.lib.run_script`: run a deterministic shell script from the project root. +- `donna.lib.output`: print information, then continue automatically. +- `donna.lib.finish`: print final information and finish the workflow task. + +`donna.lib.text` is not an operation. It can be used for unreachable notes in an artifact, but a reachable workflow section must be an operation or validation fails. + +### `request_action` + +Use `donna.lib.request_action` when the next step needs agent judgment, file edits, research, tool use, or communication with the developer. + +````markdown ```toml donna -id = "ask_agent" +id = "review_changes" kind = "donna.lib.request_action" ``` +```` -Perform the requested change. +Donna emits the section body as an action request and waits. The agent must complete the request by choosing one of the transitions declared in the action request body. -When done, continue with `{{ donna.lib.goto("finish") }}`. +Declare transitions with `{{ donna.lib.goto("") }}`: -## Finish +```markdown +1. Review the changes. +2. If fixes are needed, `{{ donna.lib.goto("fix_changes") }}`. +3. If no fixes are needed, `{{ donna.lib.goto("finish") }}`. +``` + +Each `goto` declares an allowed transition from the current request action to the target operation. + +Rules: + +- A reachable `request_action` operation must contain at least one `goto`. +- `request_action` cannot use `fsm_mode = "final"`. +- The next operation passed to `complete-action-request` must be one of the operation's declared `goto` targets. +- Use explicit result-based choices in the text so the agent knows which transition to select. + +### `run_script` + +Use `donna.lib.run_script` for deterministic checks or commands that Donna can run without agent judgment. + +````markdown +## Run Tests + +```toml donna +id = "run_tests" +kind = "donna.lib.run_script" +save_stdout_to = "test_stdout" +save_stderr_to = "test_stderr" +goto_on_success = "finish" +goto_on_failure = "fix_tests" +timeout = 120 +``` + +```bash donna script +#!/usr/bin/env bash +./bin/test.sh +``` +```` + +Fields: + +- `goto_on_success`: required operation id for exit code `0`. +- `goto_on_failure`: required fallback operation id for non-zero exit codes. +- `goto_on_code`: optional TOML table mapping specific non-zero exit codes to operation ids. +- `save_stdout_to`: optional task-context key for stdout. +- `save_stderr_to`: optional task-context key for stderr. +- `timeout`: optional timeout in seconds, default `60`. +- `fsm_mode`: optional, default `normal`. + +Example with exit-code-specific routing: + +```toml +goto_on_success = "finish" +goto_on_failure = "fix_failure" + +[goto_on_code] +"124" = "fix_timeout" +"2" = "fix_usage" +``` + +Exit code `0` must not be placed in `goto_on_code`; use `goto_on_success`. +Execution behavior: + +- Donna writes the script to a temporary executable file. +- The script runs from the Donna project root. +- The process inherits the environment. +- Stdin is closed. +- Stdout and stderr are captured. +- A timeout returns exit code `124`. +- Donna queues the selected next operation automatically. + +Script output is not automatically shown to the agent. Save it to task context and read it with `donna.lib.task_variable` in a later request action. + +### `output` + +Use `donna.lib.output` when Donna should print information and then continue automatically. + +````markdown +```toml donna +id = "show_context" +kind = "donna.lib.output" +next_operation_id = "next_step" +``` +```` + +Fields: + +- `next_operation_id`: required operation id to queue after printing the section body. +- `fsm_mode`: optional, default `normal`. + +A reachable `output` operation must have `next_operation_id`. + +### `finish` + +Use `donna.lib.finish` for the terminal operation. + +````markdown ```toml donna id = "finish" kind = "donna.lib.finish" ``` - -The workflow is complete. ```` -Validate the workflow before running it: +`finish` is always final. It prints the section body and completes the active workflow task. It must not have outgoing transitions. + +There may be multiple `finish` operations. + +Use the finish body to tell the agent what to report: + +```markdown +Workflow completed. Report the files changed, verification performed, and remaining risks. +``` + +## Transitions And Validation + +Donna validates the reachable operation graph starting from the workflow start operation. + +An operation is reachable when it is the start operation or it can be reached through outgoing transitions from another reachable operation. + +Outgoing transitions come from operation metadata: + +- `request_action`: every `{{ donna.lib.goto("...") }}` in the analyzed request body. +- `run_script`: `goto_on_success`, `goto_on_failure`, and all `goto_on_code` values. +- `output`: `next_operation_id`. +- `finish`: no transitions. + +Validation rules for the reachable graph: + +- Every reachable section must exist. +- Every reachable non-workflow section must be an operation. +- Every reachable non-final operation must have at least one outgoing transition. +- Every reachable final operation must have no outgoing transitions. + +Primitive-specific validation also runs for all sections. + +Run validation after every workflow edit: ```bash -donna -p llm validate '@/workflows/example.donna.md' +donna -p llm validate @/workflows/example.donna.md ``` -Run it: +Render the workflow when transitions or directives look wrong: ```bash -donna -p llm run '@/workflows/example.donna.md' +donna -p llm render @/workflows/example.donna.md --mode analysis ``` -## Managing Workflows +## Execution Notes + +`donna -p llm run @/path/to/workflow.donna.md` starts the workflow in the current session. + +Execution loop: + +1. Donna loads the artifact and starts at the primary workflow section. +2. The workflow section queues the start operation. +3. Donna executes queued work units for the current task. +4. Deterministic operations can queue the next operation immediately. +5. `request_action` creates an action request and pauses. +6. The agent performs the request and calls `complete-action-request` with the chosen next operation id. +7. Donna validates that the transition is allowed, queues the next operation, and continues. +8. `finish` completes the task. -Use direct file edits to create, update, move, or delete workflow files. Then use Donna to inspect the result. +## Directives And Rendering -Recommended loop: +Donna renders workflow Markdown with Jinja directives. -1. Edit the workflow source file. -2. Validate the workflow: +Directives have two author-facing uses: + +- Structural directives define workflow structure that is not convenient to express with static config alone. +- Informational directives insert meaningful runtime information into text shown to the agent. + +Donna normally renders text for the agent in `view` mode. Other render modes exist, but they are implementation details and should not affect how you write normal workflows. + +Workflow authors may use regular Jinja2 constructs such as variables, conditionals, loops, and macros to produce complex agent-facing text. + +Use view rendering when debugging what an agent will see: ```bash -donna -p llm validate '@/workflows/example.donna.md' +donna -p llm render @/workflows/example.donna.md --mode view ``` -3. List it: +Do not use templates that add or remove headings, config blocks, or operation ids depending on render context. Workflow structure should stay stable after rendering. -```bash -donna -p llm list +### `goto` + +`{{ donna.lib.goto("next_operation") }}` declares an allowed transition from a `request_action` operation to another operation in the same workflow. + +Use `goto` only in `request_action` instructions. The visible rendered text tells the agent how to complete the action request with that next operation. + +Example: + +```markdown +1. If the check passed, `{{ donna.lib.goto("finish") }}`. +2. If the check failed, `{{ donna.lib.goto("fix_issue") }}`. ``` -Keep workflow files concise. Put project-wide explanations in specifications and operational step-by-step instructions in workflows. +The argument is a local section id, not a full artifact section id. + +### `task_variable` + +`{{ donna.lib.task_variable("name") }}` inserts a value saved in the current task context. + +Use it to show the agent output captured by earlier automated operations, especially `run_script` operations with `save_stdout_to` or `save_stderr_to`. + +Example: + +````markdown +```text +{{ donna.lib.task_variable("test_stdout") }} +``` +```` + +Use the same key that the earlier operation saved: + +```toml +save_stdout_to = "test_stdout" +``` + +## Creating Workflows + +Start with the workflow's control-flow shape, then fill operation instructions. + +Recommended process: + +1. Name the workflow by its outcome, not by a generic process label. +2. Write the H1 summary that `donna list` should show. +3. Add one operation per meaningful state or decision point. +4. Prefer `run_script` for deterministic checks and `request_action` for agent judgment. +5. Add a `finish` operation(s) with clear reporting instructions. +6. Validate the workflow. + +Good operation titles stand alone: + +- `Run Unit Tests` +- `Fix Formatter Output` +- `Review Deliverables` +- `Check Migration File` + +Avoid titles that only make sense by order: + +- `Step 1` +- `Part Two` +- `Continue` +- `Do It` + +Keep request actions specific. Each request should have enough context for the agent to act and enough transition choices to continue correctly. + +Split large request actions when they mix unrelated work. A good split is usually: + +- research or inspect; +- implement one bounded change; +- verify the changed behavior; +- decide where to go next. + +Remember, you can add references to project-specific specifications in the action request text — no need to repeat all documentation there. Keep action requests short and consice. + +## Workflow Design Patterns + +Linear workflow: + +```text +start -> do_work -> verify -> finish +``` + +Retry loop: + +```text +run_check -> fix_check -> run_check +run_check -> finish +``` + +Review gate: + +```text +implement -> review +review -> implement +review -> finish +``` + +Script with manual repair: + +```text +run_script success -> next_step +run_script failure -> fix_failure +fix_failure -> run_script +``` + +Use loops deliberately. The operation that loops back should say what changed and why rerunning the earlier operation is necessary. + +## Debugging Workflows + +If a workflow is not listed: + +1. Check that the file ends with `.donna.md`. +2. Check that it is under one of `workflow_dirs`. +3. Check that the workflow directory exists. +4. Run `donna -p llm list` from the intended Donna project root or pass `--root`. + +If validation says a start operation is missing: + +1. Check `start_operation_id` in the H1 config. +2. Check the target H2 `id`. +3. If no explicit start is set, check that the workflow has at least one H2 section. + +If validation says an operation has no outgoing transitions: + +1. For `request_action`, add `{{ donna.lib.goto("next") }}` in the request body. +2. For `run_script`, set `goto_on_success` and `goto_on_failure`. +3. For `output`, set `next_operation_id`. +4. For terminal operations, use `kind = "donna.lib.finish"`. + +If an action request cannot transition to the chosen operation: + +1. Use one of the next operation ids shown in the action request. +2. Check that the `goto` target is local to the same workflow artifact. +3. Re-render in `analysis` mode to confirm Donna detected the transition. + +If a script failure output is missing from a later request action: + +1. Check `save_stdout_to` and `save_stderr_to` keys. +2. Check that `task_variable` uses the exact same key. +3. Check that the operation reading the variable runs after the script operation. + +## Authoring Checklist + +Before considering a workflow ready: + +1. The file is under a configured workflow directory and ends with `.donna.md`. +2. The H1 summary explains the workflow outcome. +3. Every operation H2 has a stable `id` and explicit `kind`. +4. The start operation is intentional. +5. Every non-final reachable operation has an outgoing transition. +6. Every final operation is `donna.lib.finish` or has `fsm_mode = "final"` with no transitions. +7. Every `request_action` gives the agent clear completion choices. +8. Every `run_script` has success and failure routing. +9. Script output needed by the agent is saved and later read from task context. +10. `donna -p llm validate ` succeeds. +11. `donna -p llm render --mode view` is readable by an agent. From 68d20b9720a248eac72bb143db457f3ee3a56229 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 11:51:09 +0200 Subject: [PATCH 16/98] workflows.md --- donna/skills/fixtures/workflows.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/donna/skills/fixtures/workflows.md b/donna/skills/fixtures/workflows.md index be078883..bda1630c 100644 --- a/donna/skills/fixtures/workflows.md +++ b/donna/skills/fixtures/workflows.md @@ -468,7 +468,7 @@ Split large request actions when they mix unrelated work. A good split is usuall - verify the changed behavior; - decide where to go next. -Remember, you can add references to project-specific specifications in the action request text — no need to repeat all documentation there. Keep action requests short and consice. +Remember, you can add references to project-specific specifications in the action request text — no need to repeat all documentation there. Keep action requests short and concise. ## Workflow Design Patterns @@ -493,6 +493,31 @@ review -> implement review -> finish ``` +Decision tree: + +```text +classify_request -> simple_path +classify_request -> complex_path +classify_request -> ask_for_clarification +simple_path -> finish +complex_path -> plan_work +ask_for_clarification -> classify_request +``` + +Classification tree: + +```text +check_outlook -> class_play_tennis +check_outlook -> check_humidity +check_outlook -> check_wind +check_humidity -> class_stay_home +check_humidity -> class_play_tennis +check_wind -> class_stay_home +check_wind -> class_play_tennis +class_play_tennis -> finish +class_stay_home -> finish +``` + Script with manual repair: ```text From bc4bf0e9f37f016b63fd5086012f662bf1a30c40 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 12:02:13 +0200 Subject: [PATCH 17/98] wip --- donna/skills/fixtures/usage.md | 14 ++++++++++- donna/skills/fixtures/workflows.md | 40 +++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/donna/skills/fixtures/usage.md b/donna/skills/fixtures/usage.md index 6c3c0bfc..ab451cb6 100644 --- a/donna/skills/fixtures/usage.md +++ b/donna/skills/fixtures/usage.md @@ -119,6 +119,18 @@ donna -p llm status When the workflow is finished, report the result to the developer before starting unrelated work. +### Nested Workflows + +Donna can have multiple active workflows in one session. Treat them as a call stack: starting a workflow while another workflow is active makes the new workflow current; finishing it returns control to the parent workflow. + +A parent action request may instruct you to run a child workflow. In that case: + +1. Start the child workflow with `donna -p llm run ...`. +2. Complete the child workflow before completing the parent action request. +3. Do not report the parent action request as complete just because the child workflow started. +4. After the child finishes, return to the parent action request and choose one of its declared transitions. +5. Keep child workflow outputs explicit: write files, update notes, or summarize results before completing the parent request. + ### Start Workflow A developer or project instruction explicitly asks you to run a Donna workflow, and Donna has no active work that must be continued first. @@ -171,7 +183,7 @@ Strictly follow every instruction Donna gives you after that. If Donna finishes A developer or an active Donna workflow asks you to create or modify a workflow. -Read the workflow authoring instructions if you have not already: +Read the workflow creation instructions if you have not already: ```bash donna -p llm skill workflows diff --git a/donna/skills/fixtures/workflows.md b/donna/skills/fixtures/workflows.md index bda1630c..7a3e576b 100644 --- a/donna/skills/fixtures/workflows.md +++ b/donna/skills/fixtures/workflows.md @@ -376,6 +376,44 @@ Execution loop: 7. Donna validates that the transition is allowed, queues the next operation, and continues. 8. `finish` completes the task. +## Workflow Stack And Child Workflows + +Donna can have multiple active workflows in one session. Treat them as a call stack: + +- The latest started workflow is the current workflow. +- Starting a workflow while another workflow is active pushes a child workflow on top of the stack. +- Finishing the child workflow pops it from the stack. +- After the child workflow finishes, the parent workflow becomes current again. + +A child workflow may be started by the developer, by the agent while following a parent action request, or by future Donna operations. This is useful when a parent workflow delegates a substantial subtask to a specialized workflow. + +The primary way to start a child workflow from a parent workflow is to ask the agent in a `request_action` operation. There are two common ways to write that request: + +- Specify the workflow path explicitly when the parent workflow requires one exact child workflow. +- Specify the task to complete and ask the agent to choose the best workflow for it. + +Prefer task-based delegation when possible. It allows dynamic behavior: the parent workflow depends on the child workflow's role, not on one concrete workflow artifact. This works like a simple form of polymorphism. + +Example with an explicit child workflow: + +```markdown +1. Run the workflow `@/workflows/prepare-release-notes.donna.md`. +2. Complete that child workflow. +3. Return to this action request. +4. If release notes are ready, `{{ donna.lib.goto("verify_release") }}`. +5. If they are blocked, `{{ donna.lib.goto("handle_release_notes_blocker") }}`. +``` + +Example with task-based child workflow selection: + +```markdown +1. Choose the best workflow for preparing release notes. +2. Run that workflow and complete it. +3. Return to this action request. +4. If release notes are ready, `{{ donna.lib.goto("verify_release") }}`. +5. If they are blocked, `{{ donna.lib.goto("handle_release_notes_blocker") }}`. +``` + ## Directives And Rendering Donna renders workflow Markdown with Jinja directives. @@ -562,7 +600,7 @@ If a script failure output is missing from a later request action: 2. Check that `task_variable` uses the exact same key. 3. Check that the operation reading the variable runs after the script operation. -## Authoring Checklist +## Workflow Checklist Before considering a workflow ready: From 34af62a88e4c69ad4e3a90ff5f2cb32de848b0b8 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 12:07:51 +0200 Subject: [PATCH 18/98] specs, begining --- specs/intro.md | 21 ++++++++ specs/meta/general.md | 109 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 specs/intro.md create mode 100644 specs/meta/general.md diff --git a/specs/intro.md b/specs/intro.md new file mode 100644 index 00000000..4d8b18ad --- /dev/null +++ b/specs/intro.md @@ -0,0 +1,21 @@ +# List of specifications + +## Goal of the document + +This document lists all specification documents and specification directories in the project and briefly describes their purpose. + +## Scope + +The scope of this specification is limited to the specification index. + +Detailed requirements for individual specifications are out of scope except for brief descriptions needed to keep the index useful. + +## Specification directories + +- `./specs/` — directory with all specifications. +- `./specs/meta/` — specifications related to requirements for specification documents. + +## Specification documents + +- `./specs/intro.md` — this file, contains a list of all specifications and their brief descriptions. +- `./specs/meta/general.md` — general requirements for specification documents. diff --git a/specs/meta/general.md b/specs/meta/general.md new file mode 100644 index 00000000..ff57d783 --- /dev/null +++ b/specs/meta/general.md @@ -0,0 +1,109 @@ +# General specification requirements + +## Goal of the document + +This document describes the general requirements for specifications in this project. + +## Scope + +The scope of this specification is limited to requirements for specification documents in this project. + +The following topics are out of scope except when they affect how specification documents should be written: + +- project behavior. +- implementation requirements. +- product requirements. + +## Dictionary + +- `specification` — a Markdown document in `./specs/` that describes requirements, behavior, terminology, or documentation rules for the project. +- `top-level section` — a section introduced by an `h2` Markdown header. +- `nested section` — a section introduced by an `h3` or deeper Markdown header. + +## Sections + +A specification MUST contain a single `h1` header with the name of the specification, which SHOULD be unique across all specifications. + +Top-level information SHOULD be organized in sections with `h2` headers. + +Nested sections MAY use `h3`, `h4`, and deeper headers when they make the document easier to navigate. + +Nested sections SHOULD be used for details that belong to a parent top-level section, such as: + +- examples. +- option descriptions. +- record fields. +- subsections of a larger topic. + +Sections that are mandatory for all specifications: + +- `Goal of the document` — a brief description of what the specification is about and what it aims to achieve. +- `Scope` — a brief description of what the specification covers and what it intentionally does not cover. + +Optional sections: + +- `Dictionary` — a list of terms that are specific to the specification. + +The first sections of a specification SHOULD be placed in this order: + +1. `Goal of the document` +2. `Scope` +3. `Dictionary`, when the section exists + +The `Goal of the document` section MUST describe the content and purpose of the document. + +The `Goal of the document` section MUST NOT define requirements for the document itself, such as saying that the document: + +- MUST define something. +- MUST list something. +- MUST describe something. + +The `Scope` section MUST describe the boundaries of the specification. It SHOULD be descriptive rather than normative when it explains what the document covers. It SHOULD explicitly mention important topics that are out of scope when those boundaries are useful for readers or future authors. It MUST NOT explain where to find requirements that belong to other specifications. + +The `Dictionary` section SHOULD be placed immediately after the `Scope` section. It SHOULD contain only terms that are specific to the specification. Terms that are used by multiple specifications SHOULD be defined in `./specs/dictionary.md`. + +## Style + +- Specifications MUST use Markdown syntax for formatting the document. +- Specifications MUST follow [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119). +- Specifications MUST NOT break long lines to fit within 80 characters or any other number; they MUST use as many characters as needed to express the idea clearly. +- Long enumerations SHOULD be organized as Markdown lists when possible. + +## Abstraction level + +Specifications MUST describe project behavior, architecture, constraints, terminology, and compatibility contracts at the highest level that is still precise enough to guide implementation. + +Specifications SHOULD define: + +- externally visible behavior and data contracts. +- stable architectural boundaries and ownership responsibilities. +- constraints that must hold across implementations. +- technology choices when they are part of the intended architecture. +- examples that clarify the requirement being specified. + +Specifications MUST NOT define incidental implementation details. + +Incidental implementation details include: + +- private helper function names. +- exact class names that are not part of a stable project convention or public boundary. +- exact file paths for code that is not owned by a module-layout or ownership requirement. +- local constructor signatures. +- temporary implementation strategies. +- repeated examples that restate ownership already defined elsewhere without adding a new constraint. + +Specifications MAY name concrete files, modules, symbols, commands, or formats when the name itself is a stable contract. + +Stable contracts include: + +- public CLI commands, options, arguments, and output records. +- configuration file names, fields, and values. +- module ownership boundaries defined by the module-layout specification. +- naming conventions that all implementations are expected to follow. +- concrete dependencies or language features that are intentional architectural choices. + +When a requirement can be expressed either as an implementation detail or as a general architectural rule, the specification MUST prefer the general rule. + +For example, a specification SHOULD require closed sets of named values to use enums instead of raw strings. It SHOULD NOT require a specific enum class name or file location unless that class name or location is itself a stable architectural boundary. + +Examples in specifications SHOULD illustrate behavior or ownership. Examples SHOULD NOT be treated as a place to enumerate every current implementation file or symbol. From 527571738dd62e00c08360dc67220bc40481937c Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 13:04:16 +0200 Subject: [PATCH 19/98] behavior specs --- README.md | 2 +- donna/cli/application.py | 6 +- donna/cli/commands/workspaces.py | 4 +- donna/cli/entities.py | 2 +- donna/cli/types.py | 16 +- donna/cli/utils.py | 15 +- donna/domain/paths.py | 2 +- donna/primitives/directives/goto.py | 4 +- donna/protocol/formatters/automation.py | 4 +- donna/skills/fixtures/initialization.md | 6 +- donna/skills/fixtures/usage.md | 10 +- donna/skills/fixtures/workflows.md | 2 +- donna/workspaces/config.py | 7 +- donna/workspaces/errors.py | 22 + donna/workspaces/initialization.py | 46 +- specs/behavior/cli.md | 587 ++++++++++++++++++++++++ specs/behavior/config.md | 295 ++++++++++++ specs/behavior/file_paths.md | 298 ++++++++++++ specs/behavior/skill_fixtures.md | 172 +++++++ specs/intro.md | 5 + 20 files changed, 1448 insertions(+), 57 deletions(-) create mode 100644 specs/behavior/cli.md create mode 100644 specs/behavior/config.md create mode 100644 specs/behavior/file_paths.md create mode 100644 specs/behavior/skill_fixtures.md diff --git a/README.md b/README.md index 039cfd2f..860da1bf 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ What you may notice: Directives, like `{{ goto("operation_id") }}`, render itself depending on the context: -- For the agent, they render an exact CLI command to run, such as `donna -p llm complete-action-request '@/workflows/polish.donna.md:finish'`. +- For the agent, they render an exact CLI command to run, such as `donna -p llm --config /path/to/project/donna.toml complete-action-request '@/workflows/polish.donna.md:finish'`. - For Donna, they render a specific marker that can be extracted and used to analyze an artifact. For example, Donna uses `goto` directives to build an FSM of the workflow and validate it before running: does each operation exist, can the workflow be completed, are there unreachable operations, etc. Generally speaking, **all you need is `donna.lib.request_action` operation** — it is enough to achieve a great deal of automation by delegating some decisions to the agent. However, there are some more specific operations that simplify things and make workflows more agile or performant. diff --git a/donna/cli/application.py b/donna/cli/application.py index 1d96c477..71879c56 100644 --- a/donna/cli/application.py +++ b/donna/cli/application.py @@ -1,7 +1,7 @@ import typer from donna.cli.entities import GLOBAL_OPTIONS_CONTEXT_KEY, GlobalOptions -from donna.cli.types import ProtocolModeOption, RootOption +from donna.cli.types import ConfigOption, ProtocolModeOption from donna.domain.paths import UntrustedPath from donna.protocol.modes import Mode @@ -12,10 +12,10 @@ def initialize( context: typer.Context, protocol: ProtocolModeOption = Mode.human, - root_dir: RootOption = None, + config_path: ConfigOption = None, ) -> None: context.meta[GLOBAL_OPTIONS_CONTEXT_KEY] = GlobalOptions( - protocol=protocol, root_dir=None if root_dir is None else UntrustedPath(root_dir) + protocol=protocol, config_path=None if config_path is None else UntrustedPath(config_path) ) diff --git a/donna/cli/commands/workspaces.py b/donna/cli/commands/workspaces.py index 7d80705b..69374dd3 100644 --- a/donna/cli/commands/workspaces.py +++ b/donna/cli/commands/workspaces.py @@ -9,8 +9,8 @@ @app.command(help="Initialize Donna project config.") def init(context: typer.Context) -> None: with command_context(context, load_environment=False) as command: - target_dir = command.target_dir() + config_path = command.target_config_path() - initialize_workspace(target_dir).unwrap() + initialize_workspace(config_path).unwrap() command.write_cells([operation_succeeded("Donna project initialized successfully")]) diff --git a/donna/cli/entities.py b/donna/cli/entities.py index c67da7fc..c2b5a827 100644 --- a/donna/cli/entities.py +++ b/donna/cli/entities.py @@ -7,4 +7,4 @@ class GlobalOptions(BaseEntity): protocol: Mode - root_dir: UntrustedPath | None = None + config_path: UntrustedPath | None = None diff --git a/donna/cli/types.py b/donna/cli/types.py index b97be602..62cbd682 100644 --- a/donna/cli/types.py +++ b/donna/cli/types.py @@ -169,19 +169,15 @@ def _parse_input_path(value: str) -> UntrustedPath: ] -RootOption = Annotated[ +ConfigOption = Annotated[ pathlib.Path | None, typer.Option( - "--root", - "-r", + "--config", resolve_path=True, - file_okay=False, - dir_okay=True, - exists=True, - help=( - "Optional project root directory. " - "If omitted, Donna discovers it by searching parent directories for donna.toml." - ), + file_okay=True, + dir_okay=False, + exists=False, + help="Optional project config file. If omitted, Donna discovers donna.toml by searching parent directories.", ), ] diff --git a/donna/cli/utils.py b/donna/cli/utils.py index ef7ffa50..df7b53dd 100644 --- a/donna/cli/utils.py +++ b/donna/cli/utils.py @@ -8,7 +8,8 @@ from donna.cli.entities import GLOBAL_OPTIONS_CONTEXT_KEY, GlobalOptions from donna.core.errors import EnvironmentError, ErrorsList from donna.core.result import UnwrapError -from donna.domain.paths import PathInput, UntrustedPath +from donna.domain.constants import DONNA_CONFIG_NAME +from donna.domain.paths import PathInput, ProjectConfigPath, UntrustedPath from donna.protocol.cells import Cell from donna.protocol.modes import Mode, get_cell_formatter from donna.workspaces import config as workspace_config @@ -44,13 +45,19 @@ def install_protocol(self) -> None: workspace_config.protocol.set(self.protocol) def load_workspace(self) -> workspace_config.Workspace: - workspace = load_workspace(root_dir=self.global_options.root_dir).unwrap() + workspace = load_workspace(config_path=self.global_options.config_path).unwrap() workspace_config.install_workspace(workspace) return workspace + def target_config_path(self) -> ProjectConfigPath: + if self.global_options.config_path is not None: + return ProjectConfigPath(self.global_options.config_path) + + return ProjectConfigPath(pathlib.Path.cwd() / DONNA_CONFIG_NAME) + def target_dir(self) -> PathInput: - if self.global_options.root_dir is not None: - return self.global_options.root_dir + if self.global_options.config_path is not None: + return UntrustedPath(pathlib.Path(self.global_options.config_path).parent) if workspace_config.project_dir.is_set(): return workspace_config.project_dir() diff --git a/donna/domain/paths.py b/donna/domain/paths.py index 2e948087..0f3fbc5e 100644 --- a/donna/domain/paths.py +++ b/donna/domain/paths.py @@ -9,4 +9,4 @@ RelativeProjectPath = NewType("RelativeProjectPath", Path) ResolvedProjectPath = NewType("ResolvedProjectPath", Path) UntrustedPath = NewType("UntrustedPath", Path) -PathInput = UntrustedPath | ProjectRootPath +PathInput = UntrustedPath | ProjectRootPath | ProjectConfigPath diff --git a/donna/primitives/directives/goto.py b/donna/primitives/directives/goto.py index 132d1bbc..c52d13e3 100644 --- a/donna/primitives/directives/goto.py +++ b/donna/primitives/directives/goto.py @@ -38,9 +38,9 @@ def _prepare_arguments( def render_view(self, context: Context, next_operation_id: ArtifactSectionId) -> Result[Any, ErrorsList]: protocol = workspace_config.protocol().value - root_dir = workspace_config.project_dir() + config_path = workspace_config.config_path() return Ok( - f"donna -p {protocol} -r '{root_dir}' " + f"donna -p {protocol} --config '{config_path}' " f"complete-action-request '{next_operation_id}'" ) diff --git a/donna/protocol/formatters/automation.py b/donna/protocol/formatters/automation.py index 39ca6406..f3fb92e7 100644 --- a/donna/protocol/formatters/automation.py +++ b/donna/protocol/formatters/automation.py @@ -8,7 +8,9 @@ class Formatter(BaseFormatter): def _json_line(self, data: object) -> bytes: - return json.dumps(data, ensure_ascii=False, indent=None, separators=(",", ":"), sort_keys=True).encode() + b"\n" + return ( + json.dumps(data, ensure_ascii=False, indent=None, separators=(",", ":"), sort_keys=True).encode() + b"\n" + ) def format_cell(self, cell: Cell) -> bytes: data: dict[str, str | int | bool | None] = {"id": cell.short_id} diff --git a/donna/skills/fixtures/initialization.md b/donna/skills/fixtures/initialization.md index d2bc60ec..35190ae5 100644 --- a/donna/skills/fixtures/initialization.md +++ b/donna/skills/fixtures/initialization.md @@ -22,7 +22,7 @@ donna -p llm skill usage Run initialization from the directory that should contain `donna.toml`. -Without `--root`, `donna init` creates `donna.toml` in the current working directory. With `--root PATH`, it creates `donna.toml` in that existing directory and treats it as the project root for later commands. +Without `--config`, `donna init` creates `donna.toml` in the current working directory. With `--config PATH`, it creates the configuration file at that path and treats the containing directory as the project root for later commands. The project root matters because Donna discovers it by locating `donna.toml`. `session_dir` and `workflow_dirs` are relative to that directory, and Donna reports workflow artifacts as `@/` anchored at that directory. Relative artifact paths passed to commands are resolved from the command's current working directory and accepted only when they point inside the project root. @@ -34,10 +34,10 @@ Create `donna.toml` in the current directory: donna -p llm init ``` -Create it in an explicit project root: +Create it at an explicit config path: ```bash -donna -p llm --root /path/to/project init +donna -p llm --config /path/to/project/donna.toml init ``` `init` does not overwrite an existing file. diff --git a/donna/skills/fixtures/usage.md b/donna/skills/fixtures/usage.md index ab451cb6..a40764f1 100644 --- a/donna/skills/fixtures/usage.md +++ b/donna/skills/fixtures/usage.md @@ -51,17 +51,17 @@ Example automation output: Global options go before the subcommand: ```bash -donna -p llm --root /path/to/project status +donna -p llm --config /path/to/project/donna.toml status ``` ## Project Root -Most commands need a Donna project. If `--root/-r` is omitted, Donna discovers the project root by searching upward from the current working directory for `donna.toml`. +Most commands need a Donna project. If `--config` is omitted, Donna discovers the project root by searching upward from the current working directory for `donna.toml`. -Use `--root PATH` when running from outside the intended project tree or when there is any ambiguity: +Use `--config PATH` when running from outside the intended project tree or when there is any ambiguity. The path points to the active `donna.toml`; the project root is the directory that contains it: ```bash -donna -p llm --root /path/to/project list +donna -p llm --config /path/to/project/donna.toml list ``` Donna uses project-root anchored ids for workflow artifacts. The `@/` prefix means "from the Donna project root": @@ -303,7 +303,7 @@ donna -p llm init Initialize Donna in an explicit directory: ```bash -donna -p llm --root /path/to/project init +donna -p llm --config /path/to/project/donna.toml init ``` `init` creates or refreshes Donna project configuration. Read `donna -p llm skill initialization` and project instructions before using it in an existing repository. diff --git a/donna/skills/fixtures/workflows.md b/donna/skills/fixtures/workflows.md index 7a3e576b..6ff90aab 100644 --- a/donna/skills/fixtures/workflows.md +++ b/donna/skills/fixtures/workflows.md @@ -573,7 +573,7 @@ If a workflow is not listed: 1. Check that the file ends with `.donna.md`. 2. Check that it is under one of `workflow_dirs`. 3. Check that the workflow directory exists. -4. Run `donna -p llm list` from the intended Donna project root or pass `--root`. +4. Run `donna -p llm list` from the intended Donna project root or pass `--config /path/to/project/donna.toml`. If validation says a start operation is missing: diff --git a/donna/workspaces/config.py b/donna/workspaces/config.py index 76f48639..8385b9e0 100644 --- a/donna/workspaces/config.py +++ b/donna/workspaces/config.py @@ -9,7 +9,7 @@ from donna.domain.constants import DONNA_DEFAULT_SESSION_DIR, DONNA_DEFAULT_WORKFLOW_DIR from donna.domain.id_paths import NormalizedRawIdPath from donna.domain.ids import SectionId -from donna.domain.paths import ProjectRootPath, RelativeProjectPath +from donna.domain.paths import ProjectConfigPath, ProjectRootPath, RelativeProjectPath from donna.domain.python_path import PythonPath from donna.workspaces import errors as world_errors @@ -122,6 +122,7 @@ class Workspace(BaseEntity): model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) root: ProjectRootPath + config_path: ProjectConfigPath config: Config @@ -151,6 +152,7 @@ def __call__(self) -> V: project_dir = GlobalConfig[ProjectRootPath]() +config_path = GlobalConfig[ProjectConfigPath]() config = GlobalConfig[Config]() protocol: GlobalConfig["Mode"] = GlobalConfig() @@ -159,5 +161,8 @@ def install_workspace(workspace: Workspace) -> None: if not project_dir.is_set(): project_dir.set(ProjectRootPath(workspace.root)) + if not config_path.is_set(): + config_path.set(ProjectConfigPath(workspace.config_path)) + if not config.is_set(): config.set(workspace.config) diff --git a/donna/workspaces/errors.py b/donna/workspaces/errors.py index 04c35289..0e166312 100644 --- a/donna/workspaces/errors.py +++ b/donna/workspaces/errors.py @@ -43,6 +43,28 @@ class WorkspaceAlreadyInitialized(WorkspaceError): config_path: ProjectConfigPath +class WorkspaceConfigNotFound(WorkspaceError): + code: str = "donna.workspaces.config_not_found" + message: str = "Donna project config does not exist at `{error.config_path}`" + ways_to_fix: list[str] = [ + "Check the config path for typos.", + "Create the Donna project config via CLI command if it does not exist yet.", + "Omit `--config` to discover `donna.toml` from the current working directory.", + ] + config_path: ProjectConfigPath + + +class WorkspaceConfigDirNotFound(WorkspaceError): + code: str = "donna.workspaces.config_dir_not_found" + message: str = "Donna project config directory does not exist for `{error.config_path}`" + ways_to_fix: list[str] = [ + "Check the config path for typos.", + "Create the directory that should contain the Donna project config.", + "Choose an existing project directory for the Donna project config.", + ] + config_path: ProjectConfigPath + + class JournalCommandConfigInvalid(WorkspaceError): code: str = "donna.workspaces.journal_command_config_invalid" message: str = "Journal command config is invalid: {error.details}" diff --git a/donna/workspaces/initialization.py b/donna/workspaces/initialization.py index 5c4a4e97..0886a215 100644 --- a/donna/workspaces/initialization.py +++ b/donna/workspaces/initialization.py @@ -16,36 +16,35 @@ @unwrap_to_error -def load_workspace(root_dir: PathInput | None = None) -> Result[config.Workspace, core_errors.ErrorsList]: +def load_workspace(config_path: PathInput | None = None) -> Result[config.Workspace, core_errors.ErrorsList]: """Load workspace configuration without mutating process-global state.""" - if root_dir is None: + if config_path is None: project_dir = utils.discover_project_dir(DONNA_CONFIG_NAME).unwrap() + resolved_config_path = ProjectConfigPath(pathlib.Path(project_dir) / DONNA_CONFIG_NAME) else: - project_dir = resolve_project_root(UntrustedPath(root_dir)) - if not (pathlib.Path(project_dir) / DONNA_CONFIG_NAME).is_file(): - return Err([core_errors.ProjectDirNotFound(config_name=DONNA_CONFIG_NAME)]) + resolved_config_path = ProjectConfigPath(pathlib.Path(config_path).expanduser().resolve()) + project_dir = resolve_project_root(UntrustedPath(pathlib.Path(resolved_config_path).parent)) - config_path = ProjectConfigPath(pathlib.Path(project_dir) / DONNA_CONFIG_NAME) - - if not config_path.exists(): - return Ok(config.Workspace(root=project_dir, config=config.Config())) + if not pathlib.Path(resolved_config_path).is_file(): + return Err([world_errors.WorkspaceConfigNotFound(config_path=resolved_config_path)]) try: - data = tomllib.loads(config_path.read_text(encoding="utf-8")) + data = pathlib.Path(resolved_config_path).read_text(encoding="utf-8") + parsed = tomllib.loads(data) except tomllib.TOMLDecodeError as e: - return Err([world_errors.ConfigParseFailed(config_path=config_path, details=str(e))]) + return Err([world_errors.ConfigParseFailed(config_path=resolved_config_path, details=str(e))]) try: - loaded_config = config.Config.model_validate(data) + loaded_config = config.Config.model_validate(parsed) except Exception as e: - return Err([world_errors.ConfigValidationFailed(config_path=config_path, details=str(e))]) + return Err([world_errors.ConfigValidationFailed(config_path=resolved_config_path, details=str(e))]) - return Ok(config.Workspace(root=project_dir, config=loaded_config)) + return Ok(config.Workspace(root=project_dir, config_path=resolved_config_path, config=loaded_config)) @unwrap_to_error def initialize_runtime( - root_dir: PathInput | None = None, + config_path: PathInput | None = None, protocol: Mode | None = None, ) -> Result[config.Workspace, core_errors.ErrorsList]: """Initialize the runtime environment for the application. @@ -55,27 +54,30 @@ def initialize_runtime( if protocol is not None: config.protocol.set(protocol) - workspace = load_workspace(root_dir=root_dir).unwrap() + workspace = load_workspace(config_path=config_path).unwrap() config.install_workspace(workspace) return Ok(workspace) @unwrap_to_error -def initialize_workspace(project_dir: PathInput) -> Result[config.Workspace, core_errors.ErrorsList]: +def initialize_workspace(config_path: PathInput) -> Result[config.Workspace, core_errors.ErrorsList]: """Initialize Donna project configuration.""" - project_dir = ProjectRootPath(pathlib.Path(project_dir).resolve()) - config_path = ProjectConfigPath(pathlib.Path(project_dir) / DONNA_CONFIG_NAME) + config_path = ProjectConfigPath(pathlib.Path(config_path).expanduser().resolve()) + project_dir = ProjectRootPath(pathlib.Path(config_path).parent) + + if not pathlib.Path(project_dir).is_dir(): + return Err([world_errors.WorkspaceConfigDirNotFound(config_path=config_path)]) - if config_path.exists(): + if pathlib.Path(config_path).exists(): return Err([world_errors.WorkspaceAlreadyInitialized(config_path=config_path)]) config_text = ( importlib.resources.files(__package__).joinpath("fixtures", BASE_CONFIG_FIXTURE).read_text(encoding="utf-8") ) - config_path.write_text(config_text, encoding="utf-8") + pathlib.Path(config_path).write_text(config_text, encoding="utf-8") - workspace = load_workspace(root_dir=project_dir).unwrap() + workspace = load_workspace(config_path=config_path).unwrap() config.install_workspace(workspace) return Ok(workspace) diff --git a/specs/behavior/cli.md b/specs/behavior/cli.md new file mode 100644 index 00000000..667ca76c --- /dev/null +++ b/specs/behavior/cli.md @@ -0,0 +1,587 @@ +# CLI Interface + +## Goal of the document + +This document describes how `donna` behaves as a command line interface, including: + +- how agents, users, and tools invoke it. +- which commands and arguments are accepted. +- which output protocols are supported. +- what each command does at the CLI boundary. + +## Scope + +The scope of this specification is limited to CLI behavior. + +The following topics are out of scope: + +- workflow operation semantics. +- Markdown artifact parsing rules. +- configuration file field semantics. +- internal session state representation. +- exact prose emitted by built-in skill documents. + +This specification may refer to the following concepts only to describe how the CLI accepts arguments and renders output: + +- Donna project roots. +- Donna artifact ids. +- artifact section ids. +- workflow artifacts. +- action requests. +- session state. + +## General behavior + +`donna` is a command line tool that helps agents run predefined workflows in a deterministic way. It maintains project-local session state, discovers workflow artifacts, runs workflow operations, emits action requests for agents, and accepts agent reports about the next operation to run. + +The CLI has four primary command areas: + +- `donna run ...` — start a workflow artifact in the current session. +- `donna continue` and `donna complete-action-request ...` — advance existing session work. +- `donna list`, `donna render ...`, and `donna validate ...` — inspect and validate workflow artifacts. +- `donna skill [DOCUMENT]` — print built-in agent-oriented documentation. + +The root command MUST be a command group. + +Global options, when present, MUST be provided before the subcommand: + +```bash +donna [GLOBAL_OPTIONS] COMMAND [COMMAND_OPTIONS] +``` + +The CLI MUST write requested command output to stdout. + +Diagnostics that are not part of the requested output SHOULD be represented as Donna error cells when the selected protocol has already been installed. + +For `automation` output, stdout MUST contain only JSON Lines records when command output is produced through Donna cells or journal records. + +The CLI MUST produce deterministic output for the same: + +- input. +- configuration. +- working directory. +- project state. + +Commands that load a workspace MUST discover or use a Donna configuration file before executing command-specific behavior. + +`donna skill ...`, `donna init`, and `donna version` MUST NOT require an existing Donna project configuration. + +## Commands + +The CLI MUST support these commands and command forms: + +- `donna [GLOBAL_OPTIONS] init` — create a starter `donna.toml`. +- `donna [GLOBAL_OPTIONS] list` — list discovered workflow artifacts. +- `donna [GLOBAL_OPTIONS] render [OPTIONS] ARTIFACT` — render one artifact. +- `donna [GLOBAL_OPTIONS] validate [OPTIONS] [ARTIFACT...]` — validate selected artifacts or every discovered artifact. +- `donna [GLOBAL_OPTIONS] new-session` — create fresh session state. +- `donna [GLOBAL_OPTIONS] continue` — continue queued workflow execution in the current session. +- `donna [GLOBAL_OPTIONS] status` — show concise session status. +- `donna [GLOBAL_OPTIONS] details` — show detailed session state. +- `donna [GLOBAL_OPTIONS] run WORKFLOW` — start a workflow artifact in the current session. +- `donna [GLOBAL_OPTIONS] complete-action-request ACTION_REQUEST_ID NEXT_OPERATION` — complete an action request and continue with the selected operation. +- `donna [GLOBAL_OPTIONS] skill [DOCUMENT]` — print built-in agent-oriented documentation for using `donna`. +- `donna [GLOBAL_OPTIONS] version` — print the tool version. +- `donna --help` — print root help information. + +The root command MUST NOT start or continue workflow execution directly. + +## Output behavior + +All output MUST use UTF-8. + +Command output produced through Donna's protocol layer MUST be represented as Donna cells. + +The `render` command MUST write rendered Markdown directly. + +The `version` command MUST print a plain version line. + +Help and command line parsing output MAY use Typer's standard rendering. + +Commands MAY emit Donna journal records while executing. Journal records are command output when they are printed by the selected protocol formatter. + +Output MUST NOT contain terminal color or styling escape sequences. + +## Output protocols + +The CLI MUST support three output protocols: + +- `human` — default protocol for terminal users. +- `llm` — text protocol optimized for coding agents that invoke `donna` as a tool. +- `automation` — protocol optimized for programs; output is serialized as JSON Lines. + +`human` and `llm` MUST be separate protocols. + +For commands that support output protocols, the output protocol MUST be selected with the global option: + +```bash +--protocol PROTOCOL +-p PROTOCOL +``` + +Allowed values MUST be: + +- `human` +- `llm` +- `automation` + +If no protocol is provided, the default protocol MUST be `human`. + +### Human output + +Human output SHOULD be compact terminal text. + +Human cell output MUST include a visible cell boundary header with the generated cell id. + +Human cell output MUST render cell metadata as `key = value` lines. + +Human journal output SHOULD include the time, current task id when present, actor id, and message. + +### LLM output + +The `llm` protocol MUST be used when a coding agent invokes `donna` as a tool. + +LLM cell output MUST use explicit machine-readable boundary lines: + +```text +--DONNA-CELL BEGIN-- +--DONNA-CELL END-- +``` + +LLM cell output MUST render cell metadata as `key=value` lines. + +LLM output SHOULD be stable and self-contained for coding agents that receive the output as a tool result. + +LLM journal output SHOULD include the full timestamp, current task id, actor id, current work unit id, current operation id, and message. + +### Automation output + +Automation output MUST be serialized as JSON Lines. + +Automation output MUST write one JSON object per line. + +Automation output MUST use stable field names. + +Automation cell output MUST include: + +```json +{"id":"generated cell id","content":"cell content or null"} +``` + +Automation cell output MUST include cell metadata as top-level JSON object fields. + +Automation output MUST sort JSON object keys. + +Automation journal output MUST serialize the journal record as one JSON object per line. + +Additional fields MAY be added in future versions. Consumers MUST ignore unknown fields. + +## Donna cells + +A Donna cell is the protocol-level output unit used by most CLI commands. + +Each Donna cell MUST have: + +- a generated `id`. +- a `kind`. +- optional `media_type`. +- optional `content`. +- metadata fields. + +Cell ids MAY be generated at runtime. Consumers MUST NOT treat generated cell ids as deterministic identifiers. + +Cell metadata fields MUST be rendered in deterministic order by metadata key when the selected formatter emits ordered metadata. + +Cell content MUST have a media type when content is present. + +Commands MAY emit multiple cells for one invocation. + +### Human cell example + +Human protocol cell output SHOULD follow this shape: + +```text +----- DONNA CELL ----- +kind = session_state_status +media_type = text/markdown +pending_action_requests = 0 +queued_work_units = 0 +tasks = 0 + +The session is IDLE. + +``` + +### LLM cell example + +LLM protocol cell output SHOULD follow this shape: + +```text +--DONNA-CELL BEGIN-- +kind=session_state_status +media_type=text/markdown +pending_action_requests=0 +queued_work_units=0 +tasks=0 + +The session is IDLE. +--DONNA-CELL END-- +``` + +### Automation cell example + +Automation protocol cell output SHOULD follow this shape: + +```json +{"content":"The session is IDLE.","id":"","pending_action_requests":0,"queued_work_units":0,"tasks":0} +``` + +## Global options + +### `-h`, `--help` + +`-h` and `--help` MUST print help information and exit with status `0`. + +Example: + +```bash +donna --help +``` + +### `-p`, `--protocol PROTOCOL` + +`-p` and `--protocol PROTOCOL` MUST be global options accepted before the subcommand. + +The selected protocol MUST be available to every subcommand. + +Subcommands that render Donna cells or journal records MUST use the selected protocol. + +Allowed values MUST be: + +- `human` +- `llm` +- `automation` + +### `--config PATH` + +`--config PATH` MUST be a global option accepted before the subcommand. + +The config path MUST identify a local TOML configuration file for commands that load a workspace. + +When provided to a command that loads a workspace, `PATH` MUST be used as the active Donna configuration file and Donna MUST NOT perform upward discovery. + +When `PATH` is relative, it MUST be resolved relative to the current working directory. + +The project root MUST be the directory containing the active configuration file. + +When omitted, commands that load a workspace MUST discover `donna.toml` by searching from the current working directory toward the filesystem root. + +Subcommands that do not load workspace configuration MAY use `PATH` to derive their target directory or target configuration file. + +## Artifact id arguments + +CLI arguments that identify Donna artifacts MUST be accepted as: + +- root-anchored artifact ids. +- relative filesystem paths that resolve inside the Donna project root. +- absolute filesystem paths that resolve inside the Donna project root. + +Accepted artifact arguments MUST normalize to canonical root-anchored artifact ids before command-specific behavior uses them. + +Artifact arguments used by workflow artifact commands MUST identify files with the Donna artifact extension: + +```text +.donna.md +``` + +Artifact arguments that load existing workflow artifacts MUST identify artifacts visible through configured workflow directories. + +Artifact section arguments MUST use artifact section id syntax: + +```text +@/path/to/workflow.donna.md:section_id +``` + +Artifact section arguments MUST normalize the artifact part as an artifact id and validate the section id part as a Donna section id. + +## `donna init` command + +The `init` command MUST create a starter Donna configuration file. + +```bash +donna init +donna --config /path/to/project/donna.toml init +``` + +When no `--config` path is provided, the command MUST create `donna.toml` in the current working directory. + +When `--config PATH` is provided, the command MUST create the configuration file at that path and use the directory containing the file as the project root. + +When `--config PATH` is provided, the directory containing `PATH` MUST exist. + +The command MUST NOT discover an existing configuration file in parent directories. + +The command MUST NOT overwrite an existing configuration file. + +The generated configuration MUST be valid TOML and use schema version `1`. + +The command MUST render a success cell when initialization succeeds. + +The command MUST NOT accept artifact arguments, session arguments, or skill document arguments. + +## `donna list` command + +The `list` command MUST list workflow artifacts discovered under configured workflow directories. + +```bash +donna list +``` + +The command MUST load workspace configuration. + +The command MUST render one status cell per discovered artifact. + +Discovered artifacts MUST be ordered deterministically by configured workflow directory order and filesystem traversal order. + +Duplicate artifact ids discovered through multiple workflow directories MUST be emitted once. + +Missing workflow directories MUST be ignored. + +The command MUST NOT accept artifact arguments or session arguments. + +## `donna render` command + +The `render` command MUST render one artifact with the selected render mode and write rendered Markdown to stdout. + +```bash +donna render --mode MODE ARTIFACT +``` + +`ARTIFACT` MUST be an artifact id or path accepted by Donna artifact id normalization. + +The `--mode MODE` option MUST be required. + +Allowed render modes MUST include: + +- `view` +- `execute` +- `analysis` + +The rendered artifact output MUST be written as raw Markdown rather than wrapped in a Donna cell. + +This command MAY still emit Donna journal records before the rendered Markdown when artifact rendering logs command activity. + +## `donna validate` command + +The `validate` command MUST validate selected workflow artifacts or every discovered workflow artifact. + +```bash +donna validate ARTIFACT... +donna validate --all +``` + +The command MUST require exactly one of: + +- one or more artifact arguments. +- `--all`. + +The command MUST fail when `--all` is used together with one or more artifact arguments. + +When artifact arguments are provided, the command MUST normalize and validate each artifact id. + +When `--all` is provided, the command MUST validate every discovered workflow artifact. + +If validation finds errors, the command MUST render error cells. + +If validation succeeds, the command MUST render a success cell. + +## `donna new-session` command + +The `new-session` command MUST create fresh session state. + +```bash +donna new-session +``` + +The command MUST load workspace configuration. + +The command MUST operate on the session stored under the configured session directory. + +The command MUST render resulting session cells. + +## `donna status` command + +The `status` command MUST show concise session status. + +```bash +donna status +``` + +The command MUST load workspace configuration. + +The command MUST operate on the session stored under the configured session directory. + +The output MUST include whether Donna is idle or has pending action requests. + +## `donna details` command + +The `details` command MUST show detailed session state. + +```bash +donna details +``` + +The command MUST load workspace configuration. + +The command MUST operate on the session stored under the configured session directory. + +The output MUST include action requests when they are present in the session state. + +## `donna continue` command + +The `continue` command MUST continue queued workflow execution. + +```bash +donna continue +``` + +The command MUST load workspace configuration. + +The command MUST operate on the session stored under the configured session directory. + +The command MUST advance queued workflow execution until the workflow finishes, workflow execution fails, or Donna emits an action request for the agent. + +The command MUST emit resulting cells. + +## `donna run` command + +The `run` command MUST start a workflow artifact in the current session. + +```bash +donna run WORKFLOW +``` + +The command MUST load workspace configuration. + +The command MUST operate on the session stored under the configured session directory. + +The command MUST normalize `WORKFLOW` as an artifact id. + +The command MUST load the workflow artifact before starting it. + +The command MUST execute the started workflow until the workflow finishes, workflow execution fails, or Donna emits an action request for the agent. + +## `donna complete-action-request` command + +The `complete-action-request` command MUST complete an action request and continue workflow execution. + +```bash +donna complete-action-request ACTION_REQUEST_ID NEXT_OPERATION +``` + +The command MUST load workspace configuration. + +The command MUST operate on the session stored under the configured session directory. + +The command MUST: + +- validate the action request id format. +- normalize `NEXT_OPERATION` as an artifact section id. +- mark the action request as completed. +- queue the selected next operation. +- continue workflow execution immediately. + +After the selected next operation is queued, the command MUST advance workflow execution until the workflow finishes, workflow execution fails, or Donna emits an action request for the agent. + +## `donna skill` command + +The `skill` command MUST print built-in documentation for coding agents. + +```bash +donna skill +donna skill usage +donna skill configuration +donna skill initialization +donna skill workflows +``` + +The command output SHOULD be suitable for coding agents that receive the output as a tool result. + +The command MUST NOT load workspace configuration. + +The `skill` command MUST accept an optional document argument. + +Allowed document argument values MUST be: + +- `usage` — print general command usage documentation. +- `configuration` — print configuration documentation. +- `initialization` — print initialization documentation. +- `workflows` — print workflow authoring and execution documentation. + +When no document argument is provided, `donna skill` MUST behave like `donna skill usage`. + +Unknown document argument values MUST fail as invalid command line arguments. + +## `donna version` command + +The `version` command MUST print the installed Donna package version and exit with status `0`. + +Version output MUST be a single line containing only the version number. + +```bash +donna version +``` + +The `version` command MUST NOT load workspace configuration. + +The `version` command MAY ignore global options that do not affect version output. + +## Help Examples + +### Help + +Command: + +```bash +donna --help +``` + +Help output SHOULD be autogenerated from: + +- command definitions. +- argument definitions. +- option definitions. + +## Errors and exit codes + +Typer command line parsing errors SHOULD use Typer's standard invalid-arguments behavior. + +Workspace, artifact, validation, and environment errors SHOULD be rendered as Donna error cells when possible. + +Environment errors rendered through Donna error cells currently exit with status `0`. + +Human and LLM error cells SHOULD be written to stdout like other Donna cells. + +For automation output, rendered fatal errors SHOULD be written to stdout as JSON Lines cell records when possible. + +If command line parsing fails before Donna installs an output protocol, diagnostics MAY be written by Typer using its standard behavior. + +## Compatibility rules + +The CLI SHOULD preserve backward compatibility for: + +- command names. +- option names. +- output protocol names. +- artifact id syntax. +- artifact section id syntax. +- automation JSONL field meanings. + +Backward-compatible additions MAY include: + +- new commands. +- new options. +- new output cell metadata fields. +- new skill documents. + +Backward-incompatible changes MUST be documented in this specification before implementation. diff --git a/specs/behavior/config.md b/specs/behavior/config.md new file mode 100644 index 00000000..b2b7a881 --- /dev/null +++ b/specs/behavior/config.md @@ -0,0 +1,295 @@ +# Configuration + +## Goal of the document + +This document describes the behavior and semantics of the `donna.toml` configuration file, including: + +- where it is found. +- how it is interpreted. +- how session storage is configured. +- how workflow artifact discovery is configured. +- how Markdown section defaults and journal forwarding are configured. + +## Scope + +The scope of this specification is limited to configuration file behavior. + +The following topics are out of scope: + +- CLI invocation details. +- output protocol formatting. +- workflow operation semantics. +- Markdown artifact source format. +- internal session state schema. +- project module ownership. + +This specification defines configuration semantics that the implementation MUST honor, but it does not require any particular implementation strategy. + +## Dictionary + +- `configuration file` — a TOML file named `donna.toml` that configures one Donna project. +- `project root` — the directory that contains the active configuration file. +- `session directory` — the project directory where Donna stores runtime session state and session-created artifacts. +- `workflow directory` — a project directory recursively scanned for Donna workflow artifacts. +- `section default` — a fallback Markdown section configuration value used when an artifact section omits the value. +- `journal command` — an optional external command invoked for Donna journal records. + +## Configuration file discovery + +The canonical configuration file name MUST be `donna.toml`. + +When a workspace-loading command is invoked without `--config`, `donna` MUST discover the configuration file by searching from the current working directory toward the filesystem root. + +Discovery MUST stop at the first directory that contains `donna.toml`. + +The directory containing the discovered file MUST be the project root. + +When `--config PATH` is provided to a workspace-loading command, `donna` MUST use that file as the configuration file and MUST NOT perform upward discovery. + +If `PATH` is relative, it MUST be resolved relative to the current working directory. + +When `--config PATH` is provided, the directory containing the resolved file MUST be the project root. + +When `--config PATH` is provided to a workspace-loading command, the resolved file MUST exist. + +If no configuration file can be found or the configured path cannot be loaded, workspace loading MUST fail. + +Configuration loading MUST be deterministic for the same: + +- configuration file content. +- current working directory. +- filesystem state. + +## TOML structure + +The configuration file MUST be valid TOML. + +The top-level configuration MAY contain these fields: + +- `version` — configuration schema version. +- `session_dir` — path to Donna runtime session storage. +- `workflow_dirs` — list of directories scanned for workflow artifacts. +- `defaults` — fallback configuration for Markdown artifact sections. +- `journal` — optional external journal forwarding configuration. + +Unknown top-level fields MUST cause configuration loading to fail. + +The initial schema version MUST be `1`. + +If `version` is omitted, `donna` MUST treat the configuration as schema version `1`. + +If `version` is present, it MUST be an integer. + +If `version` is not supported, configuration loading MUST fail. + +Minimal example: + +```toml +version = 1 +``` + +Starter example: + +```toml +version = 1 + +session_dir = ".session/donna" + +workflow_dirs = [ + "./workflows", + "./.session/donna", +] +``` + +## Session directory + +The `session_dir` field MAY be omitted. + +If omitted, `session_dir` MUST default to: + +```toml +session_dir = ".session/donna" +``` + +The `session_dir` field MUST be a project-relative path. + +The `session_dir` field MUST NOT be an absolute host filesystem path. + +The `session_dir` field MUST NOT contain parent-directory references. + +The path MUST be resolved from the Donna project root. + +Donna commands that use session state MUST store runtime state under this directory. + +Session directories MAY be created lazily by runtime commands. + +## Workflow directories + +The `workflow_dirs` field MAY be omitted. + +If omitted, `workflow_dirs` MUST default to: + +```toml +workflow_dirs = [ + "./workflows", + "./.session/donna", +] +``` + +If present, `workflow_dirs` MUST be a list of project-relative paths. + +Each workflow directory MUST NOT be an absolute host filesystem path. + +Each workflow directory MUST NOT contain parent-directory references. + +Duplicate workflow directory entries MUST be removed while preserving the first occurrence. + +Workflow directory paths MUST be resolved from the Donna project root. + +Missing workflow directories MUST be ignored during artifact discovery. + +Donna MUST recursively scan workflow directories for files ending with `.donna.md`. + +Donna MUST ignore files without the `.donna.md` suffix when discovering workflow artifacts. + +Artifact discovery MUST be deterministic for the same `workflow_dirs` order and filesystem state. + +## Defaults + +The `defaults` field MAY be omitted. + +If present, `defaults` MUST be a TOML table. + +The `defaults` table MAY contain these fields: + +- `tail_section_kind` — default primitive path for H2 sections. +- `primary_section_kind` — default primitive path for the H1 section. +- `primary_section_id` — default id for the H1 section. + +Unknown `defaults` fields MUST cause configuration loading to fail. + +If omitted, the effective defaults MUST be: + +```toml +[defaults] +tail_section_kind = "donna.lib.text" +primary_section_kind = "donna.lib.workflow" +primary_section_id = "primary" +``` + +Default primitive paths MUST be valid Python-path-like Donna primitive identifiers. + +`primary_section_id` MUST be a valid Donna section id. + +Explicit section config in a Markdown artifact MUST override configuration defaults. + +Projects SHOULD NOT change section defaults unless they intentionally use custom Donna primitives or a different artifact convention. + +## Journal + +The `journal` field MAY be omitted. + +If present, `journal` MUST be a TOML table. + +The `journal` table MAY contain: + +- `cmd` — optional command argument list used to forward Donna journal records to an external command. + +Unknown `journal` fields MUST cause configuration loading to fail. + +If `journal.cmd` is omitted or `None`, Donna MUST NOT execute an external journal command. + +If present, `journal.cmd` MUST be a non-empty list of command arguments. + +Each argument MUST be a string. + +Donna MUST execute the configured journal command once per journal record. + +Donna MUST execute the command directly as an argument list, not through a shell. + +Donna MUST treat a journal command execution failure as an environment error. + +### Journal placeholders + +Donna MUST recognize placeholders only when the whole command argument starts with `{` and ends with `}`. + +The supported placeholders MUST be: + +- `{timestamp}` — record creation time formatted as ISO-8601. +- `{actor_id}` — actor that created the record, or an empty string. +- `{message}` — journal message. +- `{current_task_id}` — current task id, or an empty string. +- `{current_work_unit_id}` — current work unit id, or an empty string. +- `{current_operation_id}` — current operation artifact section id, or an empty string. + +Unsupported placeholder names MUST cause configuration loading or journal command argument construction to fail. + +Arguments that contain placeholder-like text as only part of the value MUST be treated as literal arguments. + +For example, `{message}` is a placeholder, but `message:{message}` is a literal argument. + +Example: + +```toml +[journal] +cmd = [ + "./bin/journal-tool.sh", + "record", + "{timestamp}", + "{actor_id}", + "{current_task_id}", + "{current_operation_id}", + "{message}", +] +``` + +Donna MUST still print newly created journal records through the selected output protocol when `journal.cmd` is omitted. + +## Starter configuration + +The `donna init` command MUST create a starter configuration based on the packaged base config fixture. + +The starter configuration MUST: + +- use schema version `1`. +- set `session_dir` to `.session/donna`. +- set `workflow_dirs` to `./workflows` and `./.session/donna`. +- include commented examples for `defaults`. +- include commented examples for `journal.cmd`. + +The starter configuration MUST be valid TOML after comments are ignored. + +## Invalid configuration + +Configuration loading MUST fail for: + +- invalid TOML. +- unsupported schema version. +- unknown top-level fields. +- unknown fields in known nested tables. +- invalid `session_dir`. +- invalid `workflow_dirs`. +- invalid default primitive paths. +- invalid default primary section id. +- empty `journal.cmd`. +- unsupported journal placeholders. + +## Compatibility rules + +The configuration schema SHOULD preserve backward compatibility for: + +- the `donna.toml` file name. +- schema version semantics. +- existing top-level field names. +- existing nested field names. +- default `session_dir`. +- default `workflow_dirs`. +- supported journal placeholder meanings. + +Backward-compatible additions MAY include: + +- new optional fields. +- new supported schema versions. +- new journal placeholders. + +Backward-incompatible changes MUST be documented in this specification before implementation. diff --git a/specs/behavior/file_paths.md b/specs/behavior/file_paths.md new file mode 100644 index 00000000..5562efc0 --- /dev/null +++ b/specs/behavior/file_paths.md @@ -0,0 +1,298 @@ +# File paths + +## Goal of the document + +This document describes the syntax, semantics, and resolution rules for local project paths and artifact identifiers used by `donna`. + +## Scope + +The scope of this specification is limited to path identifiers that refer to files and artifact sections inside the active Donna project. + +The following topics are out of scope: + +- workflow operation semantics. +- artifact source parsing details. +- filesystem discovery algorithms, except for canonical path representation. +- output protocol formatting details, except for canonical path representation. + +## Dictionary + +- `project root` — the root directory of the active Donna project. +- `project path` — a file-like path identifier that addresses a non-root location inside the project root and can be represented as a canonical root-anchored id. +- `artifact id` — a canonical project-root-anchored identifier for a Donna artifact file. +- `artifact section id` — an artifact id plus a section id separated by `:`. +- `root-anchored path` — a project path that starts with `@/` and is resolved from the project root. +- `relative path` — a path that does not start with `@/` and is resolved against an explicit base path or the command current working directory. +- `canonical path` — the normalized root-anchored representation of a project path. +- `base path` — a project file or directory used by a context to resolve relative paths. + +## Project root + +The project root MUST be a local filesystem directory. + +For commands that load `donna.toml`, the project root MUST be the directory that contains the active configuration file. + +When `--config PATH` is provided, the directory containing `PATH` MUST be treated as the project root by commands that load workspace configuration. + +A project path MUST identify a non-root location inside the project root. + +A project path MUST NOT identify a location outside the project root after path normalization. + +A project path MUST NOT use an absolute host filesystem path as its canonical representation. + +A canonical project path MUST satisfy Donna artifact id path syntax, including a suffixed final path segment. + +## Root-anchored syntax + +The canonical syntax for a project path MUST be: + +```text +@/path/inside/project.ext +``` + +The `@/` marker MUST represent the project root. + +The `/` character MUST separate path segments. + +The path after `@/` MUST contain at least one path segment. + +The canonical representation MUST NOT contain: + +- empty path segments. +- `.` path segments. +- `..` path segments. +- a trailing `/`. + +Examples of valid canonical paths: + +```text +@/README.md +@/workflows/polish.donna.md +@/.session/donna/plans/implement-feature.donna.md +``` + +Examples of invalid canonical paths: + +```text +@ +@/ +@/workflows/../README.md +@/workflows//polish.donna.md +@/workflows/ +/home/user/project/workflows/polish.donna.md +``` + +## Path semantics + +A project path identifies a local project location by its normalized position under the project root. + +Path identity MUST be based on the canonical path, not on the textual form originally provided by a user, command source, or other input. + +Two path inputs that normalize to the same canonical path MUST identify the same project path. + +The existence of a project path MUST be checked by the context that uses it. + +A field that declares existing-file semantics MUST reject or skip canonical paths that do not correspond to an existing regular file, according to that field's behavior. + +A field that declares reference semantics MAY accept canonical paths that do not exist yet. + +## Path normalization + +Path normalization MUST produce a canonical root-anchored path. + +Normalization MUST: + +- resolve the input against the project root, command current working directory, or an explicit base path. +- remove redundant `.` path segments. +- process `..` path segments. +- reject the path if processing `..` escapes the project root. +- use `/` as the path separator in the canonical representation. +- preserve meaningful path segment case. + +Normalization MUST NOT require the referenced file to exist unless the calling context requires an existing file. + +Implementations MUST reject inputs that cannot be normalized to a project path inside the project root. + +## Artifact ids + +An artifact id MUST be a canonical root-anchored project path. + +Artifact ids accepted by workflow commands MUST identify files with the Donna artifact extension: + +```text +.donna.md +``` + +Artifact id path segments MUST contain only: + +- ASCII letters. +- ASCII digits. +- `.`. +- `_`. +- `-`. + +Each artifact id path segment MUST contain at least one character that is not `.` or `-`. + +The last path segment MUST have a file suffix. + +Artifact ids SHOULD be written as root-anchored paths in workflow instructions, agent notes, and persisted session state. + +Examples: + +```text +@/workflows/polish.donna.md +@/workflows/rfc/do.donna.md +@/.session/donna/plans/feature.donna.md +``` + +## Artifact section ids + +An artifact section id MUST combine an artifact id and a local section id with `:`. + +The canonical syntax MUST be: + +```text +@/path/to/artifact.donna.md:section_id +``` + +The artifact part MUST be a valid artifact id. + +The section id part MUST be a valid Donna section id. + +Section ids MUST contain only: + +- ASCII letters. +- ASCII digits. +- `.`. +- `_`. +- `-`. + +The section id MUST contain at least one character that is not `.` or `-`. + +Artifact section ids are used by `complete-action-request` to identify the next operation selected by an agent. + +## Root-anchored resolution + +A root-anchored path MUST be resolved from the project root. + +For example, in a project rooted at `/project`, the input: + +```text +@/workflows/polish.donna.md +``` + +resolves to: + +```text +/project/workflows/polish.donna.md +``` + +The normalized canonical representation remains: + +```text +@/workflows/polish.donna.md +``` + +Root-anchored inputs MAY contain redundant `.` or `..` segments before normalization, but the canonical representation MUST NOT contain them. + +For example: + +```text +@/workflows/rfc/../polish.donna.md +``` + +normalizes to: + +```text +@/workflows/polish.donna.md +``` + +If a root-anchored input attempts to escape the project root, normalization MUST fail. + +## Relative path resolution + +Relative paths MUST be accepted only by contexts that explicitly define a base path. + +CLI artifact path arguments MUST resolve relative paths from the command's current working directory. + +Artifact-local relative paths MAY be resolved relative to the directory that contains the source artifact. + +When the base path is a file, the relative path MUST be resolved against the directory that contains the base file. + +When the base path is a directory, the relative path MUST be resolved against that directory. + +After resolving a relative path, Donna MUST normalize the result to a canonical root-anchored path. + +For example, with base file: + +```text +@/workflows/rfc/do.donna.md +``` + +the relative input: + +```text +../polish.donna.md +``` + +normalizes to: + +```text +@/workflows/polish.donna.md +``` + +With the same base file, the relative input: + +```text +../../../outside.donna.md +``` + +MUST fail if it escapes the project root. + +New CLI examples and workflow instructions SHOULD prefer root-anchored paths unless relative addressing is central to the example. + +## CLI path inputs + +CLI input parameters that accept artifact paths MUST accept: + +- root-anchored paths. +- classical relative filesystem paths. +- classical absolute filesystem paths. + +CLI logic MUST normalize every accepted path input to a canonical root-anchored path before artifact loading, artifact validation, workflow execution, or action request completion. + +When a CLI input parameter receives a root-anchored path, the path MUST be resolved from the project root and normalized to the canonical root-anchored form. + +When a CLI input parameter receives a classical absolute filesystem path, the path MUST be normalized to a canonical root-anchored path if it points inside the project root. + +If a classical absolute filesystem path points outside the project root, the CLI MUST reject it. + +When a CLI input parameter receives a classical relative filesystem path, the path MUST first be resolved to an absolute filesystem path relative to the command's current working directory. + +The resolved absolute filesystem path MUST then be normalized to a canonical root-anchored path if it points inside the project root. + +If a classical relative filesystem path resolves outside the project root, the CLI MUST reject it. + +New CLI examples and protocol output SHOULD use canonical root-anchored paths unless demonstrating classical relative or absolute filesystem input compatibility. + +## Configuration paths + +Configuration paths such as `session_dir` and `workflow_dirs` MUST be project-relative paths. + +Configuration paths MUST be resolved from the Donna project root. + +Configuration paths MUST NOT be absolute host filesystem paths. + +Configuration paths MUST NOT contain parent-directory references. + +Configuration examples SHOULD use `./` prefixes for directory paths when doing so improves readability. + +## Host filesystem paths + +Absolute host filesystem paths MUST NOT be canonical project path identifiers. + +Implementations MAY accept an absolute host filesystem path as input only when the accepting context explicitly supports host path input. + +CLI artifact path parameters are one such context. + +When accepted, an absolute host filesystem path MUST resolve to a location inside the project root and MUST normalize to a canonical root-anchored path. diff --git a/specs/behavior/skill_fixtures.md b/specs/behavior/skill_fixtures.md new file mode 100644 index 00000000..3db53ca1 --- /dev/null +++ b/specs/behavior/skill_fixtures.md @@ -0,0 +1,172 @@ +# Skill Fixtures + +## Goal of the document + +This document describes the structure and content requirements for built-in skill documentation fixtures. + +Built-in skill fixtures are Markdown files embedded in the package and printed by `donna skill`. + +## Scope + +The scope of this specification is limited to built-in skill documentation fixture files. + +The following topics are out of scope: + +- exact fixture prose. +- CLI option parsing. +- output protocol rendering. +- project configuration semantics beyond examples needed in documentation. +- external Codex or agent skill installation. + +## Fixture location + +Built-in skill documentation fixtures MUST live under: + +```text +./donna/skills/fixtures/ +``` + +Each fixture MUST be a UTF-8 Markdown file. + +Each fixture SHOULD be loaded through `./donna/skills/fixtures.py` instead of direct filesystem reads from CLI or renderer code. + +## Fixture set + +The built-in fixture set MUST include one document per documentation area: + +- `usage.md` — general `donna` command usage. +- `configuration.md` — `donna.toml` configuration syntax and examples. +- `initialization.md` — project initialization workflow. +- `workflows.md` — Donna workflow artifact format, execution, and authoring guidance. + +Each fixture MUST start with a level-one heading that identifies the document: + +```markdown +# `donna` Usage +# `donna` Configuration +# `donna` Initialization +# `donna` Workflows +``` + +The fixture document names exposed by the CLI MUST be: + +- `usage`. +- `configuration`. +- `initialization`. +- `workflows`. + +## Usage fixture + +The usage fixture MUST describe how to invoke `donna` commands and interpret their output. + +It MUST describe available output protocols. + +It MUST describe project-root discovery and the `--config` option. + +It MUST describe root-anchored artifact ids and artifact section ids. + +It MUST describe how to access the other built-in skill documents. + +It MUST include agent safety rules for using Donna in an existing session. + +It MUST describe common workflow execution patterns, including: + +- starting a workflow. +- continuing queued workflow execution. +- completing action requests. +- nested workflows. +- creating or editing workflows when instructed. + +It MUST describe session commands. + +It MUST describe workflow artifact commands. + +It SHOULD keep configuration syntax brief and refer detailed configuration behavior to the configuration fixture. + +It MAY include concise configuration examples when they directly explain command output or agent-facing usage, such as journal forwarding. + +It MUST NOT tell an agent to run workflows without developer, project, or Donna instructions. + +## Configuration fixture + +The configuration fixture MUST describe `donna.toml` structure. + +It SHOULD include examples for: + +- a minimal config. +- the starter config generated by `donna init`. +- `session_dir`. +- `workflow_dirs`. +- `defaults`. +- `journal.cmd`. + +The configuration fixture MUST keep examples compatible with the implemented configuration schema. + +It MUST describe that the presence of `donna.toml` marks the Donna project root. + +It MUST describe supported journal placeholders. + +It SHOULD recommend project-owned workflow directories and session directories consistent with the default configuration. + +It SHOULD tell readers how to validate configuration changes with Donna commands. + +## Initialization fixture + +The initialization fixture MUST describe how to create a starter configuration. + +It MUST mention the `donna init` command. + +It MUST describe that initialization does not overwrite an existing configuration file. + +It MUST describe how `--config` affects the initialization target file and project root. + +It MUST describe that `donna init` creates `donna.toml` and does not create workflow files by itself. + +It MUST describe that session directories may be created lazily by runtime commands. + +It SHOULD include guidance for checking or filling the starter configuration after initialization. + +It SHOULD include guidance for creating first workflow files after initialization when the developer explicitly wants workflows. + +It SHOULD include a validation loop using current Donna commands. + +## Workflows fixture + +The workflows fixture MUST describe Donna workflow artifact format. + +It MUST describe that workflow files are Markdown files ending with `.donna.md`. + +It MUST describe the H1 and H2 section structure used by Donna artifacts. + +It MUST describe artifact ids, section ids, and artifact section ids. + +It MUST describe Donna config code fences and script code fences. + +It MUST describe standard operation kinds currently exposed through `donna.lib`, including: + +- `donna.lib.request_action`. +- `donna.lib.run_script`. +- `donna.lib.output`. +- `donna.lib.finish`. + +It MUST describe `donna.lib.text` as a text section kind rather than a workflow operation kind. + +It MUST describe `donna.lib.goto` for action request transitions. + +It MUST describe `donna.lib.task_variable` for rendering task variables. + +It MUST include at least one complete minimal workflow example. + +It MUST describe validation expectations for workflows. + +## Style + +Fixtures MUST be written as tool documentation. + +Fixtures SHOULD use concise Markdown sections, command examples, TOML examples, workflow examples, and representative output examples. + +Fixtures SHOULD prefer stable command forms and implemented behavior over speculative features. + +Fixtures MUST NOT require readers to inspect source code or specifications to understand documented command usage. + +Fixtures SHOULD use `donna -p llm ...` examples when documenting agent-facing command usage. diff --git a/specs/intro.md b/specs/intro.md index 4d8b18ad..0f6c2e91 100644 --- a/specs/intro.md +++ b/specs/intro.md @@ -13,9 +13,14 @@ Detailed requirements for individual specifications are out of scope except for ## Specification directories - `./specs/` — directory with all specifications. +- `./specs/behavior/` — specifications related to the behavior of the system. - `./specs/meta/` — specifications related to requirements for specification documents. ## Specification documents - `./specs/intro.md` — this file, contains a list of all specifications and their brief descriptions. +- `./specs/behavior/cli.md` — specification of the `donna` command line interface. +- `./specs/behavior/config.md` — specification of the `donna.toml` configuration file behavior. +- `./specs/behavior/file_paths.md` — specification of Donna local project path, artifact id, and artifact section id syntax and resolution behavior. +- `./specs/behavior/skill_fixtures.md` — specification of built-in skill documentation fixture behavior. - `./specs/meta/general.md` — general requirements for specification documents. From 01aea45100acf17953f4d092d353fd24e65e8bd0 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 16:21:47 +0200 Subject: [PATCH 20/98] dictionary --- specs/dictionary.md | 28 ++++++++++++++++++++++++++++ specs/intro.md | 1 + 2 files changed, 29 insertions(+) create mode 100644 specs/dictionary.md diff --git a/specs/dictionary.md b/specs/dictionary.md new file mode 100644 index 00000000..8e878fa8 --- /dev/null +++ b/specs/dictionary.md @@ -0,0 +1,28 @@ +# Dictionary + +## Goal of the document + +This document defines terms that are specific to the `donna` project and are shared by multiple specifications. + +## Scope + +The scope of this specification is limited to project-specific terminology. + +The following topics are out of scope: + +- detailed behavior. +- implementation requirements. +- configuration schemas. + +## Terms + +- `artifact` — a Markdown document interpreted by `donna`; currently expected to be a local file with the `.donna.md` extension. +- `workflow` — a state-machine-like graph of operations that guides an agent's work. +- `operation` — a workflow step that Donna can execute, render, or present as an action request. +- `action request` — a request emitted by Donna when workflow execution needs the agent to perform work or choose the next operation. +- `session` — project-local runtime state that Donna uses to continue workflow execution across CLI invocations. +- `protocol` — a CLI output contract selected by `--protocol`. +- `human protocol` — output protocol optimized for terminal users. +- `llm protocol` — output protocol optimized for coding agents that invoke `donna` as a tool. +- `automation protocol` — output protocol optimized for programs; output is serialized as JSON Lines. +- `warning` — a non-fatal problem discovered while processing a request. diff --git a/specs/intro.md b/specs/intro.md index 0f6c2e91..9fc30cc2 100644 --- a/specs/intro.md +++ b/specs/intro.md @@ -19,6 +19,7 @@ Detailed requirements for individual specifications are out of scope except for ## Specification documents - `./specs/intro.md` — this file, contains a list of all specifications and their brief descriptions. +- `./specs/dictionary.md` — shared project-specific terminology used by multiple specifications. - `./specs/behavior/cli.md` — specification of the `donna` command line interface. - `./specs/behavior/config.md` — specification of the `donna.toml` configuration file behavior. - `./specs/behavior/file_paths.md` — specification of Donna local project path, artifact id, and artifact section id syntax and resolution behavior. From 44113312e9ddbd8b54198c6530055e78790ed439 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 16:24:14 +0200 Subject: [PATCH 21/98] naming.md --- specs/architecture/naming.md | 50 ++++++++++++++++++++++++++++++++++++ specs/intro.md | 2 ++ 2 files changed, 52 insertions(+) create mode 100644 specs/architecture/naming.md diff --git a/specs/architecture/naming.md b/specs/architecture/naming.md new file mode 100644 index 00000000..77e3b4dc --- /dev/null +++ b/specs/architecture/naming.md @@ -0,0 +1,50 @@ +# Naming architecture + +## Goal of the document + +This document describes architectural naming conventions for project code symbols and modules. + +## Scope + +The scope of this specification is limited to stable naming rules that help keep project code understandable across modules. + +The following topics are out of scope: + +- exact names for private helpers. +- complete lists of symbol names. +- generated names in external protocols. +- formatting rules covered by language tools. + +## General principles + +Names SHOULD describe the project concept represented by the symbol. + +Names SHOULD be specific enough to avoid ambiguity at module boundaries. + +Names SHOULD NOT use generic words when a more precise project term is available. + +Names SHOULD avoid confusion with common Python standard library, typing, or framework concepts when a clearer project-specific name is available. + +Names SHOULD be consistent with the responsibility of the module that owns the symbol. + +Public names SHOULD remain stable unless the underlying project concept changes. + +## Type names + +Class, enum, and type alias names SHOULD use singular nouns when each instance or member represents one concept. + +Collection-like plural names SHOULD be used only when the type itself represents a collection or registry. + +Enum type names SHOULD describe what one enum member represents, not the set of all possible members. + +Enum member names SHOULD use the serialized value name when the enum crosses an external boundary and the serialized names are stable. + +Type names SHOULD avoid names that collide with common typing abstractions when the project concept is narrower than the abstraction. + +## Module names + +Module names SHOULD describe the responsibility owned by the module. + +Module names SHOULD be plural only when the module primarily contains a group of closely related definitions with no single narrower responsibility. + +Module names SHOULD NOT mention implementation techniques unless the technique is the module's stable responsibility. diff --git a/specs/intro.md b/specs/intro.md index 9fc30cc2..a2966cd1 100644 --- a/specs/intro.md +++ b/specs/intro.md @@ -13,6 +13,7 @@ Detailed requirements for individual specifications are out of scope except for ## Specification directories - `./specs/` — directory with all specifications. +- `./specs/architecture/` — specifications related to the architecture of the system. - `./specs/behavior/` — specifications related to the behavior of the system. - `./specs/meta/` — specifications related to requirements for specification documents. @@ -20,6 +21,7 @@ Detailed requirements for individual specifications are out of scope except for - `./specs/intro.md` — this file, contains a list of all specifications and their brief descriptions. - `./specs/dictionary.md` — shared project-specific terminology used by multiple specifications. +- `./specs/architecture/naming.md` — specification of project code naming conventions. - `./specs/behavior/cli.md` — specification of the `donna` command line interface. - `./specs/behavior/config.md` — specification of the `donna.toml` configuration file behavior. - `./specs/behavior/file_paths.md` — specification of Donna local project path, artifact id, and artifact section id syntax and resolution behavior. From 921aab669ee28afe972b0ee639651bad2a7424ba Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 16:35:59 +0200 Subject: [PATCH 22/98] modules_layout.md --- specs/architecture/modules_layout.md | 188 +++++++++++++++++++++++++++ specs/intro.md | 1 + 2 files changed, 189 insertions(+) create mode 100644 specs/architecture/modules_layout.md diff --git a/specs/architecture/modules_layout.md b/specs/architecture/modules_layout.md new file mode 100644 index 00000000..c2a1a500 --- /dev/null +++ b/specs/architecture/modules_layout.md @@ -0,0 +1,188 @@ +# Module structure + +## Goal of the document + +This document describes the intended module structure of the project. + +## Scope + +The scope of this specification is limited to the list of project modules and their intended responsibilities. + +The following topics are out of scope: + +- detailed implementation design. +- runtime behavior. +- exact migration steps from the current code layout. +- entity, error, and test conventions beyond module placement. + +## Dictionary + +- `module` — a Python package or module that owns a coherent area of project functionality. +- `submodule` — a Python package or module inside another module that owns a narrower part of its parent module's functionality. +- `test submodule` — a module or file containing tests for a corresponding parent module or submodule. + +## Modules + +- `./donna/` — root module of the project, contains all code related to the `donna` tool. +- `./donna/core/` — module responsible for the core functionality not related to domain logic. Contains: + - shared entity base classes. + - shared error base classes. + - shared result types. + - domain-independent utilities. +- `./donna/domain/` — module responsible only for universal domain entities and logic required by all or most other modules. Contains: + - shared domain-specific types. + - shared domain data structures. + - pure domain logic that is independent of more specific subsystems. + - no subsystem-specific rule evaluation, protocol rendering, workspace loading, or CLI behavior. +- `./donna/machine/` — module responsible for Donna workflow execution logic. Contains: + - session state entities. + - task, work unit, and action request entities. + - state changes. + - operation and primitive interfaces. + - workflow execution orchestration that is independent of CLI argument parsing and output formatting. +- `./donna/context/` — module responsible for invocation-local runtime context. Contains: + - context-local caches. + - context-local execution scopes. + - glue that gives machine and primitive logic access to loaded artifacts, primitive registries, and session state. +- `./donna/primitives/` — module responsible for built-in primitive implementations. Contains: + - artifact primitives. + - section primitives. + - directive primitives. + - primitive-specific validation, rendering, and execution logic. +- `./donna/lib/` — module responsible for stable public names of built-in primitive instances used by Donna artifact configuration. +- `./donna/protocol/` — module responsible for output protocol types and rendering. Contains: + - protocol enums. + - formatter selection. + - protocol-specific formatters. + - serialized record construction for external output protocols. +- `./donna/skills/` — module responsible for built-in skill text loaded by the CLI and renderers. +- `./donna/workspace/` — module responsible for workspace management, including: + - finding and parsing config. + - detecting current project root. + - operations with project files and directories. + - artifact discovery and loading. + - session state storage. + - journal forwarding. +- `./donna/cli/` — module responsible for the CLI interface of the `donna` tool. + +## Submodules + +Modules can have submodules that are responsible for more specific parts of the functionality. + +When a module contains a small closed family of interchangeable components, and each component has meaningful component-specific behavior, the module SHOULD prefer one implementation submodule per component. + +Shared package-level code for such component families SHOULD be limited to common types, public unions, selection helpers, and iteration glue. + +Some submodules have specific names that reflect their responsibilities and SHOULD be similar across different modules. + +List of specific submodules: + +- `utils` — submodule responsible for utility functions that are not related to domain logic. +- `errors` — submodule responsible for defining custom exception types. +- `domain` — submodule responsible for internal business logic related to the module's responsibilities. +- `entities` — submodule responsible for defining types and entities related to the module's responsibilities. +- `operations` — submodule responsible for low-level communication with storage, local files, external tools, or other systems owned by the module. +- `fixtures` — submodule containing reusable configuration, documentation, or data fixtures owned by the module. +- `tests` — submodule containing module tests. +- `tests.make` — test-only submodule containing constructors for test objects related to the parent module. +- `tests.helpers` — test-only submodule containing reusable test setup, mutation, and workflow helpers. + +The `errors`, `entities`, and `tests` submodules MUST follow the corresponding architecture specifications when they are present. + +The shared `entities` submodule in `./donna/core/` MUST define the common entity base used by higher-level modules. + +### Submodule nuances + +#### `errors` + +The `errors` submodule owns module-specific exception classes and error values. + +Errors SHOULD express project-level failure modes that callers can handle, not low-level library details. + +Top-level modules MAY import another top-level module's `errors` submodule when they need to catch or raise errors owned by that module. + +#### `entities` + +The `entities` submodule owns module-specific types, semantic ids, enums, and entities that represent the module's concepts. + +Entities SHOULD describe domain data and boundary data, not storage implementation details unless storage metadata is itself part of the project concept. + +Top-level modules MAY import another top-level module's `entities` submodule for shared boundary types. + +#### `domain` + +The `domain` submodule owns the parent module's internal business logic and is the public behavior boundary for other top-level modules. + +Domain functions SHOULD compose lower-level operation functions and other module boundaries into meaningful workflows, own business decisions, convert or raise module-owned errors, and shape results for callers. + +Domain functions SHOULD hide low-level communication details from callers. + +Top-level input layers such as `./donna/cli/` SHOULD call domain boundaries instead of low-level operation helpers when invoking business behavior. + +When a domain-level function only exposes an operation function without adding behavior, the `domain` submodule SHOULD prefer a direct assignment alias instead of a trivial wrapper. + +Domain wrappers SHOULD be used when they add real behavior, such as orchestration, validation, persistence boundary ownership, fallback logic, caching, error conversion, or result shaping. + +#### `operations` + +The `operations` submodule owns low-level communication with systems outside the module's internal logic. + +Operation functions MAY communicate with local files, subprocesses, external tools, or other infrastructure owned by the module's responsibility. + +Operation functions SHOULD contain communication protocol details, low-level error translation, raw response handling, storage idempotency, and technical maintenance helpers owned by the module. + +Operation functions SHOULD NOT own high-level business workflows. They SHOULD provide small communication primitives that the module's `domain` submodule can compose. + +Operation functions SHOULD NOT be imported by other top-level modules. Cross-module callers SHOULD use the owning module's `domain`, `entities`, or `errors` boundary instead. + +#### `utils` + +The `utils` submodule owns small helpers that are not domain workflows and do not naturally belong to entities, operations, settings, or integration boundaries. + +Utility functions SHOULD be pure or locally technical when practical. If a helper starts to encode module behavior, it SHOULD move to `domain`, `operations`, or a more specific submodule. + +Top-level modules SHOULD avoid depending on another top-level module's `utils` submodule because utilities are not a stable cross-module boundary. + +#### `tests` + +The `tests` submodule owns colocated tests for the parent module and its submodules. + +Tests SHOULD exercise behavior through public boundaries when practical, while operation tests MAY verify storage-specific behavior owned by the module. + +Test files SHOULD mirror implementation module names with the `test_` prefix and organize test classes around the tested function or class. + +#### `make` + +The `make` submodule MAY appear only inside tests packages, as `.tests.make`. + +`tests.make` owns constructors and factory helpers for test objects related to the parent module. + +Tests SHOULD put reusable object construction in `tests.make` instead of duplicating constructors across test files. + +Production modules MUST NOT import `tests.make`. + +#### `helpers` + +The `helpers` submodule MAY appear inside tests packages, as `.tests.helpers`. + +`tests.helpers` owns reusable test helpers that perform setup, mutate persisted test state, call module operations, or wrap common test workflows. + +Object constructors and pure fake data factories SHOULD live in `tests.make`; helpers that perform actions or coordinate multiple calls SHOULD live in `tests.helpers`. + +Production modules MUST NOT import `tests.helpers`. + +## Cross-module dependencies + +Top-level modules that need types or values from another top-level module SHOULD use only that module's `domain`, `entities`, and `errors` submodules when those submodules exist. + +Top-level modules MAY import another module's `errors` submodule when they need to catch or raise errors owned by that module. + +Top-level modules MUST NOT import implementation submodules from another top-level module when a public boundary exists. + +Top-level modules MUST NOT import another top-level module's `operations` submodule. + +## Data structures + +Project data structures SHOULD inherit from `donna.core.entities.BaseEntity` unless a third-party interface or standard-library protocol requires another type. + +Project data structures MUST NOT use `dataclass` for domain entities. diff --git a/specs/intro.md b/specs/intro.md index a2966cd1..eac5d8c5 100644 --- a/specs/intro.md +++ b/specs/intro.md @@ -21,6 +21,7 @@ Detailed requirements for individual specifications are out of scope except for - `./specs/intro.md` — this file, contains a list of all specifications and their brief descriptions. - `./specs/dictionary.md` — shared project-specific terminology used by multiple specifications. +- `./specs/architecture/modules_layout.md` — specification of the intended project module structure and ownership boundaries. - `./specs/architecture/naming.md` — specification of project code naming conventions. - `./specs/behavior/cli.md` — specification of the `donna` command line interface. - `./specs/behavior/config.md` — specification of the `donna.toml` configuration file behavior. From c9127511a6061c169df0c79bbd9b773a816fb103 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 16:52:01 +0200 Subject: [PATCH 23/98] errors.md --- specs/architecture/errors.md | 250 +++++++++++++++++++++++++++++++++++ specs/intro.md | 1 + 2 files changed, 251 insertions(+) create mode 100644 specs/architecture/errors.md diff --git a/specs/architecture/errors.md b/specs/architecture/errors.md new file mode 100644 index 00000000..5d034869 --- /dev/null +++ b/specs/architecture/errors.md @@ -0,0 +1,250 @@ +# Error architecture + +## Goal of the document + +This document describes how project modules represent internal errors, environment errors, and non-fatal problems, and how those values move from lower layers to the CLI. + +## Scope + +The scope of this specification is limited to error and warning architecture inside the Python implementation. + +The following topics are out of scope: + +- exact wording of user-facing messages. +- complete lists of future error codes. +- terminal formatting. +- workflow execution behavior. +- test coverage requirements. + +## Dictionary + +- `internal error` — a programming error, impossible state, unsupported internal operation, or violated invariant inside Donna. +- `environment error` — an expected (user, project, artifact, configuration, filesystem, session, external-tool, etc.) problem that an agent or user may be able to fix. +- `warning` — a non-fatal problem discovered while processing a request. +- `error code` — a stable machine-readable identifier for an environment error. +- `module root error` — a module-owned base class for internal errors or environment errors. +- `exception boundary` — a module boundary where low-level exceptions are converted into Donna-specific errors. + +## General principles + +Expected project failures MUST be represented as Donna-specific errors before they cross module boundaries. + +Internal errors MUST be raised as exceptions. + +Environment errors MUST be returned as `Result[..., ErrorsList]` values unless a temporary exception bridge is required by an external callback boundary. + +Lower-level modules MUST NOT print errors, write protocol records, or terminate the process. + +The CLI layer MUST be responsible for converting environment errors into Donna cells and command exit behavior. + +Environment error codes MUST be stable enough for automation and tests. + +Error codes MUST use lowercase ASCII letters, ASCII digits, `_`, and `.`. + +User-facing messages SHOULD be clear enough to diagnose the problem without exposing implementation stack details. + +## Error kinds + +Donna has two primary error kinds: + +- `InternalError` — raised exception type for bugs and internal invariant failures. +- `EnvironmentError` — structured Pydantic entity for recoverable project or environment problems. + +Internal errors are not expected to be handled by agents or users. + +Environment errors are expected to describe what failed and, when practical, how to fix it. + +## Error ownership + +Base error classes MUST be owned by `donna.core.errors`. + +Each top-level module that owns errors SHOULD define its own error hierarchy in its `errors` submodule. + +A top-level module's internal error root SHOULD be named `InternalError` and inherit from `donna.core.errors.InternalError`. + +A top-level module's environment error root SHOULD inherit from `donna.core.errors.EnvironmentError`. + +The environment root MAY have a more specific name when that name communicates the module boundary, such as `WorkspaceError` or `CliError`. + +Each unique error case SHOULD be represented by its own subclass. + +Production errors MUST NOT be defined in test modules. + +Test-only error classes MAY be defined in test modules when they are required to verify error handling behavior. + +`donna.primitives` submodules MAY define primitive-specific errors close to the primitive implementation when the primitive is a self-contained unit. + +`donna.lib` MUST NOT define an `errors` submodule because it is a collection of constructed primitive instances. + +## Internal errors + +`donna.core.errors.InternalError` MUST inherit from `Exception`. + +Internal error subclasses MAY define a parametrized `message` class attribute. + +Internal error instances MUST store constructor keyword arguments for message formatting. + +Internal errors SHOULD be raised with the standard `raise` statement. + +Internal errors SHOULD NOT be converted into Donna cells during normal CLI command handling. + +Internal errors SHOULD be used for: + +- impossible states. +- unsupported internal methods. +- missing process-local context. +- misuse of `Result.unwrap()` and `Result.unwrap_err()`. +- violations of assumptions already guaranteed by prior validation. + +## Environment errors + +`donna.core.errors.EnvironmentError` MUST inherit from Donna's common entity base. + +Environment errors MUST NOT inherit from `Exception`. + +Environment errors MUST define: + +- `code`. +- `message`. +- `cell_kind`. + +Environment errors MAY define: + +- `cell_media_type`. +- `ways_to_fix`. +- structured fields with context needed for rendering, logging, or tests. +- `content_intro()` when the default intro is not specific enough. + +Environment error messages and ways to fix MAY use `{error.}` formatting. + +Leaf environment errors SHOULD define their message and fix guidance in the class body, not at construction sites. + +Construction sites SHOULD pass only the structured fields that vary for that error instance. + +Environment errors MUST be rendered through `EnvironmentError.node()` when converted to cells. + +Rendered environment error cells MUST include the error code as metadata. + +Rendered environment error cell metadata SHOULD include structured context fields when those fields are scalar and deterministic. + +## Results + +Functions that can fail with environment errors SHOULD return `Result[T, ErrorsList]`. + +`ErrorsList` MUST be a list of `EnvironmentError` instances. + +Successful results MUST be returned with `Ok(value)`. + +Environment failures MUST be returned with `Err(errors)`. + +When multiple validation errors can be discovered in one pass, code SHOULD collect them and return one `Err(errors)` value. + +Code that propagates an existing compatible error result SHOULD return the original result or original error list instead of unwrapping and wrapping it again. + +Code SHOULD NOT define duplicate functions that differ only by error handling strategy. + +If a function is changed to return environment errors, callers up the call stack MUST be updated to process, propagate, or render those errors according to their layer. + +## `unwrap_to_error` + +The `unwrap_to_error` decorator SHOULD be the preferred way to compose calls to functions that return `Result` objects when it makes unwrapping and propagation simpler. + +The decorator SHOULD be used on functions that return `Result[T, ErrorsList]` and primarily call other `Result`-returning functions. + +Decorated functions MAY call `.unwrap()` on intermediate `Result` values to keep straight-line code readable. + +This style SHOULD be preferred over repeated manual checks like `if result.is_err(): return Err(result.unwrap_err())`. + +When `.unwrap()` raises `UnwrapError`, `unwrap_to_error` MUST convert the unwrapped error value back into `Err(...)`. + +`unwrap_to_error` MUST NOT be used to hide internal errors or arbitrary exceptions. + +## Temporary environment error bridges + +Some external callback boundaries cannot return `Result` directly. + +At those boundaries, Donna MAY use a technical internal exception such as `EnvironmentErrorsProxy` to carry environment errors through the callback stack. + +The proxy MUST be caught at the nearest Donna-controlled boundary and converted back into `Result[..., ErrorsList]`. + +The proxy MUST NOT cross into CLI rendering as an internal error. + +## Exception boundaries + +Modules that call external systems MUST convert relevant low-level failures into environment errors at the boundary where useful context is still available. + +External systems include: + +- filesystem operations. +- TOML parsing. +- Pydantic model validation for external input. +- Markdown and template parsing. +- Python import loading for configured primitives or directives. +- subprocess execution. +- external journal commands. + +Pydantic validation errors MUST NOT be exposed directly across high-level module boundaries for user-provided data. + +Modules that create Pydantic entities from external input MUST convert `pydantic.ValidationError`, `ValueError`, and similar low-level validation failures into Donna environment errors at the nearest useful boundary. + +Unexpected programming errors MAY propagate during development, but code that handles expected user or environment failures MUST convert them into environment errors. + +When converting an exception, details SHOULD preserve enough information for diagnosis without requiring stack traces in user-facing output. + +## CLI mapping + +Typer command line parsing errors MAY use Typer's standard invalid-argument behavior. + +CLI argument parsing MAY raise `typer.BadParameter`, `click.UsageError`, or `typer.Exit` before command execution has fully entered Donna's result-based flow. + +After the selected protocol is installed, environment errors SHOULD be rendered as Donna error cells. + +Human and LLM environment error cells SHOULD be written to stdout like other Donna cells. + +Automation environment error cells SHOULD be written to stdout as JSON Lines cell records. + +Environment errors rendered through Donna error cells currently exit with status `0`. + +The CLI SHOULD write environment error journal records when workspace configuration is loaded and journal forwarding is available. + +Modules outside the CLI module MUST NOT know about CLI exit codes. + +## Warnings + +Warnings represent non-fatal problems discovered while processing a request. + +Warnings MUST be used only when processing can continue and the command can still produce useful requested output. + +Warnings MUST NOT be used for invalid command line arguments, invalid configuration that prevents workspace loading, invalid artifacts that prevent requested validation, or workflow execution failures. + +Donna currently has no shared warning storage or warning protocol record architecture. + +Until such architecture is specified and implemented, modules MUST NOT invent ad hoc warning channels. + +## Naming error classes + +Error class names SHOULD be short and descriptive. + +Leaf error class names SHOULD avoid `Error`, `Exception`, `Failure`, `Internal`, and `Environment` suffixes when the shorter name remains clear. + +Root classification classes MAY use `InternalError`, `EnvironmentError`, or a module-specific boundary name such as `WorkspaceError`. + +## Asserts + +`assert` statements MAY be used as hints for type checkers and linters when the invariant is guaranteed by earlier control flow. + +`assert` statements MUST NOT be used for user input validation, environment validation, artifact validation, or recoverable workflow failures. + +Recoverable failures MUST use environment errors. + +Impossible runtime states SHOULD use internal errors when they need explicit handling. + +## Other exception types + +Production Donna code SHOULD use `InternalError` for internal exceptions unless a third-party interface or Python protocol requires another exception type. + +Other exception types MAY be used when required by third-party libraries, Pydantic validators, Typer/click command parsing, or Python protocols. + +`NotImplementedError` MAY be used as a temporary placeholder only while implementation is still in progress. + +Before a change is considered complete, temporary `NotImplementedError` usages SHOULD be replaced with Donna-specific errors or implemented behavior. diff --git a/specs/intro.md b/specs/intro.md index eac5d8c5..3d2093c5 100644 --- a/specs/intro.md +++ b/specs/intro.md @@ -21,6 +21,7 @@ Detailed requirements for individual specifications are out of scope except for - `./specs/intro.md` — this file, contains a list of all specifications and their brief descriptions. - `./specs/dictionary.md` — shared project-specific terminology used by multiple specifications. +- `./specs/architecture/errors.md` — specification of project error handling architecture. - `./specs/architecture/modules_layout.md` — specification of the intended project module structure and ownership boundaries. - `./specs/architecture/naming.md` — specification of project code naming conventions. - `./specs/behavior/cli.md` — specification of the `donna` command line interface. From 319c7a90919e1b577eb6c23043e4bb35c7fff686 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 17:04:20 +0200 Subject: [PATCH 24/98] entities.md --- specs/architecture/entities.md | 204 +++++++++++++++++++++++++++ specs/architecture/modules_layout.md | 2 +- specs/intro.md | 1 + 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 specs/architecture/entities.md diff --git a/specs/architecture/entities.md b/specs/architecture/entities.md new file mode 100644 index 00000000..ddce14b3 --- /dev/null +++ b/specs/architecture/entities.md @@ -0,0 +1,204 @@ +# Entity architecture + +## Goal of the document + +This document describes the architecture of project entities and data structures used to pass artifact, workflow, configuration, session, protocol, and command information between modules. + +## Scope + +The scope of this specification is limited to architectural requirements for Python data structures that represent project concepts. + +The following topics are out of scope: + +- exact class names. +- exact constructor signatures. +- serialization formats for CLI protocols. +- workflow execution algorithms. +- Markdown parsing algorithms. +- validation rules already specified by behavior specifications. + +## Dictionary + +- `entity` — a typed Python object that represents one project concept and can be passed across module boundaries. +- `value entity` — an entity whose equality is based on its data rather than object identity. +- `boundary entity` — an entity that is passed between modules with different responsibilities. +- `serialized representation` — a plain data representation prepared for an external protocol such as JSON Lines, Markdown output, logs, or persistent storage. + +## General principles + +Project concepts MUST be represented as explicit typed entities before they cross module boundaries. + +Boundary entities MUST NOT be represented as untyped dictionaries unless the data is already being prepared as a serialized representation. + +Project entities SHOULD use Pydantic v2 for structured data models. + +Project entities SHOULD keep behavior close to their data only when that behavior is pure and local to the entity. + +Entities MUST NOT perform: + +- filesystem access. +- process execution. +- terminal output. +- configuration file discovery. +- workspace loading. + +Value entities SHOULD be immutable after construction when practical. + +Entities that can be used for de-duplication SHOULD be hashable when practical. + +## Pydantic baseline + +The project accepts Pydantic v2 as the default dependency for entity modeling. + +The project SHOULD provide shared base entity infrastructure owned by the core module. + +Project entities SHOULD inherit from the shared base entity unless they have a specific reason to use Pydantic directly. + +Direct Pydantic usage MAY be used for: + +- configuration objects that mirror external input shapes. +- external transfer objects that intentionally mirror boundary-facing data shapes. +- third-party interfaces that require a direct Pydantic model shape. + +Shared entity defaults SHOULD: + +- strip surrounding whitespace from string values. +- validate default values. +- reject unknown fields. +- prefer immutable value objects where practical. +- validate assignment when mutation is explicitly enabled. +- avoid attribute-based construction unless a boundary explicitly needs it. + +Entities SHOULD use Pydantic field metadata and validators for local field constraints, default factories, discriminators, and model invariants. + +Entities SHOULD use Pydantic serialization methods at boundaries that need model dumps or JSON. + +The shared base entity MUST provide a copy-with-changes operation. + +Very small internal helper values MAY use plain Python classes with `__slots__` when Pydantic would add no practical value. + +Project data structures MUST NOT use `dataclasses.dataclass`. + +## Enumeration conventions + +Closed sets of named values MUST be represented as Python enum classes. + +String-valued external protocols, render modes, record kinds, primitive modes, document names, and similar closed type sets SHOULD use `enum.StrEnum`. + +Integer-valued closed sets SHOULD use `enum.IntEnum` when the integer value is part of the external contract or persisted state. + +Enum classes MUST use `enum.StrEnum` or `enum.IntEnum` instead of `str, enum.Enum` or `int, enum.Enum`. + +Plain strings MUST NOT be used as the primary internal representation for values that have a finite configured, persisted, or specified set of allowed names. + +Enum values that cross external boundaries MUST preserve the specified serialized or persisted value exactly. + +Output protocol values are a closed set of named string values and MUST be represented internally with an enum rather than raw strings or lists of strings. + +## Semantic primitive types + +Semantically specific primitive values MUST have semantically specific Python types before they cross module boundaries. + +For example, an artifact id, artifact section id, section id, action request id, work unit id, task id, project path, or primitive path MUST NOT be represented as an unqualified primitive value in entities or public function signatures when the value has a distinct Donna meaning. + +Semantic primitive types SHOULD use `typing.NewType` when runtime behavior is identical to the underlying primitive. + +Semantic primitive types MAY use small custom classes when they need validation, normalization, ordering, immutability, Pydantic integration, or custom string rendering. + +Raw primitive types MAY be used at parsing, rendering, storage, and serialization boundaries where external data is converted into or out of project types. + +Raw primitive types MAY be used inside local helper code when the value has already been validated or when adding a semantic type would not improve module-boundary clarity. + +Semantic primitive types SHOULD be owned by the module that owns the corresponding project concept. + +Shared semantic primitive types SHOULD belong to the domain module. + +Module-specific semantic primitive types SHOULD belong to the owning module. + +## Entity ownership + +Shared entity infrastructure MUST belong to the core module. + +Shared domain primitive types and universal domain entities MUST belong to the domain module. + +Module-specific entities MUST belong to the module that owns the corresponding responsibility. + +A module MAY expose entities from its public package interface when doing so simplifies imports for callers. + +Public re-exports MUST NOT hide ownership. The defining module MUST remain clear from the source tree. + +## Core and domain entities + +The core module MUST contain only shared entity infrastructure. + +Core entity infrastructure MUST NOT contain domain-specific Donna concepts such as artifacts, workflows, sections, sessions, protocols, primitives, or workspace paths. + +The domain layer MUST contain only universal entities and semantic primitive types for concepts shared by all or most other modules. + +Domain entities MUST model universal Donna concepts independently from the concrete interface that created or renders them. + +Domain entities MUST NOT depend on CLI option parsing, output protocol rendering, workspace loading, session storage, or concrete configuration file syntax. + +Domain entities MUST NOT contain subsystem-specific entities when those entities are required only by one narrower module. + +## Configuration entities + +Configuration entities MAY represent parsed TOML data at the configuration loading boundary when their responsibility is to validate the configuration file shape. + +Configuration entities SHOULD use Pydantic validation to reject malformed parsed data before it reaches lower layers. + +Workspace entities MUST model validated configuration concepts independently from raw TOML table shapes before data reaches lower layers. + +Workspace entities MUST expose shared project concepts as domain entities rather than configuration-specific entities. + +Configuration entities MUST preserve enough information to report useful configuration errors. + +Configuration entities MUST NOT execute workflow artifacts, run primitives, write session state, or forward journal records. + +Configuration entities MAY reference domain entities when the referenced concept has already been validated as a domain concept. + +## CLI and command entities + +CLI entities MUST represent parsed user intent before the command is executed. + +CLI entities MUST model command selection and parsed options independently from rendered output. + +CLI entities MUST NOT contain rendered output. + +CLI entities MUST NOT perform command execution. + +CLI entities SHOULD use domain semantic primitive types for parsed ids and paths when the values have already been normalized or validated. + +## Protocol and serialization entities + +Serialized representations MUST be created at protocol boundaries and SHOULD be treated as write-only output data. + +Serialized representations SHOULD be produced from entities through explicit boundary code, not by leaking Pydantic dump shapes into domain behavior. + +Protocol entities MAY contain presentation-oriented metadata when their responsibility is output formatting. + +Domain, machine, workspace, and CLI entities SHOULD NOT depend on concrete serialized protocol record shapes. + +Entity methods that return dictionaries for protocol metadata, logging, or storage MUST return structured values with stable keys and MUST NOT contain terminal formatting. + +## Data structure conventions + +Ordered input from users, workflow artifacts, configuration files, and persisted session state SHOULD be represented with ordered collections. + +Sets MAY be used internally for de-duplication, but externally visible output order MUST be produced explicitly according to the relevant behavior specification. + +Mappings keyed by semantic ids SHOULD use the semantic id type when possible. + +Optional values MUST be represented with `None` instead of sentinel strings. + +Collections that represent stacks, queues, or ordered workflow state MUST preserve order explicitly. + +## Validation boundaries + +Parsing layers SHOULD validate external data before creating entities that are used by lower layers. + +Pydantic model validation MAY validate local invariants that are always true for the entity. + +Validation that requires filesystem access, configuration discovery, workspace loading, artifact discovery, primitive execution, subprocess execution, or command execution MUST live outside pure entity definitions. + +Invalid external input MUST be reported through the error architecture instead of by returning partially valid entities. diff --git a/specs/architecture/modules_layout.md b/specs/architecture/modules_layout.md index c2a1a500..5fc72527 100644 --- a/specs/architecture/modules_layout.md +++ b/specs/architecture/modules_layout.md @@ -12,7 +12,7 @@ The following topics are out of scope: - detailed implementation design. - runtime behavior. -- exact migration steps from the current code layout. +- migration planning. - entity, error, and test conventions beyond module placement. ## Dictionary diff --git a/specs/intro.md b/specs/intro.md index 3d2093c5..21a968ab 100644 --- a/specs/intro.md +++ b/specs/intro.md @@ -21,6 +21,7 @@ Detailed requirements for individual specifications are out of scope except for - `./specs/intro.md` — this file, contains a list of all specifications and their brief descriptions. - `./specs/dictionary.md` — shared project-specific terminology used by multiple specifications. +- `./specs/architecture/entities.md` — specification of project entity and data structure architecture. - `./specs/architecture/errors.md` — specification of project error handling architecture. - `./specs/architecture/modules_layout.md` — specification of the intended project module structure and ownership boundaries. - `./specs/architecture/naming.md` — specification of project code naming conventions. From 585aee8c32b9e0cd92b7252ad7fc781173e69f87 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 17:15:18 +0200 Subject: [PATCH 25/98] tests.md --- specs/architecture/tests.md | 327 ++++++++++++++++++++++++++++++++++++ specs/intro.md | 1 + 2 files changed, 328 insertions(+) create mode 100644 specs/architecture/tests.md diff --git a/specs/architecture/tests.md b/specs/architecture/tests.md new file mode 100644 index 00000000..0f063936 --- /dev/null +++ b/specs/architecture/tests.md @@ -0,0 +1,327 @@ +# Test architecture + +## Goal of the document + +This document describes the architecture of project tests, including where tests live, how they relate to modules, and how they cover entities, errors, behavior, and CLI boundaries. + +## Scope + +The scope of this specification is limited to test organization and architectural testing expectations for Python code. + +The following topics are out of scope: + +- exact test framework configuration. +- exact fixture names. +- continuous integration configuration. +- package publishing checks. +- performance benchmarks. + +## Dictionary + +- `unit test` — a test focused on one module or one small group of closely related functions or entities. +- `integration test` — a test that checks multiple modules through a public boundary such as configuration loading, artifact loading, workflow execution, or CLI command execution. +- `fixture` — test data or setup used by one or more tests. +- `architecture test` — a test that verifies a project-wide convention from an architecture specification. +- `behavior example test` — a test that verifies an example or rule from a behavior specification. + +## General principles + +Tests MUST be written as part of the Python project. + +Development-related test execution MUST happen through the project development container commands. + +The preferred command form for running tests is: + +```bash +./bin/dev.sh poetry run pytest +``` + +The preferred command form for running targeted tests is: + +```bash +./bin/dev.sh poetry run pytest +``` + +Tests SHOULD be deterministic for the same repository state and filesystem state. + +Tests that require specific shared state MUST prepare that state at the start of the relevant test or with an autouse fixture at the test class or test module level. + +Tests SHOULD prefer end-to-end coverage through public boundaries when that is practical for the behavior under test. + +Tests SHOULD use mocks, stubs, and monkeypatching as little as possible. + +Tests SHOULD prefer real project code, temporary files, explicit fixtures, and small fakes over mocked collaborators. + +Tests MUST NOT depend on external network access. + +Tests MUST NOT depend on user-specific files outside test-created temporary directories. + +Tests MUST NOT modify Docker configuration or runtime parameters. + +Tests SHOULD verify observable behavior and locally owned validation instead of implementation declarations such as annotations, imports, or exact helper types. + +Tests SHOULD prefer separate tests for orthogonal execution paths or validation cases instead of one combined test that verifies all cases at once. + +Static typing requirements SHOULD be enforced by static analysis, code review, or dedicated architecture checks, not by ordinary unit tests that inspect runtime annotations. + +Tests MAY inspect annotations only in dedicated architecture tests that validate a broad project-wide convention. + +Per-entity unit tests MUST NOT inspect annotations only to restate the entity declaration. + +Tests MUST NOT use identity assertions except for `None` checks. + +Tests MUST NOT use identity assertions for boolean values. Use `assert condition` instead of `assert condition is True`, and `assert not condition` instead of `assert condition is False`. + +## Mocking + +Tests SHOULD prefer real project code, explicit fixtures, test constructors, temporary files, and small fakes over mocks. + +When a test must replace a Python collaborator, setting, method, or attribute, it SHOULD use the project-standard pytest mocking tool. + +Patches SHOULD be scoped to the test that needs them and SHOULD patch the name as it is looked up by the code under test. + +Use `mocker.patch("", ...)` for imported module-level collaborators and settings when `pytest-mock` is available. + +Use `mocker.patch.object(...)` when replacing an attribute on an object or class already available in the test. + +Use direct `unittest.mock.MagicMock` only for local fake objects or callables that are passed into the code under test. + +Tests SHOULD NOT use pytest `monkeypatch` for ordinary Python attribute replacement when the project-standard mocking fixture is available. + +## Test module layout + +Each implementation module or submodule SHOULD have corresponding tests under a `tests` submodule owned by the same parent module. + +The name of a test file MUST be built from the name of the tested module by adding the `test_` prefix. + +The structure of tests SHOULD mirror the implementation structure when that makes ownership clear. + +Examples: + +- `./donna/core/utils.py` -> `./donna/core/tests/test_utils.py` +- `./donna/domain/ids.py` -> `./donna/domain/tests/test_ids.py` +- `./donna/workspace/config.py` -> `./donna/workspace/tests/test_config.py` + +Cross-module integration tests MAY live under the module that owns the public boundary being exercised. + +CLI integration tests SHOULD live under `./donna/cli/tests/`. + +Test data constructors reused by multiple test modules in the same package SHOULD live in `tests/make`. + +`tests.make` SHOULD contain small factory functions that create valid entities, value objects, workflow artifacts, session state, and other project data for tests. + +`tests.make` MUST NOT contain assertions or behavior-verification helpers. + +Test helper functions reused by multiple test modules in the same package SHOULD live in `tests/helpers`. + +`tests.helpers` SHOULD contain assertion helpers, setup helpers, cleanup helpers, and test workflow utilities. + +`tests.helpers` MUST NOT contain ordinary project data constructors when those constructors fit `tests.make`. + +## Test organization + +Tests SHOULD be organized around the tested function or tested class. + +Each production module-level function MUST have a corresponding test class when the function owns non-trivial behavior. + +Each class SHOULD have a corresponding test class when the class owns non-trivial behavior. + +Test classes MUST use `Test` naming, where `` is the tested function or class name converted to PascalCase. + +Examples: + +- function `load_workspace` -> `class TestLoadWorkspace`. +- class `Artifact` -> `class TestArtifact`. +- class `ActionRequest` -> `class TestActionRequest`. + +Tests for a class MUST group method tests inside the class's test class. + +Tests for a class method MUST use this method name format: + +```text +test___ +``` + +`` MUST be the tested method name in snake case. + +`` MUST describe the execution path or behavior being verified in snake case. + +Examples: + +- `test_parse__invalid_value`. +- `test_validate_artifact__multiple_primary_sections`. +- `test_complete_action_request__invalid_transition`. + +When a test class tests one module-level function, test methods MAY omit the function name and use this format: + +```text +test_ +``` + +Examples: + +- `TestLoadWorkspace.test_success`. +- `TestLoadWorkspace.test_config_not_found`. +- `TestNormalizePath.test_escapes_project_root`. + +Test-only helper functions do not need corresponding test classes. + +Standalone test functions SHOULD be used only for module-level invariants or file-level checks that do not naturally belong to one tested function or class. + +Every meaningful execution path of a tested function or method MUST have a corresponding test method or parametrized test case. + +Execution paths include: + +- successful path. +- default-value path. +- empty-input path. +- invalid-input path. +- handled error path. +- warning-producing path. +- branch-specific path. + +Tests MUST cover corner cases for each tested function or method. + +Corner cases include: + +- boundary values. +- empty collections and empty strings. +- missing optional values. +- duplicate values. +- unsupported values. +- malformed input. +- paths that do not exist. +- values that require normalization. +- repeated calls that may reveal state leaks. + +## Entity tests + +Entity tests SHOULD verify local invariants of entities. + +Entity tests MUST verify behavior or invariants owned by the entity. + +Entity tests SHOULD cover: + +- entity-specific Pydantic field validation. +- entity-specific Pydantic model validation. +- non-trivial defaults or default factories. +- normalization behavior owned by the entity. +- entity-specific methods and properties. +- serialization or deserialization behavior owned by the entity. +- rejection of invalid values that the entity is responsible for rejecting. + +Entity tests MUST NOT be added only to satisfy file-to-module layout symmetry. + +Entity tests MUST NOT verify that constructor arguments are assigned to fields unchanged. + +Entity tests MUST NOT verify simple Pydantic model construction when the entity has no custom validators, constrained fields, non-trivial defaults, normalization, serialization, computed properties, or entity-specific methods. + +Entity tests MUST NOT verify plain `NewType`, enum member existence, or passive data-container fields unless the module owns non-trivial conversion, validation, serialization, persistence compatibility, protocol compatibility, or external-input compatibility behavior for them. + +For entity-only modules that contain only passive entities, the absence of a matching `tests/test_.py` file is acceptable and SHOULD be preferred over meaningless tests. + +Entity tests SHOULD NOT test behavior inherited unchanged from the shared base entity. + +Entity tests SHOULD NOT test trivial Pydantic behavior unless the entity customizes that behavior. + +Entity tests MUST NOT require filesystem access unless the entity itself explicitly owns path normalization that depends on filesystem semantics. + +Entity tests MUST NOT verify CLI rendering. + +Entity tests MAY assert `pydantic.ValidationError` for invalid low-level model construction. + +## Error tests + +Error tests SHOULD verify behavior customized by concrete error classes. + +Error tests SHOULD cover: + +- custom error codes. +- custom message formatting. +- custom structured details. +- custom cell metadata. +- behavior added by an intermediate error class. + +Error tests SHOULD NOT test unchanged inheritance from project or module root error classes. + +Error tests SHOULD NOT test behavior inherited unchanged from the shared base error class. + +Tests for exception boundaries SHOULD verify that expected low-level failures are converted into Donna environment errors. + +Tests for exception boundaries SHOULD verify that `pydantic.ValidationError` from external input is converted into Donna environment errors. + +Tests for `Result`-returning functions SHOULD verify error values through `Result` state rather than by expecting environment errors to be raised. + +Internal error tests MAY assert raised `InternalError` subclasses when the tested behavior is an internal invariant. + +CLI tests SHOULD verify that rendered environment errors use the expected Donna cell shape when command execution has entered Donna's protocol layer. + +## Behavior coverage + +Behavior specifications are the source of expected externally visible behavior. + +Tests SHOULD cover examples and rules in behavior specifications when the corresponding behavior is implemented and runtime-testable. + +Configuration tests SHOULD cover: + +- configuration discovery. +- `--config` behavior. +- supported TOML structure. +- default values. +- path normalization. +- invalid configuration failures. +- journal command configuration. + +Artifact and workflow tests SHOULD cover: + +- artifact id normalization. +- artifact section id normalization. +- Markdown artifact parsing. +- workflow artifact validation. +- primitive resolution. +- workflow execution until finish, failure, or action request. +- action request completion and transition validation. + +CLI tests SHOULD cover: + +- command forms. +- option parsing. +- output protocol selection. +- command output cell shape. +- artifact listing and ordering. +- artifact rendering modes. +- validation selection with explicit artifact ids and `--all`. +- session status and details. +- errors and exit behavior. + +Architecture specifications are the source of expected project-wide conventions. + +Architecture tests SHOULD cover examples and rules in architecture specifications when the corresponding behavior is implemented and runtime-testable. + +## Fixtures and temporary data + +Tests that need files SHOULD create those files in temporary directories. + +Tests SHOULD keep fixture data as small as possible while preserving the behavior being verified. + +Inline fixture data SHOULD be preferred for short configuration files, short workflow artifacts, short Markdown inputs, and short expected outputs. + +Reusable fixture files MAY be added when inline data would obscure the test. + +Fixture paths SHOULD use forward slashes in expected normalized identifiers. + +Tests that change the current working directory MUST restore it before the test ends. + +Tests that modify process-level global state, context variables, protocol mode, installed workspace configuration, or mocks MUST restore them before the test ends. + +## Assertions + +Tests SHOULD assert structured values before rendered text when structured values are available. + +Rendered output tests SHOULD assert exact output only for stable protocol, CLI, or persisted-state contracts. + +Rendered output tests MAY assert selected lines, fields, or records when exact text is intentionally outside the relevant specification. + +Automation protocol tests SHOULD parse JSON Lines output before asserting record contents. + +Tests SHOULD assert both positive and negative outcomes when a branch is expected to exclude another branch. diff --git a/specs/intro.md b/specs/intro.md index 21a968ab..12cb56b6 100644 --- a/specs/intro.md +++ b/specs/intro.md @@ -25,6 +25,7 @@ Detailed requirements for individual specifications are out of scope except for - `./specs/architecture/errors.md` — specification of project error handling architecture. - `./specs/architecture/modules_layout.md` — specification of the intended project module structure and ownership boundaries. - `./specs/architecture/naming.md` — specification of project code naming conventions. +- `./specs/architecture/tests.md` — specification of project test organization and testing expectations. - `./specs/behavior/cli.md` — specification of the `donna` command line interface. - `./specs/behavior/config.md` — specification of the `donna.toml` configuration file behavior. - `./specs/behavior/file_paths.md` — specification of Donna local project path, artifact id, and artifact section id syntax and resolution behavior. From 2f52a5efe14cfebef85bf6eb9a0018683c5e2635 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 17:16:27 +0200 Subject: [PATCH 26/98] removed old specs --- old_specs/core/error_handling.md | 305 ----------------------- old_specs/core/top_level_architecture.md | 33 --- old_specs/intro.md | 58 ----- 3 files changed, 396 deletions(-) delete mode 100644 old_specs/core/error_handling.md delete mode 100644 old_specs/core/top_level_architecture.md delete mode 100644 old_specs/intro.md diff --git a/old_specs/core/error_handling.md b/old_specs/core/error_handling.md deleted file mode 100644 index d79673ff..00000000 --- a/old_specs/core/error_handling.md +++ /dev/null @@ -1,305 +0,0 @@ - -# Error handling - -This document describes how Donna handles errors that may occur during its operation, including error propagation, logging, and recovery strategies. - -## Error kinds - -There are two different kinds of errors in Donna: - -- **Internal errors** — represented by the base class `InternalError(Exception)` — these errors indicate that something went wrong inside Donna itself, such as bugs in the code, unexpected states, or violations of internal invariants. These errors are not expected to be handled by agents or users, and usually indicate a need for fixing Donna's code. -- **Environment errors** — represented by the base class `EnvironmentError` (not an `Exception`) — these errors indicate that something went wrong in the external environment where Donna operates, such as missing artifacts, invalid artifacts formats, network issues, etc. These errors are expected to be handled by agents or users, and may have recovery strategies. - -Environment errors has functionality to describe themselves in a user-friendly way, according to `donna.protocol`. - -## Defining errors - -Base error classes are defined in the `donna.core.errors` module. - -Each top-level submodule of Donna MUST define its own error hierarchy by subclassing from `InternalError` and `EnvironmentError`. By convention, all error definitions MUST be located in the `donna..errors` module. - -The exceptions for this rule are: - -- `donna.primitives` — each submodule of primitives should be treated as self-contained unit, so error definitions SHOULD be located in the same module as primitive's code itself. -- `donna.lib` MUST not contain `errors` module, as it is just a collection of constructed primitives — no new errors should be defined there. - -Here is an example of defining a hierarchy of errors in `donna.xxx.errors`: - -```python -from donna.core import errors as core_errors - -################# -# Internal errors -################# - -class InternalError(core_errors.InternalError): - """Base class for internal errors in donna.xxx submodule.""" - pass - -class SomethingGoneWrong(InternalError): - """Indicates that something went wrong in donna.xxx submodule in a single specific case.""" - pass - -class DomainOrEntityError(InternalError): - """Base class for domain-specific or entity-specific internal errors.""" - pass - -class EntityGoneWrongCaseA(DomainOrEntityError): - """Indicates that something went wrong with an entity in a single case.""" - pass - -class EntityGoneWrongCaseB(DomainOrEntityError): - """Indicates that something else went wrong with an entity in another case.""" - pass - -#################### -# Environment errors -#################### - -class EnvironmentError(core_errors.EnvironmentError): - """Base class for environment errors in donna.xxx submodule.""" - pass - -# The logic of defining subclasses is similar to internal errors. - -``` - -**Each unique error case MUST be represented by its own subclass.** This allows precise error handling and testing. - -## Internal errors behavior - -Subclasses of internal errors can define parametrized error message: - -``` -... - -class MyInternalError(InternalError): - message = "Something gone wrong with entity {entity_id} due to {reason}." -``` - -**Internal errors are raised** using standard `raise` statement: - -``` -def produce_error(entity_id: str, reason: str): - raise MyInternalError(entity_id=entity_id, reason=reason) -``` - -## Environment errors behavior - -Environment errors are subclasses of Pydantic models and allow specifying structured data about the error. - -Domain specific environment errors MUST redefine `cell_kind` and domain-level attributes. For example: - -```python -class ArtifactValidationError(EnvironmentError): - cell_kind: str = "artifact_validation_error" - artifact_id: FullArtifactId - section_id: ArtifactSectionId | None = None - - def content_intro(self) -> str: - if self.section_id: - return f"Error in artifact '{self.artifact_id}', section '{self.section_id}'" - - return f"Error in artifact '{self.artifact_id}'" -``` - -Leaf/case environment errors must provide explicit information about the error and how the user can fix it. For example: - -```python -class FinalOperationHasTransitions(ArtifactValidationError): - code: str = "donna.workflows.final_operation_has_transitions" - message: str = "Final operation `{error.workflow_section_id}` should not have outgoing transitions." - ways_to_fix: list[str] = [ - "Approach A: Remove all outgoing transitions from this operation.", - "Approach B: Change the `fsm_mode` of this operation from `final` to `normal`", - "Approach C: Remove the `fsm_mode` setting from this operation, as `normal` is the default.", - ] - workflow_section_id: ArtifactSectionId -``` - -**Environment errors are returned** from functions as part of `Result` type: - -```python -... -from donna.core.errors import ErrorsList -from donna.core.result import Err, Ok, Result -... - -def validate_artifact(self) -> Result[None, ErrorsList]: - ... - - errors: ErrorsList = [] - - ... - - errors.append( - FinalOperationHasTransitions( - artifact_id=artifact.id, section_id=section_id, workflow_section_id=workflow_section.id - ) - ) - ... - - if errors: - return Err(errors) - - return Ok(None) -``` - -## Naming error classes - -Follow these guidelines when naming error classes: - -- Try to keep names short but descriptive. -- Prefer not to use `Error`, `Exception`, `Failure` suffixes if possible. -- Prefer not to use `Internal` or `Environment` in the class names. - -## Exceptions besides `InternalError` - -- **Use only `InternalError` for all Donna's internal exceptions, until the developer or the specification explicitly requires otherwise.** -- You MAY use other exception types if the third-party library you are working with requires it. For example, Pydantic models require `ValidationError` exceptions. -- You MAY use `NotImplementedError` as temporary code when: - - You need a temporary placeholder for the code that is not implemented yet, but will be in the scope of the current task. - - You need to raise an exception, but you have no established error hierarchy yet. - -You are encouraged to follow the next strategy when implementing new code: - -1. Use `NotImplementedError` as a temporary placeholders whenever you need to raise an exception. -2. When the code is ready, you review all `NotImplementedError` usages implement proper error hierarchy by defining new error classes as needed. -3. Replace all `NotImplementedError` usages with proper error classes. - -## Asserts - -Use `assert` statements as a hint for type checkers and linters to confirm invariants that are guaranteed by the code logic but type checkers cannot infer them automatically. - -Don't use `assert` statements for any other purpose, replace them with proper error handling code. - -## Do and Don'ts - -### No duplicated logic - -**DO NOT** define multiple functions with the same logic but different error handling strategies (e.g., one function raises exceptions, another returns `Result`). Instead define a single function with one error handling strategy that are explicitly specified by the developer or logically deduced from the context. - -### Update all calls up the call stack - -If you modify function to return environment errors, you **MUST** update all functions up the call stack that call this function to handle the returned errors properly: process, propagate or output (according to the context). - -### Long error definitions — short construction - -Define all possible environment error parameters in the body of subclass, not in the place where you construct the error object. It simplifies writing unit tests and shorten the code. - -Good example: - -```python -class FinalOperationHasTransitions(ArtifactValidationError): - code: str = "donna.workflows.final_operation_has_transitions" - message: str = "Final operation `{error.workflow_section_id}` should not have outgoing transitions." - ways_to_fix: list[str] = [ - "Approach A: Remove all outgoing transitions from this operation.", - "Approach B: Change the `fsm_mode` of this operation from `final` to `normal`", - "Approach C: Remove the `fsm_mode` setting from this operation, as `normal` is the default.", - ] - workflow_section_id: ArtifactSectionId - -... - -error = FinalOperationHasTransitions( - artifact_id=artifact.id, section_id=section_id, workflow_section_id=workflow_section.id -) -``` - -Bad Example: - -```python - -class FinalOperationHasTransitions(ArtifactValidationError): - workflow_section_id: ArtifactSectionId - -... - -error = FinalOperationHasTransitions( - artifact_id=artifact.id, - section_id=section_id, - workflow_section_id=workflow_section.id, - message="Final operation `{error.workflow_section_id}` should not have outgoing transitions.", - ways_to_fix=[ - "Approach A: Remove all outgoing transitions from this operation.", - "Approach B: Change the `fsm_mode` of this operation from `final` to `normal`", - "Approach C: Remove the `fsm_mode` setting from this operation, as `normal` is the default.", - ], -) -``` - -### Propagate original errors - -If you need to propagate errors and the function expected to return the same result type, do not unwrap it and wrap again, just return the original result. - -```python - -def some_function_a() -> Result[SomeType, SomeErrorType]: - ... - -def some_function_b() -> Result[SomeType, SomeErrorType]: - result = some_function_a() - - if result.is_err(): - return result # good: propagate the original errors - -def bad_example() -> Result[SomeType, SomeErrorType]: - result = some_function_a() - - if result.is_err(): - return Err(result.unwrap_err()) # Bad: unwrapping and wrapping again is unnecessary -``` - -### Use `unwrap_to_error` to make code shorter - -The `unwrap_to_error` decorator catches exceptions from `.unwrap()` calls and translate them to results with original errors. - -Use it whenever you see a construction like - -```python -result = some_function() -if result.is_err(): - return Err(result.unwrap_err()) -``` - -Good example: - -```python -... -from donna.core.result import Err, Ok, Result, unwrap_to_error -from donna.workspaces.artifacts import RENDER_CONTEXT_VIEW - -@unwrap_to_error -def resolve(target_id: FullArtifactSectionId) -> Result[ArtifactSection, ErrorsList]: - from donna.context.context import context - - artifact = context().artifacts.load(target_id.full_artifact_id, RENDER_CONTEXT_VIEW).unwrap() - - section = artifact.get_section(target_id.local_id).unwrap() - - return Ok(section) -``` - - -Bad example: - -```python -... -from donna.core.result import Err, Ok, Result, unwrap_to_error - -def resolve(target_id: FullArtifactSectionId) -> Result[ArtifactSection, ErrorsList]: - artifact_result = context().artifacts.load(target_id.full_artifact_id, RENDER_CONTEXT_VIEW) - - if artifact_result.is_err(): - return Err(artifact_result.unwrap_err()) - - artifact = artifact_result.unwrap() - - section_result = artifact.get_section(target_id.local_id) - - if section_result.is_err(): - return Err(section_result.unwrap_err()) - - return Ok(section_result.unwrap()) -``` diff --git a/old_specs/core/top_level_architecture.md b/old_specs/core/top_level_architecture.md deleted file mode 100644 index f83e8749..00000000 --- a/old_specs/core/top_level_architecture.md +++ /dev/null @@ -1,33 +0,0 @@ - -# Top-level architecture - -This document describes the top-level architecture of the Donna project, providing an overview of its main components and their interactions. - -## Basic statements - -- Donna is a CLI tool (`donna`) implemented in Python. - -## Code organization - -All the Donna's code is located in the `./donna/` directory. - -The code is separated by layers/subsystems into subpackages: - -- `donna.core` — code that not in the Donna's domain, but required to its functioning: domain-independent utils, basic classes for errors, exceptions and other entities, etc. -- `donna.domain` — code that is required by all Donna'specific logic: ID classes, common types, etc. -- `donna.machine` — code that implements the core Donna's logic — how Donna works regardless of external environments, i.e. pure domain behavior. -- `donna.context` — code that stores and provides execution-scoped runtime context for Donna's domain logic: artifact/state/primitive caches and scoped values like current actor/work unit/operation identifiers. -- `donna.workspaces` — code that integrates Donna with the project root and filesystem: runtime configuration, artifact discovery/loading, session storage, source parsing, and project initialization. -- `donna.protocol` — code that implements protocol via which Donna's core domain logic interacts with external environments: CLI, API, etc. Includes basic classes for information representing (for the external environments) and its formatting. -- `donna.cli` — code that implements the `donna` CLI tool, its commands, arguments parsing, etc. -- `donna.primitives` — code that implements basic building blocks for Donna's behavior: concrete implementations of various classes from the `donna.machine`. -- `donna.lib` — module that contains constructed primitives to be used in donna artifacts by referencing them by python import path. Like `donna.lib.workflow`, `donna.lib.goto`, etc. -- `donna.skills` — code and built-in text documents used by the `donna skill` command. - -## Data structures - -- Do not use `dataclass` for data structures. Use `donna.core.entities.BaseEntity` (subclass of the `pydantic.BaseModel`) for complex data structures and Python classes with `__slots__` for very simple ones (like cache keys). - -## Autotests - -- No autotests in the project for now. diff --git a/old_specs/intro.md b/old_specs/intro.md deleted file mode 100644 index 2030c729..00000000 --- a/old_specs/intro.md +++ /dev/null @@ -1,58 +0,0 @@ - -# Introduction to the Donna development -This document provides an introduction to the Donna project for agents and developers who want to understand how to work with the Donna codebase. - -## Project overview - -`Donna` is a CLI tool that helps manage the work of AI agents like Codex. - -It is designed to invert control flow: instead of agents deciding what to do next, the `donna` tells agents what to do next by following predefined workflows. - -The core idea is that most high-level workflows are more algorithmic than it may seem at first glance. For example, it may be difficult to fix a particular type issue in the codebase, but the overall process of polishing the codebase is quite linear: - -1. Ensure all tests pass. -2. Ensure the code is formatted correctly. -3. Ensure there are no linting errors. -4. Go to the step 1 if you changed something in the process. -5. Finish. - -We may need coding agents on each step of the process, but there is no reason for agents to manage the whole grooming loop by themselves — it takes longer time, spends tokens and may lead to confusion of agents. - -## Dictionary - -- **Action request** — an instruction to the agent (who runs Donna) to perform the specified operations. Action requests are created by operations, like `donna.lib.request_action`. After finishing following the instructions of an action request, the agent MUST report back to Donna specifying the next operation to continue with. The list of next operations is specified in the action request itself. -- **Artifact** — any text or binary document managed by Donna in the project filesystem; text artifacts are typically Markdown templates with metadata and are the primary units of knowledge and instructions. -- **Artifact Section** — a part of a text artifact separated by markdown headers, has its own configuration block and semantics depending on section kind. -- **Configuration block** — a fenced code block with the `donna` keyword (preferably TOML) that configures an artifact or its section. -- **Directive** — a Jinja2 helper like `donna.lib.goto(...)` that adds meta information or special behavior to an artifact. -- **Environment error** — a structured, user-facing error describing problem in the environment Donna operates in (e.g., missing artifact, invalid config). These errors are expected to be handled by agents or users. -- **Head section** — the H1 section of a markdown artifact (before the first H2) that contains the primary description and mandatory config block. -- **Internal error** — an error caused by a bug or unexpected state in Donna itself. These errors are not expected to be handled by agents or users. -- **Protocol** — the output/interaction mode for Donna (e.g., `llm`) that governs CLI behavior and rendering. -- **Session** — the active unit of work tracked by Donna; its state and artifacts live under `/.session/donna`. -- **Specification** — a normal Markdown document that describes behavior, rules, or project guidance. -- **Story** — a semantically consistent scope of work within a session; a conceptual unit not directly represented in the tool. -- **Tail section** — each H2 section of an artifact. -- **Workspace** — the Donna project rooted at the directory containing `donna.toml`; runtime state lives under the configured session directory. -- **Workflow** — a `donna.lib.workflow` artifact that encodes a finite-state machine of operations guiding the agent's work. -- **Workflow operation** — a single step in a workflow, defined by a tail section with an `id`, `kind`, and instructions. - -## Points of interest - -- `./donna/` — a directory containing source code of project — `donna` CLI tool. -- `./specs/` — a directory containing project-specific documentation. -- `./workflows/` — a directory containing project-specific Donna workflows used to manage the work of AI agents on this project. -- `./.session/donna/` — the configured temporary session directory used by Donna for runtime state and session artifacts. - -## Documentation of interest - -Since this is the repository that contains the Donna project itself, you MUST pay additional attention to which project-scoped artifact ids you are viewing. - -- `@/.agents/donna/**` contains local Donna documentation for this repository. -- `@/specs/**` contains project-specific documentation for developing the Donna codebase. You access it when you need to understand how to introduce changes to this repository. -- `@/workflows/**` contains project-specific workflows for developing the Donna codebase. You change them when you change the development processes of the Donna project as a software project. - -Check the next documents: - -- Read the `@/specs/core/top_level_architecture.md` file when you need to introduce any changes in Donna or to research its code. -- Read the `@/specs/core/error_handling.md` file when you need to implement any new feature in Donna that may produce, process or propagate errors. From 32f0eb0435a3237792e07196da0eab103a4362f1 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 17:32:00 +0200 Subject: [PATCH 27/98] tach --- bin/dev-check-semantics.sh | 4 + poetry.lock | 234 ++++++++++++++++++++++++++- pyproject.toml | 1 + specs/architecture/modules_layout.md | 2 +- specs/architecture/tests.md | 2 +- tach.toml | 144 +++++++++++++++++ workflows/polish.donna.md | 46 +++++- 7 files changed, 421 insertions(+), 12 deletions(-) create mode 100644 tach.toml diff --git a/bin/dev-check-semantics.sh b/bin/dev-check-semantics.sh index 2c9f4a14..dac5b523 100755 --- a/bin/dev-check-semantics.sh +++ b/bin/dev-check-semantics.sh @@ -2,6 +2,10 @@ set -e +echo "run tach" + +./bin/dev.sh poetry run tach check + echo "run autoflake" ./bin/dev.sh poetry run autoflake --check --quiet ./donna diff --git a/poetry.lock b/poetry.lock index 31284d48..a234b8c3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -380,6 +380,40 @@ files = [ [package.dependencies] flake8 = "*" +[[package]] +name = "gitdb" +version = "4.0.12" +description = "Git Object Database" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, + {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, +] + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.50" +description = "GitPython is a Python library used to interact with Git repositories" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9"}, + {file = "gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc"}, +] + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[package.extras] +doc = ["sphinx (>=7.4.7,<8)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] +test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy (==1.18.2) ; python_version >= \"3.9\"", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] + [[package]] name = "isort" version = "7.0.0" @@ -753,6 +787,29 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +[[package]] +name = "networkx" +version = "3.6" +description = "Python package for creating and manipulating graphs and networks" +optional = false +python-versions = ">=3.11" +groups = ["dev"] +files = [ + {file = "networkx-3.6-py3-none-any.whl", hash = "sha256:cdb395b105806062473d3be36458d8f1459a4e4b98e236a66c3a48996e07684f"}, + {file = "networkx-3.6.tar.gz", hash = "sha256:285276002ad1f7f7da0f7b42f004bcba70d381e936559166363707fdad3d72ad"}, +] + +[package.extras] +benchmarking = ["asv", "virtualenv"] +default = ["matplotlib (>=3.8)", "numpy (>=1.25)", "pandas (>=2.0)", "scipy (>=1.11.2)"] +developer = ["mypy (>=1.15)", "pre-commit (>=4.1)"] +doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=10)", "pydata-sphinx-theme (>=0.16)", "sphinx (>=8.0)", "sphinx-gallery (>=0.18)", "texext (>=0.6.7)"] +example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "iplotx (>=0.9.0)", "momepy (>=0.7.2)", "osmnx (>=2.0.0)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] +extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] +release = ["build (>=0.10)", "changelist (==0.5)", "twine (>=4.0)", "wheel (>=0.40)"] +test = ["pytest (>=7.2)", "pytest-cov (>=4.0)", "pytest-xdist (>=3.0)"] +test-extras = ["pytest-mpl", "pytest-randomly"] + [[package]] name = "packaging" version = "25.0" @@ -794,6 +851,21 @@ docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx- test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] type = ["mypy (>=1.18.2)"] +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"}, + {file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"}, +] + +[package.dependencies] +wcwidth = "*" + [[package]] name = "pycodestyle" version = "2.14.0" @@ -1004,6 +1076,28 @@ snowballstemmer = ">=2.2.0" [package.extras] toml = ["tomli (>=1.2.3) ; python_version < \"3.11\""] +[[package]] +name = "pydot" +version = "4.0.1" +description = "Python interface to Graphviz's Dot" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pydot-4.0.1-py3-none-any.whl", hash = "sha256:869c0efadd2708c0be1f916eb669f3d664ca684bc57ffb7ecc08e70d5e93fee6"}, + {file = "pydot-4.0.1.tar.gz", hash = "sha256:c2148f681c4a33e08bf0e26a9e5f8e4099a82e0e2a068098f32ce86577364ad5"}, +] + +[package.dependencies] +pyparsing = ">=3.1.0" + +[package.extras] +dev = ["chardet", "parameterized", "pydot[lint]", "pydot[types]"] +lint = ["ruff"] +release = ["zest.releaser[recommended]"] +tests = ["pydot[dev]", "pytest", "pytest-cov", "pytest-xdist[psutil]", "tox"] +types = ["mypy"] + [[package]] name = "pyflakes" version = "3.4.0" @@ -1031,6 +1125,21 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyparsing" +version = "3.3.2" +description = "pyparsing - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, + {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + [[package]] name = "python-dotenv" version = "1.2.1" @@ -1196,6 +1305,18 @@ files = [ {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] +[[package]] +name = "smmap" +version = "5.0.3" +description = "A pure Python implementation of a sliding window memory map manager" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f"}, + {file = "smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c"}, +] + [[package]] name = "snowballstemmer" version = "3.0.1" @@ -1239,13 +1360,108 @@ files = [ {file = "stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945"}, ] +[[package]] +name = "tach" +version = "0.34.1" +description = "A Python tool to maintain a modular package architecture." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tach-0.34.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6dff869e7f8933be23055f407a751e6f9e4c8ecb0bf3a93a962a0815e106c313"}, + {file = "tach-0.34.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:70d55ec38dfcc15f1fff9e420ba17d63c36bb1b27f92abb8e8c910ffb71fd233"}, + {file = "tach-0.34.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:862d240a7fc297283a51c857eb269d8c56163f14b7e398f757b284bed1e3df4f"}, + {file = "tach-0.34.1-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcb8ca825287160c1dd37b1cb164b88675e01ee97304ce14d50d7e8e1d3b853c"}, + {file = "tach-0.34.1-cp37-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b272ee726d8b0ef3887d3a3057f618922ab0f7471e1b1d6096f78f2ae93727e4"}, + {file = "tach-0.34.1-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6042b4f040c5941d28229963451b94a8cf2b08cdbb0ab0e338a04a46887c712f"}, + {file = "tach-0.34.1-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b52a70262f16b701aaa03781656d808f88a3eb6a263762f8cea726ea9ffb980"}, + {file = "tach-0.34.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cba2d957371fde09c64cb1b70349844e33fae3d87f55e6b002929587c5c7826"}, + {file = "tach-0.34.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c600ea6f8748ea13183aaf802fb733c148238e252cc0e66a85ac8d6de9939906"}, + {file = "tach-0.34.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:7be15f69c99b89cdadc2835368fc3cfb3ad45e74c7a704a7d42bcdd2917608b2"}, + {file = "tach-0.34.1-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:d917fc81adae85e9b3f2d29e135ba572a69dc677661727012b3adf95e0216e9e"}, + {file = "tach-0.34.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f94909630d699806e8682662962de0ac76ecc05dfcf7893af2c7e1a5c71de57e"}, + {file = "tach-0.34.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1246b2718cfffecd4e6d66d6e232323fc4b3f2c68b4c990af0063464808f85fa"}, + {file = "tach-0.34.1-cp37-abi3-win32.whl", hash = "sha256:abd9d1e468a9b8bec1e0037c06797b31e9c23c62adf13bcfacf057a21310ad3f"}, + {file = "tach-0.34.1-cp37-abi3-win_amd64.whl", hash = "sha256:0220c002da4bf4656b121c6317313490e8660627e5200251e0bca3e00a27d0c4"}, + {file = "tach-0.34.1-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b54972211022eafe29a03f5fd4c34d2a7b52ed77eb0e9ff897d64f4c8c2dcd2f"}, + {file = "tach-0.34.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c396aef13bd7e5398674886115160f4d6b9cf7baae1fb9431689d9ac4237f97a"}, + {file = "tach-0.34.1.tar.gz", hash = "sha256:58b5a8f9dd4f5c9fc9b1ade875aa0b31d3ac2f2f6802c655c05197a503d5acde"}, +] + +[package.dependencies] +gitpython = ">=3.1,<4.0" +networkx = ">=2.6,<4.0" +prompt-toolkit = ">=3.0,<4.0" +pydot = ">=2,<5" +pyyaml = ">=6.0,<7.0" +rich = ">=13.0" +tomli = ">=1.2.2" +tomli-w = ">=1.0,<2.0" + +[[package]] +name = "tomli" +version = "2.4.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, +] + [[package]] name = "tomli-w" version = "1.2.0" description = "A lil' TOML writer" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"}, {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, @@ -1308,7 +1524,19 @@ files = [ [package.dependencies] typing-extensions = ">=4.12.0" +[[package]] +name = "wcwidth" +version = "0.7.0" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2"}, + {file = "wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0"}, +] + [metadata] lock-version = "2.1" python-versions = ">=3.12,<4.0" -content-hash = "abc2a7618eca73586b62af3d5a9449463fbbb314f03b5c1429aa896abe44b9bb" +content-hash = "d46dfa8fef715bfadde72ce163380fb5c5e500c8d715c057fd65c8648fa5c12c" diff --git a/pyproject.toml b/pyproject.toml index c463a4ef..4819df89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ isort = "7.0.*" black = "25.12.*" autoflake = "2.3.*" codespell = "2.4.*" +tach = "0.34.*" flake8 = "7.3.*" flake8-docstrings = "1.7.*" diff --git a/specs/architecture/modules_layout.md b/specs/architecture/modules_layout.md index 5fc72527..d98880b1 100644 --- a/specs/architecture/modules_layout.md +++ b/specs/architecture/modules_layout.md @@ -56,7 +56,7 @@ The following topics are out of scope: - protocol-specific formatters. - serialized record construction for external output protocols. - `./donna/skills/` — module responsible for built-in skill text loaded by the CLI and renderers. -- `./donna/workspace/` — module responsible for workspace management, including: +- `./donna/workspaces/` — module responsible for workspace management, including: - finding and parsing config. - detecting current project root. - operations with project files and directories. diff --git a/specs/architecture/tests.md b/specs/architecture/tests.md index 0f063936..3e16c72f 100644 --- a/specs/architecture/tests.md +++ b/specs/architecture/tests.md @@ -100,7 +100,7 @@ Examples: - `./donna/core/utils.py` -> `./donna/core/tests/test_utils.py` - `./donna/domain/ids.py` -> `./donna/domain/tests/test_ids.py` -- `./donna/workspace/config.py` -> `./donna/workspace/tests/test_config.py` +- `./donna/workspaces/config.py` -> `./donna/workspaces/tests/test_config.py` Cross-module integration tests MAY live under the module that owns the public boundary being exercised. diff --git a/tach.toml b/tach.toml new file mode 100644 index 00000000..260b1d91 --- /dev/null +++ b/tach.toml @@ -0,0 +1,144 @@ +source_roots = ["."] +root_module = "ignore" +ignore_type_checking_imports = true +forbid_circular_dependencies = true +layers_explicit_depends_on = true + +exclude = [ + ".git/", + ".mypy_cache/", + ".pytest_cache/", + ".session/", + ".venv/", + ".venv-donna/", + "build/", + "dist/", + "**/__pycache__/", +] + +layers = [ + "meta-tests", + "cli", + "lib", + "primitives", + "context", + "workspace", + "protocol", + "machine", + "domain", + "support", + "core", +] + +[[modules]] +path = "donna" +depends_on = [] +visibility = [] + +[[modules]] +path = "donna.**.tests" +depends_on = [ + "donna.**", +] +layer = "meta-tests" + +[[modules]] +path = "donna.cli" +depends_on = [ + "donna.context", + "donna.core", + "donna.domain", + "donna.lib", + "donna.machine", + "donna.primitives", + "donna.protocol", + "donna.skills", + "donna.workspaces", +] +layer = "cli" + +[[modules]] +path = "donna.lib" +depends_on = [ + "donna.primitives", +] +layer = "lib" + +[[modules]] +path = "donna.primitives" +depends_on = [ + "donna.context", + "donna.core", + "donna.domain", + "donna.machine", + "donna.protocol", + "donna.workspaces", +] +layer = "primitives" + +[[modules]] +path = "donna.context" +depends_on = [ + "donna.core", + "donna.domain", + "donna.machine", + "donna.workspaces", +] +layer = "context" + +[[modules]] +path = "donna.workspaces" +depends_on = [ + "donna.core", + "donna.domain", + "donna.machine", + "donna.protocol", +] +layer = "workspace" + +[[modules]] +path = "donna.protocol" +depends_on = [ + "donna.core", + "donna.domain", + "donna.machine", + "donna.skills", +] +layer = "protocol" + +[[modules]] +path = "donna.machine" +depends_on = [ + "donna.core", + "donna.domain", +] +layer = "machine" + +[[modules]] +path = "donna.domain" +depends_on = [ + "donna.core", +] +layer = "domain" + +[[modules]] +path = "donna.skills" +depends_on = [] +layer = "support" + +[[modules]] +path = "donna.core" +depends_on = [] +layer = "core" + +[external] +exclude = [ + "pytest", + "pytest_mock", + "tach", + "typer", +] + +[rules] +unused_ignore_directives = "warn" +require_ignore_directive_reasons = "off" diff --git a/workflows/polish.donna.md b/workflows/polish.donna.md index 69929b1f..5ef3b17d 100644 --- a/workflows/polish.donna.md +++ b/workflows/polish.donna.md @@ -2,12 +2,44 @@ Initiate operations to polish and refine the donna codebase: running & fixing tests, formatting code, fixing type annotations, etc. This workflow MUST NOT be used to introduce new logic into the project or refactor it — only to fix existing issues. +## Run Tach + +```toml donna +id = "run_tach_script" +kind = "donna.lib.run_script" +fsm_mode = "start" +save_stdout_to = "tach_output" +goto_on_success = "run_autoflake_script" +goto_on_failure = "fix_tach" +``` + +```bash donna script +#!/usr/bin/env bash + +tach check 2>&1 +``` + +## Fix Tach Issues + +```toml donna +id = "fix_tach" +kind = "donna.lib.request_action" +``` + +``` +{{ donna.lib.task_variable("tach_output") }} +``` + +1. Fix the tach issues based on the output above that you are allowed to fix. +2. Ask the developer to fix any remaining issues manually. +3. Ensure your changes are saved. +4. `{{ donna.lib.goto("run_tach_script") }}` + ## Run Autoflake ```toml donna id = "run_autoflake_script" kind = "donna.lib.run_script" -fsm_mode = "start" save_stdout_to = "autoflake_output" goto_on_success = "run_isort_script" goto_on_failure = "fix_autoflake" @@ -32,7 +64,7 @@ kind = "donna.lib.request_action" 1. Fix the autoflake issues based on the output above. 2. Ensure your changes are saved. -3. `{{ donna.lib.goto("run_autoflake_script") }}` +3. `{{ donna.lib.goto("run_tach_script") }}` ## Run isort @@ -63,7 +95,7 @@ kind = "donna.lib.request_action" 1. Fix the isort issues based on the output above. 2. Ensure your changes are saved. -3. `{{ donna.lib.goto("run_autoflake_script") }}` +3. `{{ donna.lib.goto("run_tach_script") }}` ## Run Black @@ -94,7 +126,7 @@ kind = "donna.lib.request_action" 1. Fix the Black issues based on the output above. 2. Ensure your changes are saved. -3. `{{ donna.lib.goto("run_autoflake_script") }}` +3. `{{ donna.lib.goto("run_tach_script") }}` ## Run Codespell @@ -125,7 +157,7 @@ kind = "donna.lib.request_action" 1. Fix the codespell issues based on the output above. 2. Ensure your changes are saved. -3. `{{ donna.lib.goto("run_autoflake_script") }}` +3. `{{ donna.lib.goto("run_tach_script") }}` ## Run Flake8 @@ -156,7 +188,7 @@ kind = "donna.lib.request_action" 1. Fix the flake8 issues based on the output above. 2. Ensure your changes are saved. -3. `{{ donna.lib.goto("run_autoflake_script") }}` +3. `{{ donna.lib.goto("run_tach_script") }}` Instructions on fixing special cases: @@ -195,7 +227,7 @@ kind = "donna.lib.request_action" 1. Fix the mypy issues based on the output above that you are allowed to fix. 2. Ask the developer to fix any remaining issues manually. 3. Ensure your changes are saved. -4. `{{ donna.lib.goto("run_autoflake_script") }}` +4. `{{ donna.lib.goto("run_tach_script") }}` Issues you are allowed to fix: From 37cf48bcc2433ac33368d71c57240e7039ddd7de Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 17:59:47 +0200 Subject: [PATCH 28/98] depmesh integration --- AGENTS.md | 12 ++ bin/tach-map-query.py | 87 +++++++++++ depmesh.toml | 337 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 436 insertions(+) create mode 100644 bin/tach-map-query.py create mode 100644 depmesh.toml diff --git a/AGENTS.md b/AGENTS.md index 60bc5792..6195e41e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,6 +86,18 @@ You run workflows only when explicitly instructed to do so by a developer or Don Donna is configured to log significant operation steps via `task` tool. +### `depmesh` + +`depmesh` — a tool for discovering dependencies between project artifacts. + +Agents MUST use `depmesh` for dependency types supported by its configuration. + +At the start of each work session, read the `depmesh` usage instructions for details: + +```bash +depmesh skill usage +``` + ### `ast-grep` `ast-grep` — a tool for searching and manipulating Abstract Syntax Trees in code. Use it when you work with particular code patterns, structures, or constructs in the codebase. diff --git a/bin/tach-map-query.py b/bin/tach-map-query.py new file mode 100644 index 00000000..22797757 --- /dev/null +++ b/bin/tach-map-query.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import os +from pathlib import PurePosixPath +import subprocess +import sys +from typing import Any + + +def main() -> int: + parser = argparse.ArgumentParser(description="Print Tach file relations for one Donna artifact.") + parser.add_argument("--direction", choices=("dependencies", "dependents"), required=True) + parser.add_argument("--artifact", required=True) + args = parser.parse_args() + + env = os.environ.copy() + env["COMPOSE_IGNORE_ORPHANS"] = "true" + + completed = subprocess.run( + [ + "./bin/dev.sh", + "poetry", + "run", + "python", + "-m", + "tach", + "map", + "--direction", + args.direction, + "--output", + "-", + ], + capture_output=True, + check=False, + env=env, + text=True, + ) + + if completed.returncode != 0: + sys.stderr.write(completed.stderr) + return completed.returncode + + try: + dependency_map = json.loads(completed.stdout) + except json.JSONDecodeError as error: + print(f"could not parse tach map JSON: {error}", file=sys.stderr) + return 1 + + if not isinstance(dependency_map, dict): + print("tach map JSON must be an object", file=sys.stderr) + return 1 + + artifact = _normalize_artifact(args.artifact) + + for dependency in sorted(_dependencies_for(dependency_map, artifact)): + print(f"./{dependency}") + + return 0 + + +def _normalize_artifact(artifact: str) -> str: + normalized = artifact.strip() + + if normalized.startswith("@/"): + normalized = normalized[2:] + + if normalized.startswith("./"): + normalized = normalized[2:] + + return PurePosixPath(normalized).as_posix() + + +def _dependencies_for(dependency_map: dict[str, Any], artifact: str) -> list[str]: + values = dependency_map.get(artifact, dependency_map.get(f"./{artifact}", [])) + + if not isinstance(values, list): + print(f"tach map value for {artifact!r} must be a list", file=sys.stderr) + return [] + + return [_normalize_artifact(value) for value in values if isinstance(value, str)] + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/depmesh.toml b/depmesh.toml new file mode 100644 index 00000000..9132d793 --- /dev/null +++ b/depmesh.toml @@ -0,0 +1,337 @@ +version = 1 + +############################# +# Relation: tested_by / tests +############################# + +[[relations]] +id = "tested_by" +description = "Tests that verify the artifact." + +[[relations]] +id = "tests" +description = "Artifacts verified by the test." + +# Package initializers are verified by matching tests/test_init.py files. +[[rules]] +relation = "tested_by" +input = { type = "all", items = [ + { type = "glob", pattern = "@/donna/{**package_path}/__init__.py" }, + { type = "not", item = { type = "glob", pattern = "@/donna/**/tests/**/*.py" } }, +] } +output = { type = "files", pattern = "@/donna/{package_path}/tests/test_init.py" } + +# Regular Python modules are verified by sibling tests/test_.py files. +[[rules]] +relation = "tested_by" +input = { type = "all", items = [ + { type = "glob", pattern = "@/donna/{**package_path}/{*module}.py" }, + { type = "not", item = { type = "glob", pattern = "@/donna/**/__init__.py" } }, + { type = "not", item = { type = "glob", pattern = "@/donna/**/tests/**/*.py" } }, +] } +output = { type = "files", pattern = "@/donna/{package_path}/tests/test_{module}.py" } + +# tests/test_init.py maps back to the package initializer it verifies. +[[rules]] +relation = "tests" +input = { type = "glob", pattern = "@/donna/{**package_path}/tests/test_init.py" } +output = { type = "list", artifacts = ["@/donna/{package_path}/__init__.py"] } + +# tests/test_.py maps back to the module it verifies. +[[rules]] +relation = "tests" +input = { type = "all", items = [ + { type = "glob", pattern = "@/donna/{**package_path}/tests/test_{*module}.py" }, + { type = "not", item = { type = "glob", pattern = "@/donna/**/tests/test_init.py" } }, +] } +output = { type = "list", artifacts = ["@/donna/{package_path}/{module}.py"] } + +################################# +# Relation: imports / imported_by +################################# + +[[relations]] +id = "imports" +description = "Python files imported by the artifact." + +[[relations]] +id = "imported_by" +description = "Python files that import the artifact." + +# Tach provides direct Python imports for project modules. +[[rules]] +relation = "imports" +input = { type = "glob", pattern = "@/donna/{**module_path}.py" } +output = { + type = "command", + command = "python ./bin/tach-map-query.py --direction dependencies --artifact ./donna/{module_path}.py", +} + +# Tach provides reverse Python import lookups for project modules. +[[rules]] +relation = "imported_by" +input = { type = "glob", pattern = "@/donna/{**module_path}.py" } +output = { + type = "command", + command = "python ./bin/tach-map-query.py --direction dependents --artifact ./donna/{module_path}.py", +} + +################################ +# Relation: governed_by / governs +################################ + +[[relations]] +id = "governed_by" +description = "Specifications that apply to the artifact." + +[[relations]] +id = "governs" +description = "Artifacts governed by the specification." + +# Every specification document is governed by the general specification rules. +# Note: general.md governs itself too. +[[rules]] +relation = "governed_by" +input = { type = "any", items = [ + { type = "glob", pattern = "@/specs/*.md" }, + { type = "glob", pattern = "@/specs/**/*.md" }, +] } +output = { type = "list", artifacts = ["@/specs/meta/general.md"] } + +# The general specification rules govern every specification document. +# Note: general.md governs itself too. +[[rules]] +relation = "governs" +input = { type = "one_of", artifacts = ["@/specs/meta/general.md"] } +output = { type = "union", items = [ + { type = "files", pattern = "@/specs/*.md" }, + { type = "files", pattern = "@/specs/**/*.md" }, +] } + +# Python files are governed by every architecture spec. +[[rules]] +relation = "governed_by" +input = { type = "glob", pattern = "@/donna/**/*.py" } +output = { type = "files", pattern = "@/specs/architecture/*.md" } + +# Architecture specs govern every Python file. +[[rules]] +relation = "governs" +input = { type = "glob", pattern = "@/specs/architecture/*.md" } +output = { type = "files", pattern = "@/donna/**/*.py" } + +# CLI, protocol, and skill-facing code is governed by the CLI behavior spec. +[[rules]] +relation = "governed_by" +input = { type = "any", items = [ + { type = "glob", pattern = "@/donna/cli/**/*.py" }, + { type = "glob", pattern = "@/donna/protocol/**/*.py" }, + { type = "glob", pattern = "@/donna/skills/**/*.py" }, + { type = "glob", pattern = "@/donna/skills/**/*.md" }, + { type = "one_of", artifacts = [ + "@/donna/__init__.py", + "@/donna/cli/__main__.py", + ] }, +] } +output = { type = "list", artifacts = ["@/specs/behavior/cli.md"] } + +# The CLI behavior spec governs CLI, protocol, skill, and entrypoint code. +[[rules]] +relation = "governs" +input = { type = "one_of", artifacts = ["@/specs/behavior/cli.md"] } +output = { type = "union", items = [ + { type = "files", pattern = "@/donna/cli/**/*.py" }, + { type = "files", pattern = "@/donna/protocol/**/*.py" }, + { type = "files", pattern = "@/donna/skills/**/*.py" }, + { type = "files", pattern = "@/donna/skills/**/*.md" }, + { type = "list", artifacts = [ + "@/donna/__init__.py", + "@/donna/cli/__main__.py", + ] }, +] } + +# Configuration behavior governs configuration loading, initialization, fixtures, and project config artifacts. +[[rules]] +relation = "governed_by" +input = { type = "any", items = [ + { type = "one_of", artifacts = [ + "@/donna.toml", + "@/donna/workspaces/fixtures/base_config.toml", + ] }, + { type = "glob", pattern = "@/donna/workspaces/config.py" }, + { type = "glob", pattern = "@/donna/workspaces/initialization.py" }, + { type = "glob", pattern = "@/donna/workspaces/journal.py" }, +] } +output = { type = "list", artifacts = ["@/specs/behavior/config.md"] } + +# The configuration spec governs configuration loading, initialization, fixtures, and project config artifacts. +[[rules]] +relation = "governs" +input = { type = "one_of", artifacts = ["@/specs/behavior/config.md"] } +output = { type = "union", items = [ + { type = "list", artifacts = [ + "@/donna.toml", + "@/donna/workspaces/fixtures/base_config.toml", + ] }, + { type = "files", pattern = "@/donna/workspaces/config.py" }, + { type = "files", pattern = "@/donna/workspaces/initialization.py" }, + { type = "files", pattern = "@/donna/workspaces/journal.py" }, +] } + +# File path behavior governs project path and artifact id normalization code. +[[rules]] +relation = "governed_by" +input = { type = "any", items = [ + { type = "glob", pattern = "@/donna/domain/*paths.py" }, + { type = "glob", pattern = "@/donna/domain/artifact_ids.py" }, + { type = "glob", pattern = "@/donna/domain/id_paths.py" }, + { type = "glob", pattern = "@/donna/workspaces/paths.py" }, + { type = "glob", pattern = "@/donna/workspaces/files.py" }, + { type = "glob", pattern = "@/donna/workspaces/artifacts.py" }, + { type = "glob", pattern = "@/donna/cli/**/*.py" }, +] } +output = { type = "list", artifacts = ["@/specs/behavior/file_paths.md"] } + +# The file path behavior spec governs project path and artifact id normalization code. +[[rules]] +relation = "governs" +input = { type = "one_of", artifacts = ["@/specs/behavior/file_paths.md"] } +output = { type = "union", items = [ + { type = "files", pattern = "@/donna/domain/*paths.py" }, + { type = "files", pattern = "@/donna/domain/artifact_ids.py" }, + { type = "files", pattern = "@/donna/domain/id_paths.py" }, + { type = "files", pattern = "@/donna/workspaces/paths.py" }, + { type = "files", pattern = "@/donna/workspaces/files.py" }, + { type = "files", pattern = "@/donna/workspaces/artifacts.py" }, + { type = "files", pattern = "@/donna/cli/**/*.py" }, +] } + +# Built-in skill fixtures are governed by the skill fixture behavior spec. +[[rules]] +relation = "governed_by" +input = { type = "any", items = [ + { type = "glob", pattern = "@/donna/skills/fixtures.py" }, + { type = "glob", pattern = "@/donna/skills/fixtures/*.md" }, +] } +output = { type = "list", artifacts = ["@/specs/behavior/skill_fixtures.md"] } + +# The skill fixture behavior spec governs built-in skill fixtures. +[[rules]] +relation = "governs" +input = { type = "one_of", artifacts = ["@/specs/behavior/skill_fixtures.md"] } +output = { type = "union", items = [ + { type = "files", pattern = "@/donna/skills/fixtures.py" }, + { type = "files", pattern = "@/donna/skills/fixtures/*.md" }, +] } + +# Dependency mesh and Tach configuration are governed by the module layout architecture spec. +[[rules]] +relation = "governed_by" +input = { type = "one_of", artifacts = [ + "@/AGENTS.md", + "@/bin/tach-map-query.py", + "@/depmesh.toml", + "@/tach.toml", +] } +output = { type = "list", artifacts = ["@/specs/architecture/modules_layout.md"] } + +# The module layout spec governs dependency mesh and Tach configuration. +[[rules]] +relation = "governs" +input = { type = "one_of", artifacts = ["@/specs/architecture/modules_layout.md"] } +output = { type = "list", artifacts = [ + "@/AGENTS.md", + "@/bin/tach-map-query.py", + "@/depmesh.toml", + "@/tach.toml", +] } + +############################################ +# Relation: terms_defined_by / defines_terms_for +############################################ + +[[relations]] +id = "terms_defined_by" +description = "Dictionaries that define terms used by the artifact." + +[[relations]] +id = "defines_terms_for" +description = "Artifacts that use terms from the dictionary." + +# Specs, code, workflows, documentation, and dependency metadata use terms defined in the project dictionary. +# Note: dictionary.md can define terms for itself too. +[[rules]] +relation = "terms_defined_by" +input = { type = "any", items = [ + { type = "glob", pattern = "@/specs/*.md" }, + { type = "glob", pattern = "@/specs/**/*.md" }, + { type = "glob", pattern = "@/donna/**/*.py" }, + { type = "glob", pattern = "@/donna/**/*.md" }, + { type = "glob", pattern = "@/workflows/**/*.donna.md" }, + { type = "glob", pattern = "@/changes/*.md" }, + { type = "one_of", artifacts = [ + "@/AGENTS.md", + "@/bin/tach-map-query.py", + "@/CHANGELOG.md", + "@/README.md", + "@/depmesh.toml", + "@/donna.toml", + "@/tach.toml", + ] }, +] } +output = { type = "list", artifacts = ["@/specs/dictionary.md"] } + +# The dictionary defines terms used by specs, code, workflows, documentation, and dependency metadata. +# Note: dictionary.md can define terms for itself too. +[[rules]] +relation = "defines_terms_for" +input = { type = "one_of", artifacts = ["@/specs/dictionary.md"] } +output = { type = "union", items = [ + { type = "files", pattern = "@/specs/*.md" }, + { type = "files", pattern = "@/specs/**/*.md" }, + { type = "files", pattern = "@/donna/**/*.py" }, + { type = "files", pattern = "@/donna/**/*.md" }, + { type = "files", pattern = "@/workflows/**/*.donna.md" }, + { type = "files", pattern = "@/changes/*.md" }, + { type = "list", artifacts = [ + "@/AGENTS.md", + "@/bin/tach-map-query.py", + "@/CHANGELOG.md", + "@/README.md", + "@/depmesh.toml", + "@/donna.toml", + "@/tach.toml", + ] }, +] } + +############################# +# Relation: indexed_by / indexes +############################# + +[[relations]] +id = "indexed_by" +description = "Indexes that list the specification." + +[[relations]] +id = "indexes" +description = "Specifications listed by the index." + +# Every specification document is listed by the spec index. +# Note: intro.md indexes itself too. +[[rules]] +relation = "indexed_by" +input = { type = "any", items = [ + { type = "glob", pattern = "@/specs/*.md" }, + { type = "glob", pattern = "@/specs/**/*.md" }, +] } +output = { type = "list", artifacts = ["@/specs/intro.md"] } + +# intro.md indexes every specification document. +# Note: intro.md indexes itself too. +[[rules]] +relation = "indexes" +input = { type = "one_of", artifacts = ["@/specs/intro.md"] } +output = { type = "union", items = [ + { type = "files", pattern = "@/specs/*.md" }, + { type = "files", pattern = "@/specs/**/*.md" }, +] } From 100e8068ac528e508961aea7c38a6e046627b291 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 18:37:30 +0200 Subject: [PATCH 29/98] decouple core from protocol Acked-by: Aliaksei Yaletski (Tiendil) --- donna/cli/commands/artifacts.py | 3 +- donna/cli/types.py | 3 +- donna/cli/utils.py | 5 +-- donna/core/errors.py | 63 -------------------------------- donna/machine/artifacts.py | 7 ++-- donna/protocol/errors.py | 64 +++++++++++++++++++++++++++++++++ specs/architecture/errors.md | 6 +++- 7 files changed, 80 insertions(+), 71 deletions(-) diff --git a/donna/cli/commands/artifacts.py b/donna/cli/commands/artifacts.py index 42d45217..996736a7 100644 --- a/donna/cli/commands/artifacts.py +++ b/donna/cli/commands/artifacts.py @@ -10,6 +10,7 @@ from donna.context.context import context from donna.machine import journal as machine_journal from donna.protocol.cell_shortcuts import operation_succeeded +from donna.protocol.errors import environment_error_node from donna.workspaces.artifacts import RENDER_CONTEXT_VIEW, ArtifactRenderContext, fetch_artifact_bytes from donna.workspaces.templates import render as render_template @@ -86,7 +87,7 @@ def validate( # noqa: CCR001 errors.extend(result.unwrap_err()) if errors: - command.write_cells(error.node().info() for error in errors) + command.write_cells(environment_error_node(error).info() for error in errors) return command.write_cells([operation_succeeded("All artifacts are valid")]) diff --git a/donna/cli/types.py b/donna/cli/types.py index 62cbd682..bb88d52c 100644 --- a/donna/cli/types.py +++ b/donna/cli/types.py @@ -15,6 +15,7 @@ from donna.domain.constants import DONNA_ARTIFACT_EXTENSION from donna.domain.internal_ids import ActionRequestId from donna.domain.paths import PathInput, UntrustedPath +from donna.protocol.errors import environment_error_node from donna.protocol.modes import Mode from donna.workspaces import paths as workspace_paths from donna.workspaces.artifacts import has_donna_artifact_extension @@ -22,7 +23,7 @@ def _exit_with_errors(errors: ErrorsList) -> NoReturn: - output_cells([error.node().info() for error in errors]) + output_cells([environment_error_node(error).info() for error in errors]) raise typer.Exit(code=0) diff --git a/donna/cli/utils.py b/donna/cli/utils.py index df7b53dd..027df801 100644 --- a/donna/cli/utils.py +++ b/donna/cli/utils.py @@ -11,6 +11,7 @@ from donna.domain.constants import DONNA_CONFIG_NAME from donna.domain.paths import PathInput, ProjectConfigPath, UntrustedPath from donna.protocol.cells import Cell +from donna.protocol.errors import environment_error_node from donna.protocol.modes import Mode, get_cell_formatter from donna.workspaces import config as workspace_config from donna.workspaces.initialization import load_workspace @@ -91,7 +92,7 @@ def _write_errors_to_journal(errors: ErrorsList) -> None: from donna.machine import journal as machine_journal for error in errors: - message = f"Error: {error.node().journal_message()} [{error.code}]" + message = f"Error: {environment_error_node(error).journal_message()} [{error.code}]" machine_journal.add( message=message, @@ -117,4 +118,4 @@ def _cells_from_unwrap(error: UnwrapError) -> Iterable[Cell]: if workspace_config.config.is_set(): _write_errors_to_journal(errors) - return [item.node().info() for item in errors] + return [environment_error_node(item).info() for item in errors] diff --git a/donna/core/errors.py b/donna/core/errors.py index 0f42938c..9d8bbb99 100644 --- a/donna/core/errors.py +++ b/donna/core/errors.py @@ -3,8 +3,6 @@ import pydantic from donna.core.entities import BaseEntity -from donna.protocol.cells import Cell, MetaValue, to_meta_value -from donna.protocol.nodes import Node class InternalError(Exception): @@ -31,9 +29,6 @@ class EnvironmentError(BaseEntity): def content_intro(self) -> str: return "Error" - def node(self) -> "EnvironmentErrorNode": - return EnvironmentErrorNode(self) - class EnvironmentErrorsProxy(InternalError): message = "This is a technical exception to pass an environment error up the call stack." @@ -58,62 +53,4 @@ class ProjectDirNotFound(CoreEnvironmentError): config_name: str -class EnvironmentErrorNode(Node): - __slots__ = ("_error",) - - def __init__(self, environment_error: EnvironmentError) -> None: - self._error = environment_error - - def meta(self) -> dict[str, MetaValue]: - meta: dict[str, MetaValue] = { - "error_code": self._error.code, - } - - for field_name, _field in self._error.model_fields.items(): - if field_name in ("code", "message", "cell_kind", "cell_media_type", "ways_to_fix"): - continue - - value = getattr(self._error, field_name) - - if value is None: - continue - - meta[field_name] = to_meta_value(value) - - return meta - - def content(self) -> str: - intro = self._error.content_intro() - - message = self._error.message.format(error=self._error).strip() - - ways_to_fix = [fix.format(error=self._error).strip() for fix in self._error.ways_to_fix] - - if "\n" in self._error.message: - content = f"{intro}:\n\n{message}" - else: - content = f"{intro}: {message}" - - if not ways_to_fix: - return content - - if len(ways_to_fix) == 1: - return f"{content}\nWay to fix: {ways_to_fix[0]}" - - fixes = "\n".join(f"- {fix}" for fix in ways_to_fix) - - return f"{content}\n\nWays to fix:\n\n{fixes}" - - def status(self) -> Cell: - return Cell.build( - kind=self._error.cell_kind, - media_type=self._error.cell_media_type, - content=self.content(), - **self.meta(), - ) - - def journal_message(self) -> str: - return self._error.message.format(error=self._error).replace("\n", " ").strip() - - ErrorsList = list[EnvironmentError] diff --git a/donna/machine/artifacts.py b/donna/machine/artifacts.py index 9a081ab3..3d945247 100644 --- a/donna/machine/artifacts.py +++ b/donna/machine/artifacts.py @@ -8,6 +8,7 @@ from donna.domain.python_path import PythonPath from donna.machine.errors import ArtifactPrimarySectionMissing, ArtifactSectionNotFound, MultiplePrimarySectionsError from donna.protocol.cells import Cell +from donna.protocol.errors import environment_error_node from donna.protocol.nodes import Node @@ -137,7 +138,7 @@ def __init__(self, artifact: Artifact) -> None: def status(self) -> Cell: primary_section_result = self._artifact.primary_section() if primary_section_result.is_err(): - return primary_section_result.unwrap_err()[0].node().status() + return environment_error_node(primary_section_result.unwrap_err()[0]).info() primary_section = primary_section_result.unwrap() return Cell.build_markdown( @@ -151,12 +152,12 @@ def status(self) -> Cell: def info(self) -> Cell: primary_section_result = self._artifact.primary_section() if primary_section_result.is_err(): - return primary_section_result.unwrap_err()[0].node().info() + return environment_error_node(primary_section_result.unwrap_err()[0]).info() primary_section = primary_section_result.unwrap() blocks_result = self._artifact.markdown_blocks() if blocks_result.is_err(): - return blocks_result.unwrap_err()[0].node().info() + return environment_error_node(blocks_result.unwrap_err()[0]).info() return Cell.build_markdown( kind="artifact_info", diff --git a/donna/protocol/errors.py b/donna/protocol/errors.py index b584629e..85398e53 100644 --- a/donna/protocol/errors.py +++ b/donna/protocol/errors.py @@ -1,4 +1,6 @@ from donna.core import errors as core_errors +from donna.protocol.cells import Cell, MetaValue, to_meta_value +from donna.protocol.nodes import Node class InternalError(core_errors.InternalError): @@ -15,3 +17,65 @@ class UnsupportedFormatterMode(InternalError): class ContentWithoutMediaType(InternalError): message: str = "Cannot set content when media_type is None." + + +class EnvironmentErrorNode(Node): + __slots__ = ("_error",) + + def __init__(self, environment_error: core_errors.EnvironmentError) -> None: + self._error = environment_error + + def meta(self) -> dict[str, MetaValue]: + meta: dict[str, MetaValue] = { + "error_code": self._error.code, + } + + for field_name, _field in self._error.model_fields.items(): + if field_name in ("code", "message", "cell_kind", "cell_media_type", "ways_to_fix"): + continue + + value = getattr(self._error, field_name) + + if value is None: + continue + + meta[field_name] = to_meta_value(value) + + return meta + + def content(self) -> str: + intro = self._error.content_intro() + + message = self._error.message.format(error=self._error).strip() + + ways_to_fix = [fix.format(error=self._error).strip() for fix in self._error.ways_to_fix] + + if "\n" in self._error.message: + content = f"{intro}:\n\n{message}" + else: + content = f"{intro}: {message}" + + if not ways_to_fix: + return content + + if len(ways_to_fix) == 1: + return f"{content}\nWay to fix: {ways_to_fix[0]}" + + fixes = "\n".join(f"- {fix}" for fix in ways_to_fix) + + return f"{content}\n\nWays to fix:\n\n{fixes}" + + def status(self) -> Cell: + return Cell.build( + kind=self._error.cell_kind, + media_type=self._error.cell_media_type, + content=self.content(), + **self.meta(), + ) + + def journal_message(self) -> str: + return self._error.message.format(error=self._error).replace("\n", " ").strip() + + +def environment_error_node(error: core_errors.EnvironmentError) -> EnvironmentErrorNode: + return EnvironmentErrorNode(error) diff --git a/specs/architecture/errors.md b/specs/architecture/errors.md index 5d034869..c625e5d6 100644 --- a/specs/architecture/errors.md +++ b/specs/architecture/errors.md @@ -121,7 +121,11 @@ Leaf environment errors SHOULD define their message and fix guidance in the clas Construction sites SHOULD pass only the structured fields that vary for that error instance. -Environment errors MUST be rendered through `EnvironmentError.node()` when converted to cells. +Environment errors MUST be rendered through the protocol module when converted to cells. + +The core error base classes MUST NOT depend on protocol cells, protocol nodes, protocol formatters, or CLI output. + +Protocol conversion code MUST preserve the error code as cell metadata. Rendered environment error cells MUST include the error code as metadata. From 6c26dbc7f286effdcf5e7ff9b8a11d64d2a974b3 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 19:43:22 +0200 Subject: [PATCH 30/98] better protocol module positioning --- donna/cli/utils.py | 36 +++++++++++--- donna/context/context.py | 5 +- donna/context/output.py | 22 +++++++++ donna/machine/journal.py | 49 ++------------------ donna/primitives/sections/finish_workflow.py | 4 +- donna/primitives/sections/output.py | 4 +- donna/protocol/formatters/automation.py | 2 +- donna/protocol/formatters/base.py | 2 +- donna/protocol/formatters/human.py | 2 +- donna/protocol/formatters/llm.py | 2 +- donna/protocol/journal.py | 38 +++++++++++++++ donna/protocol/modes.py | 7 ++- donna/protocol/utils.py | 25 ---------- donna/workspaces/journal.py | 2 +- tach.toml | 6 +-- 15 files changed, 114 insertions(+), 92 deletions(-) create mode 100644 donna/context/output.py create mode 100644 donna/protocol/journal.py delete mode 100644 donna/protocol/utils.py diff --git a/donna/cli/utils.py b/donna/cli/utils.py index 027df801..0928e466 100644 --- a/donna/cli/utils.py +++ b/donna/cli/utils.py @@ -12,17 +12,39 @@ from donna.domain.paths import PathInput, ProjectConfigPath, UntrustedPath from donna.protocol.cells import Cell from donna.protocol.errors import environment_error_node +from donna.protocol.formatters.base import Formatter +from donna.protocol.journal import JournalRecord from donna.protocol.modes import Mode, get_cell_formatter from donna.workspaces import config as workspace_config from donna.workspaces.initialization import load_workspace +def instant_output(text: bytes) -> None: + if text.endswith(b"\n"): + sys.stdout.buffer.write(text) + else: + sys.stdout.buffer.write(text + b"\n") + sys.stdout.buffer.flush() + + +class CliEmitter: + __slots__ = ("_formatter",) + + def __init__(self, formatter: Formatter) -> None: + self._formatter = formatter + + def emit_cell(self, cell: Cell) -> None: + instant_output(self._formatter.format_cell(cell)) + + def emit_journal(self, record: JournalRecord) -> None: + instant_output(self._formatter.format_journal(record)) + + def output_cells(cells: Iterable[Cell]) -> None: - formatter = get_cell_formatter() + emitter = CliEmitter(get_cell_formatter(workspace_config.protocol())) for cell in cells: - output = formatter.format_cell(cell) - sys.stdout.buffer.write(output) + emitter.emit_cell(cell) def global_options(context: typer.Context) -> GlobalOptions: @@ -35,11 +57,12 @@ def global_options(context: typer.Context) -> GlobalOptions: class CommandContext: - __slots__ = ("global_options", "protocol") + __slots__ = ("emitter", "global_options", "protocol") def __init__(self, context: typer.Context) -> None: self.global_options = global_options(context) self.protocol = self.global_options.protocol + self.emitter = CliEmitter(get_cell_formatter(self.protocol)) def install_protocol(self) -> None: if not workspace_config.protocol.is_set(): @@ -66,7 +89,8 @@ def target_dir(self) -> PathInput: return UntrustedPath(pathlib.Path.cwd()) def write_cells(self, cells: Iterable[Cell]) -> None: - output_cells(cells) + for cell in cells: + self.emitter.emit_cell(cell) @contextmanager @@ -80,7 +104,7 @@ def command_context(context: typer.Context, *, load_environment: bool = True) -> if load_environment: command.load_workspace() - set_context(Context()) + set_context(Context(output=command.emitter)) yield command except UnwrapError as error: diff --git a/donna/context/context.py b/donna/context/context.py index a10ba527..e93491ca 100644 --- a/donna/context/context.py +++ b/donna/context/context.py @@ -1,6 +1,7 @@ import contextvars from donna.context.artifacts import ArtifactsCache +from donna.context.output import NoopEmitter, OutputEmitter from donna.context.primitives import PrimitivesCache from donna.context.state import StateCache from donna.context.value_scope import ValueScope @@ -13,14 +14,16 @@ class Context: "_artifacts", "_state", "_primitives", + "output", "current_work_unit_id", "current_operation_id", ) - def __init__(self) -> None: + def __init__(self, output: OutputEmitter | None = None) -> None: self._artifacts = ArtifactsCache() self._state = StateCache() self._primitives = PrimitivesCache() + self.output = output if output is not None else NoopEmitter() self.current_work_unit_id: ValueScope[WorkUnitId] = ValueScope() self.current_operation_id: ValueScope[ArtifactSectionId] = ValueScope() diff --git a/donna/context/output.py b/donna/context/output.py new file mode 100644 index 00000000..3b38ca7e --- /dev/null +++ b/donna/context/output.py @@ -0,0 +1,22 @@ +from typing import Protocol + +from donna.protocol.cells import Cell +from donna.protocol.journal import JournalRecord + + +class OutputEmitter(Protocol): + def emit_cell(self, cell: Cell) -> None: + pass + + def emit_journal(self, record: JournalRecord) -> None: + pass + + +class NoopEmitter: + __slots__ = () + + def emit_cell(self, cell: Cell) -> None: + pass + + def emit_journal(self, record: JournalRecord) -> None: + pass diff --git a/donna/machine/journal.py b/donna/machine/journal.py index d2a2ccbd..7e48f74d 100644 --- a/donna/machine/journal.py +++ b/donna/machine/journal.py @@ -1,49 +1,14 @@ -import datetime -import json - -import pydantic - -from donna.core.entities import BaseEntity from donna.core.errors import ErrorsList -from donna.core.result import Err, Ok, Result, unwrap_to_error +from donna.core.result import Ok, Result, unwrap_to_error from donna.core.utils import now from donna.domain.artifact_ids import ArtifactSectionId from donna.domain.internal_ids import TaskId, WorkUnitId from donna.machine import errors as machine_errors +from donna.protocol import journal as protocol_journal from donna.workspaces import journal as workspace_journal from donna.workspaces.config import protocol as protocol_mode -def message_has_newlines(message: str) -> bool: - return "\n" in message or "\r" in message - - -class JournalRecord(BaseEntity): - timestamp: datetime.datetime - actor_id: str | None - message: str - current_task_id: TaskId | None - current_work_unit_id: WorkUnitId | None - current_operation_id: ArtifactSectionId | None - - @pydantic.field_validator("message", mode="after") - @classmethod - def validate_message_no_newlines(cls, value: str) -> str: - if message_has_newlines(value): - raise ValueError("Journal message must not contain newline characters.") - - return value - - -def serialize_record(record: JournalRecord) -> bytes: - return json.dumps( - record.model_dump(mode="json"), - ensure_ascii=False, - separators=(",", ":"), - sort_keys=True, - ).encode("utf-8") - - def smart_agent_id() -> str: from donna.protocol.modes import Mode as ProtocolMode @@ -62,12 +27,8 @@ def smart_agent_id() -> str: def add( # noqa: CCR001 message: str, actor_id: str | None = None, -) -> Result[JournalRecord, ErrorsList]: +) -> Result[protocol_journal.JournalRecord, ErrorsList]: from donna.context.context import context - from donna.protocol.utils import instant_output_journal - - if message_has_newlines(message): - return Err([machine_errors.JournalMessageContainsNewlines()]) if actor_id is None: actor_id = smart_agent_id() @@ -78,7 +39,7 @@ def add( # noqa: CCR001 parsed_work_unit_id: WorkUnitId | None = ctx.current_work_unit_id.get() parsed_operation_id: ArtifactSectionId | None = ctx.current_operation_id.get() - record = JournalRecord( + record = protocol_journal.JournalRecord( timestamp=now(), actor_id=actor_id, message=message, @@ -88,6 +49,6 @@ def add( # noqa: CCR001 ) workspace_journal.write_record(record).unwrap() - instant_output_journal(record) + ctx.output.emit_journal(record) return Ok(record) diff --git a/donna/primitives/sections/finish_workflow.py b/donna/primitives/sections/finish_workflow.py index 45c76d8b..bf73dd11 100644 --- a/donna/primitives/sections/finish_workflow.py +++ b/donna/primitives/sections/finish_workflow.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING, ClassVar, Literal, cast +from donna.context.context import context from donna.core.errors import ErrorsList from donna.core.result import Ok, Result, unwrap_to_error from donna.domain.artifact_ids import ArtifactId @@ -7,7 +8,6 @@ from donna.machine.artifacts import Artifact, ArtifactSectionConfig, ArtifactSectionMeta from donna.machine.operations import FsmMode, OperationConfig, OperationKind, OperationMeta from donna.protocol import cell_shortcuts -from donna.protocol.utils import instant_output_cell from donna.workspaces import markdown from donna.workspaces.markdown_parser import MarkdownSectionMixin @@ -29,7 +29,7 @@ def execute_section( operation = artifact.get_section(section_id).unwrap() info = cell_shortcuts.info(operation.description) - instant_output_cell(info) + context().output.emit_cell(info) return Ok([ChangeFinishTask(task_id=task.id)]) diff --git a/donna/primitives/sections/output.py b/donna/primitives/sections/output.py index b26c0312..7174227d 100644 --- a/donna/primitives/sections/output.py +++ b/donna/primitives/sections/output.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING, ClassVar, cast +from donna.context.context import context from donna.core.errors import ErrorsList from donna.core.result import Err, Ok, Result, unwrap_to_error from donna.domain.artifact_ids import ArtifactId, artifact_section_id, split_artifact_section_id @@ -8,7 +9,6 @@ from donna.machine.errors import ArtifactValidationError from donna.machine.operations import OperationConfig, OperationKind, OperationMeta from donna.protocol import cell_shortcuts -from donna.protocol.utils import instant_output_cell from donna.workspaces import markdown from donna.workspaces.markdown_parser import MarkdownSectionMixin @@ -68,7 +68,7 @@ def execute_section( meta = cast(OutputMeta, operation.meta) info = cell_shortcuts.info(operation.description) - instant_output_cell(info) + context().output.emit_cell(info) next_operation_id = meta.next_operation_id assert next_operation_id is not None diff --git a/donna/protocol/formatters/automation.py b/donna/protocol/formatters/automation.py index f3fb92e7..df3a4b8d 100644 --- a/donna/protocol/formatters/automation.py +++ b/donna/protocol/formatters/automation.py @@ -1,8 +1,8 @@ import json -from donna.machine.journal import JournalRecord, serialize_record from donna.protocol.cells import Cell from donna.protocol.formatters.base import Formatter as BaseFormatter +from donna.protocol.journal import JournalRecord, serialize_record class Formatter(BaseFormatter): diff --git a/donna/protocol/formatters/base.py b/donna/protocol/formatters/base.py index 0ffb1241..6a463b22 100644 --- a/donna/protocol/formatters/base.py +++ b/donna/protocol/formatters/base.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod -from donna.machine.journal import JournalRecord from donna.protocol.cells import Cell +from donna.protocol.journal import JournalRecord class Formatter(ABC): diff --git a/donna/protocol/formatters/human.py b/donna/protocol/formatters/human.py index 3f6d230a..5b58ab8e 100644 --- a/donna/protocol/formatters/human.py +++ b/donna/protocol/formatters/human.py @@ -1,6 +1,6 @@ -from donna.machine.journal import JournalRecord from donna.protocol.cells import Cell from donna.protocol.formatters.base import Formatter as BaseFormatter +from donna.protocol.journal import JournalRecord class Formatter(BaseFormatter): diff --git a/donna/protocol/formatters/llm.py b/donna/protocol/formatters/llm.py index 8a56a653..cd2e02d2 100644 --- a/donna/protocol/formatters/llm.py +++ b/donna/protocol/formatters/llm.py @@ -1,6 +1,6 @@ -from donna.machine.journal import JournalRecord from donna.protocol.cells import Cell from donna.protocol.formatters.base import Formatter as BaseFormatter +from donna.protocol.journal import JournalRecord class Formatter(BaseFormatter): diff --git a/donna/protocol/journal.py b/donna/protocol/journal.py new file mode 100644 index 00000000..6f5b3c20 --- /dev/null +++ b/donna/protocol/journal.py @@ -0,0 +1,38 @@ +import datetime +import json + +import pydantic + +from donna.core.entities import BaseEntity +from donna.domain.artifact_ids import ArtifactSectionId +from donna.domain.internal_ids import TaskId, WorkUnitId + + +def message_has_newlines(message: str) -> bool: + return "\n" in message or "\r" in message + + +class JournalRecord(BaseEntity): + timestamp: datetime.datetime + actor_id: str | None + message: str + current_task_id: TaskId | None + current_work_unit_id: WorkUnitId | None + current_operation_id: ArtifactSectionId | None + + @pydantic.field_validator("message", mode="after") + @classmethod + def validate_message_no_newlines(cls, value: str) -> str: + if message_has_newlines(value): + raise ValueError("Journal message must not contain newline characters.") + + return value + + +def serialize_record(record: JournalRecord) -> bytes: + return json.dumps( + record.model_dump(mode="json"), + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") diff --git a/donna/protocol/modes.py b/donna/protocol/modes.py index 4ac97b0f..47a32478 100644 --- a/donna/protocol/modes.py +++ b/donna/protocol/modes.py @@ -5,7 +5,6 @@ from donna.protocol.formatters.base import Formatter from donna.protocol.formatters.human import Formatter as HumanFormatter from donna.protocol.formatters.llm import Formatter as LLMFormatter -from donna.workspaces.config import protocol as protocol_mode class Mode(enum.StrEnum): @@ -14,8 +13,8 @@ class Mode(enum.StrEnum): automation = "automation" -def get_cell_formatter() -> Formatter: - match protocol_mode(): +def get_cell_formatter(mode: Mode) -> Formatter: + match mode: case Mode.human: return HumanFormatter() case Mode.llm: @@ -23,4 +22,4 @@ def get_cell_formatter() -> Formatter: case Mode.automation: return AutomationFormatter() case _: - raise UnsupportedFormatterMode(mode=protocol_mode()) + raise UnsupportedFormatterMode(mode=mode) diff --git a/donna/protocol/utils.py b/donna/protocol/utils.py deleted file mode 100644 index 6cddb0d1..00000000 --- a/donna/protocol/utils.py +++ /dev/null @@ -1,25 +0,0 @@ -import sys - -from donna.machine.journal import JournalRecord -from donna.protocol.cells import Cell -from donna.protocol.modes import get_cell_formatter - - -def instant_output(text: bytes) -> None: - if text.endswith(b"\n"): - sys.stdout.buffer.write(text) - else: - sys.stdout.buffer.write(text + b"\n") - sys.stdout.buffer.flush() - - -def instant_output_journal(record: JournalRecord) -> None: - formatter = get_cell_formatter() - formatted_output = formatter.format_journal(record) - instant_output(formatted_output) - - -def instant_output_cell(cell: Cell) -> None: - formatter = get_cell_formatter() - formatted_output = formatter.format_cell(cell) - instant_output(formatted_output) diff --git a/donna/workspaces/journal.py b/donna/workspaces/journal.py index 9532988f..90f1a553 100644 --- a/donna/workspaces/journal.py +++ b/donna/workspaces/journal.py @@ -9,7 +9,7 @@ from donna.workspaces.config import JournalRecordAttribute if TYPE_CHECKING: - from donna.machine.journal import JournalRecord + from donna.protocol.journal import JournalRecord def _is_variable_argument(argument: str) -> bool: diff --git a/tach.toml b/tach.toml index 260b1d91..28392f61 100644 --- a/tach.toml +++ b/tach.toml @@ -23,8 +23,8 @@ layers = [ "primitives", "context", "workspace", - "protocol", "machine", + "protocol", "domain", "support", "core", @@ -82,6 +82,7 @@ depends_on = [ "donna.core", "donna.domain", "donna.machine", + "donna.protocol", "donna.workspaces", ] layer = "context" @@ -101,8 +102,6 @@ path = "donna.protocol" depends_on = [ "donna.core", "donna.domain", - "donna.machine", - "donna.skills", ] layer = "protocol" @@ -111,6 +110,7 @@ path = "donna.machine" depends_on = [ "donna.core", "donna.domain", + "donna.protocol", ] layer = "machine" From 781480e6b42c43458f3e7ccc69b8f0574b15592c Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 20:16:42 +0200 Subject: [PATCH 31/98] runtime module --- donna/cli/commands/sessions.py | 2 +- donna/runtime/__init__.py | 1 + donna/{machine => runtime}/sessions.py | 0 specs/architecture/modules_layout.md | 6 ++++++ tach.toml | 14 ++++++++++++++ 5 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 donna/runtime/__init__.py rename donna/{machine => runtime}/sessions.py (100%) diff --git a/donna/cli/commands/sessions.py b/donna/cli/commands/sessions.py index 75d179b8..b5a24dd3 100644 --- a/donna/cli/commands/sessions.py +++ b/donna/cli/commands/sessions.py @@ -9,7 +9,7 @@ parse_artifact_section_id_argument, ) from donna.cli.utils import command_context -from donna.machine import sessions +from donna.runtime import sessions @app.command(name="new-session", help="Create fresh session state.") diff --git a/donna/runtime/__init__.py b/donna/runtime/__init__.py new file mode 100644 index 00000000..e124802f --- /dev/null +++ b/donna/runtime/__init__.py @@ -0,0 +1 @@ +"""Runtime orchestration for Donna workflow execution.""" diff --git a/donna/machine/sessions.py b/donna/runtime/sessions.py similarity index 100% rename from donna/machine/sessions.py rename to donna/runtime/sessions.py diff --git a/specs/architecture/modules_layout.md b/specs/architecture/modules_layout.md index d98880b1..a8f06019 100644 --- a/specs/architecture/modules_layout.md +++ b/specs/architecture/modules_layout.md @@ -40,6 +40,12 @@ The following topics are out of scope: - state changes. - operation and primitive interfaces. - workflow execution orchestration that is independent of CLI argument parsing and output formatting. +- `./donna/runtime/` — module responsible for command-independent runtime orchestration. Contains: + - session lifecycle use cases. + - workflow execution loop coordination. + - invocation-local wiring between context, machine, workspaces, and protocol-facing results. + - journal event forwarding orchestration. + - no CLI argument parsing. - `./donna/context/` — module responsible for invocation-local runtime context. Contains: - context-local caches. - context-local execution scopes. diff --git a/tach.toml b/tach.toml index 28392f61..c38c4ea6 100644 --- a/tach.toml +++ b/tach.toml @@ -19,6 +19,7 @@ exclude = [ layers = [ "meta-tests", "cli", + "runtime", "lib", "primitives", "context", @@ -52,11 +53,24 @@ depends_on = [ "donna.machine", "donna.primitives", "donna.protocol", + "donna.runtime", "donna.skills", "donna.workspaces", ] layer = "cli" +[[modules]] +path = "donna.runtime" +depends_on = [ + "donna.context", + "donna.core", + "donna.domain", + "donna.machine", + "donna.protocol", + "donna.workspaces", +] +layer = "runtime" + [[modules]] path = "donna.lib" depends_on = [ From 1578b3fe510fe1cfc0b698432996d1d3a58ca1c0 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 20:35:59 +0200 Subject: [PATCH 32/98] decouple domain & core --- donna/core/errors.py | 10 ---------- donna/core/utils.py | 30 ------------------------------ donna/workspaces/errors.py | 11 +++++++++++ donna/workspaces/initialization.py | 2 +- donna/workspaces/utils.py | 26 ++++++++++++++++++++++++++ 5 files changed, 38 insertions(+), 41 deletions(-) create mode 100644 donna/workspaces/utils.py diff --git a/donna/core/errors.py b/donna/core/errors.py index 9d8bbb99..3c9aa078 100644 --- a/donna/core/errors.py +++ b/donna/core/errors.py @@ -43,14 +43,4 @@ class CoreEnvironmentError(EnvironmentError): cell_kind: str = "core_environment_error" -class ProjectDirNotFound(CoreEnvironmentError): - code: str = "donna.core.project_dir_not_found" - message: str = "Could not find a project directory containing `{error.config_name}`." - ways_to_fix: list[str] = [ - "Run Donna from within a project directory that contains the Donna config file.", - "Create the Donna project config via CLI command if it does not exist yet.", - ] - config_name: str - - ErrorsList = list[EnvironmentError] diff --git a/donna/core/utils.py b/donna/core/utils.py index 474c6c0f..d59479e0 100644 --- a/donna/core/utils.py +++ b/donna/core/utils.py @@ -1,35 +1,5 @@ import datetime -import pathlib - -from donna.core import errors as core_errors -from donna.core.result import Err, Ok, Result -from donna.domain.paths import ProjectRootPath def now() -> datetime.datetime: return datetime.datetime.now(datetime.UTC) - - -def first_project_dir_with_config(config_name: str) -> ProjectRootPath | None: - """Get the first parent directory containing the Donna config file. - - Search from the current working directory upwards for a folder with Donna config. - """ - current_dir = pathlib.Path.cwd().resolve() - - for parent in [current_dir] + list(current_dir.parents): - config_path = parent / config_name - if config_path.is_file(): - return ProjectRootPath(parent) - - return None - - -def discover_project_dir(config_name: str) -> Result[ProjectRootPath, core_errors.ErrorsList]: - """Discover the project directory by looking for the Donna config file in parent folders.""" - project_dir = first_project_dir_with_config(config_name) - - if project_dir is None: - return Err([core_errors.ProjectDirNotFound(config_name=config_name)]) - - return Ok(project_dir) diff --git a/donna/workspaces/errors.py b/donna/workspaces/errors.py index 0e166312..f3180fcb 100644 --- a/donna/workspaces/errors.py +++ b/donna/workspaces/errors.py @@ -32,6 +32,17 @@ class ConfigValidationFailed(WorkspaceConfigError): details: str +class WorkspaceConfigNotDiscovered(WorkspaceError): + code: str = "donna.workspaces.config_not_discovered" + message: str = "Could not find a project directory containing `{error.config_name}`." + ways_to_fix: list[str] = [ + "Run Donna from within a project directory that contains the Donna config file.", + "Create the Donna project config via CLI command if it does not exist yet.", + "Pass `--config PATH` to use a specific Donna config file.", + ] + config_name: str + + class WorkspaceAlreadyInitialized(WorkspaceError): code: str = "donna.workspaces.workspace_already_initialized" message: str = "Donna project config already exists at `{error.config_path}`" diff --git a/donna/workspaces/initialization.py b/donna/workspaces/initialization.py index 0886a215..a4114cc6 100644 --- a/donna/workspaces/initialization.py +++ b/donna/workspaces/initialization.py @@ -3,13 +3,13 @@ import tomllib from donna.core import errors as core_errors -from donna.core import utils from donna.core.result import Err, Ok, Result, unwrap_to_error from donna.domain.constants import DONNA_CONFIG_NAME from donna.domain.paths import PathInput, ProjectConfigPath, ProjectRootPath, UntrustedPath from donna.protocol.modes import Mode from donna.workspaces import config from donna.workspaces import errors as world_errors +from donna.workspaces import utils from donna.workspaces.paths import resolve_project_root BASE_CONFIG_FIXTURE = "base_config.toml" diff --git a/donna/workspaces/utils.py b/donna/workspaces/utils.py new file mode 100644 index 00000000..4108cb04 --- /dev/null +++ b/donna/workspaces/utils.py @@ -0,0 +1,26 @@ +import pathlib + +from donna.core.errors import ErrorsList +from donna.core.result import Err, Ok, Result +from donna.domain.paths import ProjectRootPath +from donna.workspaces import errors as workspace_errors + + +def first_project_dir_with_config(config_name: str) -> ProjectRootPath | None: + current_dir = pathlib.Path.cwd().resolve() + + for parent in [current_dir] + list(current_dir.parents): + config_path = parent / config_name + if config_path.is_file(): + return ProjectRootPath(parent) + + return None + + +def discover_project_dir(config_name: str) -> Result[ProjectRootPath, ErrorsList]: + project_dir = first_project_dir_with_config(config_name) + + if project_dir is None: + return Err([workspace_errors.WorkspaceConfigNotDiscovered(config_name=config_name)]) + + return Ok(project_dir) From a6e2c4e9db22ba97a1aebe21cd7d2c0f04174518 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 21:02:58 +0200 Subject: [PATCH 33/98] better specs --- specs/architecture/modules_layout.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/specs/architecture/modules_layout.md b/specs/architecture/modules_layout.md index a8f06019..0a09e55b 100644 --- a/specs/architecture/modules_layout.md +++ b/specs/architecture/modules_layout.md @@ -39,7 +39,8 @@ The following topics are out of scope: - task, work unit, and action request entities. - state changes. - operation and primitive interfaces. - - workflow execution orchestration that is independent of CLI argument parsing and output formatting. + - workflow execution orchestration that is independent of CLI argument parsing and protocol-specific formatting. + - protocol-facing views of machine-owned entities expressed through protocol-neutral boundary entities. - `./donna/runtime/` — module responsible for command-independent runtime orchestration. Contains: - session lifecycle use cases. - workflow execution loop coordination. @@ -56,7 +57,8 @@ The following topics are out of scope: - directive primitives. - primitive-specific validation, rendering, and execution logic. - `./donna/lib/` — module responsible for stable public names of built-in primitive instances used by Donna artifact configuration. -- `./donna/protocol/` — module responsible for output protocol types and rendering. Contains: +- `./donna/protocol/` — module responsible for Donna output boundary entities and rendering. Contains: + - protocol-neutral output entities such as cells, nodes, and journal records. - protocol enums. - formatter selection. - protocol-specific formatters. @@ -187,6 +189,10 @@ Top-level modules MUST NOT import implementation submodules from another top-lev Top-level modules MUST NOT import another top-level module's `operations` submodule. +Top-level modules MAY import protocol-neutral boundary entities from `./donna/protocol/` when they need to expose their owned concepts as Donna output units. + +Top-level modules outside `./donna/protocol/` and `./donna/cli/` SHOULD NOT depend on protocol-specific formatter implementation submodules. + ## Data structures Project data structures SHOULD inherit from `donna.core.entities.BaseEntity` unless a third-party interface or standard-library protocol requires another type. From 9b8ca73860c9bc35e708d1546d8cdb322dd1da2a Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 21:09:42 +0200 Subject: [PATCH 34/98] journal moved from machine to context --- donna/cli/commands/artifacts.py | 3 +- donna/cli/utils.py | 4 +- donna/context/__init__.py | 3 +- donna/context/context.py | 7 +++ donna/context/journal.py | 63 +++++++++++++++++++++++++ donna/machine/journal.py | 54 --------------------- donna/machine/state.py | 9 ++-- donna/machine/tasks.py | 3 +- donna/primitives/sections/run_script.py | 6 +-- donna/runtime/sessions.py | 3 +- 10 files changed, 84 insertions(+), 71 deletions(-) create mode 100644 donna/context/journal.py delete mode 100644 donna/machine/journal.py diff --git a/donna/cli/commands/artifacts.py b/donna/cli/commands/artifacts.py index 996736a7..1a117fb1 100644 --- a/donna/cli/commands/artifacts.py +++ b/donna/cli/commands/artifacts.py @@ -8,7 +8,6 @@ from donna.cli.types import ArtifactIdArgument, ArtifactIdsArgument, RenderModeOption, parse_artifact_id_argument from donna.cli.utils import command_context from donna.context.context import context -from donna.machine import journal as machine_journal from donna.protocol.cell_shortcuts import operation_succeeded from donna.protocol.errors import environment_error_node from donna.workspaces.artifacts import RENDER_CONTEXT_VIEW, ArtifactRenderContext, fetch_artifact_bytes @@ -16,7 +15,7 @@ def _log_artifact_operation(message: str) -> None: - machine_journal.add(message=message) + context().journal.add(message=message) @app.command(name="list", help="List available workflow artifacts and show their status summaries.") diff --git a/donna/cli/utils.py b/donna/cli/utils.py index 0928e466..cd2b9ccc 100644 --- a/donna/cli/utils.py +++ b/donna/cli/utils.py @@ -113,12 +113,12 @@ def command_context(context: typer.Context, *, load_environment: bool = True) -> def _write_errors_to_journal(errors: ErrorsList) -> None: - from donna.machine import journal as machine_journal + from donna.context.context import context for error in errors: message = f"Error: {environment_error_node(error).journal_message()} [{error.code}]" - machine_journal.add( + context().journal.add( message=message, actor_id="donna", ) diff --git a/donna/context/__init__.py b/donna/context/__init__.py index 1bb672b8..44a7f0ca 100644 --- a/donna/context/__init__.py +++ b/donna/context/__init__.py @@ -1,3 +1,4 @@ from donna.context.context import Context, context, reset_context, set_context +from donna.context.journal import Journal -__all__ = ("Context", "context", "set_context", "reset_context") +__all__ = ("Context", "Journal", "context", "set_context", "reset_context") diff --git a/donna/context/context.py b/donna/context/context.py index e93491ca..4b88aef1 100644 --- a/donna/context/context.py +++ b/donna/context/context.py @@ -1,6 +1,7 @@ import contextvars from donna.context.artifacts import ArtifactsCache +from donna.context.journal import Journal from donna.context.output import NoopEmitter, OutputEmitter from donna.context.primitives import PrimitivesCache from donna.context.state import StateCache @@ -14,6 +15,7 @@ class Context: "_artifacts", "_state", "_primitives", + "_journal", "output", "current_work_unit_id", "current_operation_id", @@ -23,6 +25,7 @@ def __init__(self, output: OutputEmitter | None = None) -> None: self._artifacts = ArtifactsCache() self._state = StateCache() self._primitives = PrimitivesCache() + self._journal = Journal(self) self.output = output if output is not None else NoopEmitter() self.current_work_unit_id: ValueScope[WorkUnitId] = ValueScope() self.current_operation_id: ValueScope[ArtifactSectionId] = ValueScope() @@ -39,6 +42,10 @@ def state(self) -> StateCache: def primitives(self) -> PrimitivesCache: return self._primitives + @property + def journal(self) -> Journal: + return self._journal + _context_var: contextvars.ContextVar[Context | None] = contextvars.ContextVar("donna_machine_context", default=None) diff --git a/donna/context/journal.py b/donna/context/journal.py new file mode 100644 index 00000000..e71a9495 --- /dev/null +++ b/donna/context/journal.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from donna.core.errors import ErrorsList +from donna.core.result import Ok, Result, unwrap_to_error +from donna.core.utils import now +from donna.domain.artifact_ids import ArtifactSectionId +from donna.domain.internal_ids import TaskId, WorkUnitId +from donna.protocol import errors as protocol_errors +from donna.protocol import journal as protocol_journal +from donna.protocol import modes as protocol_modes +from donna.workspaces import journal as workspace_journal +from donna.workspaces.config import protocol as protocol_mode + +if TYPE_CHECKING: + from donna.context.context import Context + + +class Journal: + __slots__ = ("_context",) + + def __init__(self, context: Context) -> None: + self._context = context + + def smart_actor_id(self) -> str: + match protocol_mode(): + case protocol_modes.Mode.human: + return "human" + case protocol_modes.Mode.llm: + return "agent" + case protocol_modes.Mode.automation: + return "automation" + case _: + raise protocol_errors.UnsupportedFormatterMode(mode=protocol_mode()) + + @unwrap_to_error + def add( + self, + message: str, + actor_id: str | None = None, + ) -> Result[protocol_journal.JournalRecord, ErrorsList]: + if actor_id is None: + actor_id = self.smart_actor_id() + + state = self._context.state.load().unwrap() + parsed_task_id: TaskId | None = state.current_task.id if state.current_task else None + parsed_work_unit_id: WorkUnitId | None = self._context.current_work_unit_id.get() + parsed_operation_id: ArtifactSectionId | None = self._context.current_operation_id.get() + + record = protocol_journal.JournalRecord( + timestamp=now(), + actor_id=actor_id, + message=message, + current_task_id=parsed_task_id, + current_work_unit_id=parsed_work_unit_id, + current_operation_id=parsed_operation_id, + ) + + workspace_journal.write_record(record).unwrap() + self._context.output.emit_journal(record) + + return Ok(record) diff --git a/donna/machine/journal.py b/donna/machine/journal.py deleted file mode 100644 index 7e48f74d..00000000 --- a/donna/machine/journal.py +++ /dev/null @@ -1,54 +0,0 @@ -from donna.core.errors import ErrorsList -from donna.core.result import Ok, Result, unwrap_to_error -from donna.core.utils import now -from donna.domain.artifact_ids import ArtifactSectionId -from donna.domain.internal_ids import TaskId, WorkUnitId -from donna.machine import errors as machine_errors -from donna.protocol import journal as protocol_journal -from donna.workspaces import journal as workspace_journal -from donna.workspaces.config import protocol as protocol_mode - - -def smart_agent_id() -> str: - from donna.protocol.modes import Mode as ProtocolMode - - match protocol_mode(): - case ProtocolMode.human: - return "human" - case ProtocolMode.llm: - return "agent" - case ProtocolMode.automation: - return "automation" - case _: - raise machine_errors.UnsupportedFormatterMode(mode=protocol_mode()) - - -@unwrap_to_error -def add( # noqa: CCR001 - message: str, - actor_id: str | None = None, -) -> Result[protocol_journal.JournalRecord, ErrorsList]: - from donna.context.context import context - - if actor_id is None: - actor_id = smart_agent_id() - - ctx = context() - state = ctx.state.load().unwrap() - parsed_task_id: TaskId | None = state.current_task.id if state.current_task else None - parsed_work_unit_id: WorkUnitId | None = ctx.current_work_unit_id.get() - parsed_operation_id: ArtifactSectionId | None = ctx.current_operation_id.get() - - record = protocol_journal.JournalRecord( - timestamp=now(), - actor_id=actor_id, - message=message, - current_task_id=parsed_task_id, - current_work_unit_id=parsed_work_unit_id, - current_operation_id=parsed_operation_id, - ) - - workspace_journal.write_record(record).unwrap() - ctx.output.emit_journal(record) - - return Ok(record) diff --git a/donna/machine/state.py b/donna/machine/state.py index 2a2098b1..c9468882 100644 --- a/donna/machine/state.py +++ b/donna/machine/state.py @@ -11,7 +11,6 @@ from donna.domain.artifact_ids import ArtifactSectionId, split_artifact_section_id from donna.domain.internal_ids import ActionRequestId, InternalId, TaskId, WorkUnitId from donna.machine import errors as machine_errors -from donna.machine import journal as machine_journal from donna.machine.action_requests import ActionRequest from donna.machine.changes import ( Change, @@ -124,7 +123,7 @@ def mark_started(self) -> None: def add_action_request(self, action_request: ActionRequest) -> None: full_request = action_request.replace(id=self.next_action_request_id()) - machine_journal.add( + context().journal.add( actor_id="donna", message=f"Request agent action `{full_request.title}`", ).unwrap() @@ -162,7 +161,7 @@ def complete_action_request( assert current_task is not None action_request = self.get_action_request(request_id).unwrap() - machine_journal.add( + context().journal.add( message=f"Complete agent action `{action_request.title}`", ).unwrap() @@ -180,7 +179,7 @@ def start_workflow(self, full_operation_id: ArtifactSectionId) -> Result[None, E artifact = context().artifacts.load(operation_parts.artifact_id, RENDER_CONTEXT_VIEW).unwrap() workflow = artifact.get_section(operation_parts.section_id).unwrap() - machine_journal.add( + context().journal.add( message=f"Start workflow `{workflow.title}`", ).unwrap() @@ -196,7 +195,7 @@ def finish_workflow(self, task_id: TaskId) -> None: artifact = context().artifacts.load(workflow_parts.artifact_id, RENDER_CONTEXT_VIEW).unwrap() workflow = artifact.get_section(workflow_parts.section_id).unwrap() - machine_journal.add( + context().journal.add( message=f"Finish workflow `{workflow.title}`", ).unwrap() diff --git a/donna/machine/tasks.py b/donna/machine/tasks.py index 13c08c10..ea9413ef 100644 --- a/donna/machine/tasks.py +++ b/donna/machine/tasks.py @@ -55,7 +55,6 @@ def build( @unwrap_to_error def run(self, task: Task) -> Result[list["Change"], ErrorsList]: from donna.context.context import context - from donna.machine import journal as machine_journal from donna.workspaces.artifacts import ArtifactRenderContext from donna.workspaces.templates import RenderMode @@ -72,7 +71,7 @@ def run(self, task: Task) -> Result[list["Change"], ErrorsList]: operation = artifact.get_section(operation_parts.section_id).unwrap() operation_kind = ctx.primitives.resolve(operation.kind).unwrap() - machine_journal.add( + ctx.journal.add( actor_id="donna", message=operation.title, ).unwrap() diff --git a/donna/primitives/sections/run_script.py b/donna/primitives/sections/run_script.py index 7ca7955d..8ccc327f 100644 --- a/donna/primitives/sections/run_script.py +++ b/donna/primitives/sections/run_script.py @@ -5,13 +5,13 @@ import pydantic +from donna.context.context import context from donna.core import errors as core_errors from donna.core.errors import ErrorsList from donna.core.result import Err, Ok, Result, unwrap_to_error from donna.domain.artifact_ids import ArtifactId, artifact_section_id, split_artifact_section_id from donna.domain.ids import SectionId from donna.domain.paths import ProjectRootPath -from donna.machine import journal as machine_journal from donna.machine.artifacts import Artifact, ArtifactSectionConfig, ArtifactSectionMeta from donna.machine.errors import ArtifactValidationError from donna.machine.operations import OperationConfig, OperationKind, OperationMeta @@ -152,7 +152,7 @@ def execute_section( script = meta.script assert script is not None - machine_journal.add( + context().journal.add( actor_id="donna", message=f"Run script `{operation.title}`", ).unwrap() @@ -163,7 +163,7 @@ def execute_section( project_dir=workspace_config.project_dir(), ) - machine_journal.add( + context().journal.add( actor_id="donna", message=( f"Script finished `{operation.title}`, exit code: {exit_code}, " diff --git a/donna/runtime/sessions.py b/donna/runtime/sessions.py index b62624c4..678efe8a 100644 --- a/donna/runtime/sessions.py +++ b/donna/runtime/sessions.py @@ -7,7 +7,6 @@ from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId, artifact_section_id, split_artifact_section_id from donna.domain.internal_ids import ActionRequestId from donna.machine import errors as machine_errors -from donna.machine import journal as machine_journal from donna.machine.operations import OperationMeta from donna.machine.state import ConsistentState, MutableState from donna.protocol.cell_shortcuts import operation_succeeded @@ -74,7 +73,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> CellsResult: def new_session() -> Result[list[Cell], ErrorsList]: _save_state(MutableState.build().freeze()).unwrap() - machine_journal.add(message="Created new session state.").unwrap() + context().journal.add(message="Created new session state.").unwrap() return Ok([operation_succeeded("Created new session state.")]) From 65989425b5bfd9e533519712faac40afcfcc05d5 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 21:49:20 +0200 Subject: [PATCH 35/98] decouplin machine & context --- donna/cli/types.py | 2 +- donna/cli/utils.py | 17 ++++- donna/context/artifacts.py | 25 +++++++- donna/context/context.py | 2 +- donna/context/value_scope.py | 24 ------- donna/machine/artifacts.py | 3 +- donna/machine/context.py | 97 +++++++++++++++++++++++++++++ donna/machine/errors.py | 4 ++ donna/machine/state.py | 7 +-- donna/machine/tasks.py | 12 +--- donna/machine/templates.py | 29 +++++++-- donna/workspaces/artifacts.py | 2 +- donna/workspaces/markdown_parser.py | 3 +- donna/workspaces/templates.py | 26 +------- 14 files changed, 177 insertions(+), 76 deletions(-) delete mode 100644 donna/context/value_scope.py create mode 100644 donna/machine/context.py diff --git a/donna/cli/types.py b/donna/cli/types.py index bb88d52c..e8c2b4f1 100644 --- a/donna/cli/types.py +++ b/donna/cli/types.py @@ -15,11 +15,11 @@ from donna.domain.constants import DONNA_ARTIFACT_EXTENSION from donna.domain.internal_ids import ActionRequestId from donna.domain.paths import PathInput, UntrustedPath +from donna.machine.templates import RenderMode from donna.protocol.errors import environment_error_node from donna.protocol.modes import Mode from donna.workspaces import paths as workspace_paths from donna.workspaces.artifacts import has_donna_artifact_extension -from donna.workspaces.templates import RenderMode def _exit_with_errors(errors: ErrorsList) -> NoReturn: diff --git a/donna/cli/utils.py b/donna/cli/utils.py index cd2b9ccc..76a6de6b 100644 --- a/donna/cli/utils.py +++ b/donna/cli/utils.py @@ -2,10 +2,12 @@ import sys from collections.abc import Iterable, Iterator from contextlib import contextmanager +from contextvars import Token import typer from donna.cli.entities import GLOBAL_OPTIONS_CONTEXT_KEY, GlobalOptions +from donna.context.context import Context from donna.core.errors import EnvironmentError, ErrorsList from donna.core.result import UnwrapError from donna.domain.constants import DONNA_CONFIG_NAME @@ -95,21 +97,32 @@ def write_cells(self, cells: Iterable[Cell]) -> None: @contextmanager def command_context(context: typer.Context, *, load_environment: bool = True) -> Iterator[CommandContext]: - from donna.context import Context, set_context + from donna.context import reset_context, set_context + from donna.machine import context as machine_context command = CommandContext(context) + context_token: Token[Context | None] | None = None + machine_context_token: Token[machine_context.MachineContext | None] | None = None try: command.install_protocol() if load_environment: command.load_workspace() - set_context(Context(output=command.emitter)) + runtime_context = Context(output=command.emitter) + context_token = set_context(runtime_context) + machine_context_token = machine_context.set_context(runtime_context) yield command except UnwrapError as error: command.write_cells(_cells_from_unwrap(error)) raise typer.Exit(code=0) from error + finally: + if machine_context_token is not None: + machine_context.reset_context(machine_context_token) + + if context_token is not None: + reset_context(context_token) def _write_errors_to_journal(errors: ErrorsList) -> None: diff --git a/donna/context/artifacts.py b/donna/context/artifacts.py index 14bf9323..61d38617 100644 --- a/donna/context/artifacts.py +++ b/donna/context/artifacts.py @@ -4,8 +4,9 @@ from donna.core.result import Err, Ok, Result, unwrap_to_error from donna.domain.artifact_ids import ArtifactId from donna.machine.artifacts import Artifact +from donna.machine.tasks import Task, WorkUnit +from donna.machine.templates import RenderMode from donna.workspaces import errors as workspace_errors -from donna.workspaces.templates import RenderMode if TYPE_CHECKING: from donna.workspaces.artifacts import ArtifactRenderContext, FilesystemRawArtifact @@ -78,6 +79,28 @@ def _get_cache_value(self, artifact_id: ArtifactId) -> Result[_ArtifactCacheValu def invalidate(self, artifact_id: ArtifactId) -> None: self._cache.pop(artifact_id, None) + def load_for_view(self, artifact_id: ArtifactId) -> Result[Artifact, ErrorsList]: + from donna.workspaces.artifacts import RENDER_CONTEXT_VIEW + + return self.load(artifact_id, RENDER_CONTEXT_VIEW) + + def load_for_execution( + self, + artifact_id: ArtifactId, + task: Task, + work_unit: WorkUnit, + ) -> Result[Artifact, ErrorsList]: + from donna.workspaces.artifacts import ArtifactRenderContext + + return self.load( + artifact_id, + ArtifactRenderContext( + primary_mode=RenderMode.execute, + current_task=task, + current_work_unit=work_unit, + ), + ) + @unwrap_to_error def load( # noqa: CCR001 self, diff --git a/donna/context/context.py b/donna/context/context.py index 4b88aef1..bf01c1c8 100644 --- a/donna/context/context.py +++ b/donna/context/context.py @@ -5,9 +5,9 @@ from donna.context.output import NoopEmitter, OutputEmitter from donna.context.primitives import PrimitivesCache from donna.context.state import StateCache -from donna.context.value_scope import ValueScope from donna.domain.artifact_ids import ArtifactSectionId from donna.domain.internal_ids import WorkUnitId +from donna.machine.context import ValueScope class Context: diff --git a/donna/context/value_scope.py b/donna/context/value_scope.py deleted file mode 100644 index beac9bfd..00000000 --- a/donna/context/value_scope.py +++ /dev/null @@ -1,24 +0,0 @@ -from collections.abc import Iterator -from contextlib import contextmanager -from typing import Generic, TypeVar - -V = TypeVar("V") - - -class ValueScope(Generic[V]): - __slots__ = ("_value",) - - def __init__(self, initial: V | None = None) -> None: - self._value: V | None = initial - - def get(self) -> V | None: - return self._value - - @contextmanager - def scope(self, value: V | None) -> Iterator[None]: - previous = self._value - self._value = value - try: - yield - finally: - self._value = previous diff --git a/donna/machine/artifacts.py b/donna/machine/artifacts.py index 3d945247..e58ed815 100644 --- a/donna/machine/artifacts.py +++ b/donna/machine/artifacts.py @@ -6,6 +6,7 @@ from donna.domain.artifact_ids import ArtifactId from donna.domain.ids import SectionId from donna.domain.python_path import PythonPath +from donna.machine.context import context from donna.machine.errors import ArtifactPrimarySectionMissing, ArtifactSectionNotFound, MultiplePrimarySectionsError from donna.protocol.cells import Cell from donna.protocol.errors import environment_error_node @@ -63,8 +64,6 @@ def primary_section(self) -> Result[ArtifactSection, ErrorsList]: return Ok(primary_sections[0]) def validate_artifact(self) -> Result[None, ErrorsList]: # noqa: CCR001 - from donna.context.context import context - primary_sections = self._primary_sections() errors: ErrorsList = [] diff --git a/donna/machine/context.py b/donna/machine/context.py new file mode 100644 index 00000000..1b355400 --- /dev/null +++ b/donna/machine/context.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import contextvars +from collections.abc import Iterator +from contextlib import contextmanager +from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar + +from donna.core.errors import ErrorsList +from donna.core.result import Result +from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId +from donna.domain.internal_ids import WorkUnitId +from donna.domain.python_path import PythonPath +from donna.machine import errors as machine_errors + +if TYPE_CHECKING: + from donna.machine.artifacts import Artifact + from donna.machine.primitives import Primitive + from donna.machine.tasks import Task, WorkUnit + +TScopedValue = TypeVar("TScopedValue") + + +class ValueScope(Generic[TScopedValue]): + __slots__ = ("_value",) + + def __init__(self, initial: TScopedValue | None = None) -> None: + self._value: TScopedValue | None = initial + + def get(self) -> TScopedValue | None: + return self._value + + @contextmanager + def scope(self, value: TScopedValue | None) -> Iterator[None]: + previous = self._value + self._value = value + try: + yield + finally: + self._value = previous + + +class MachineArtifacts(Protocol): + def load_for_view(self, artifact_id: ArtifactId) -> Result["Artifact", ErrorsList]: ... + + def load_for_execution( + self, + artifact_id: ArtifactId, + task: "Task", + work_unit: "WorkUnit", + ) -> Result["Artifact", ErrorsList]: ... + + +class MachinePrimitives(Protocol): + def resolve(self, primitive_id: PythonPath) -> Result["Primitive", ErrorsList]: ... + + +class MachineJournal(Protocol): + def add(self, message: str, actor_id: str | None = None) -> Result[Any, ErrorsList]: ... + + +class MachineContext(Protocol): + @property + def artifacts(self) -> MachineArtifacts: ... + + @property + def primitives(self) -> MachinePrimitives: ... + + @property + def journal(self) -> MachineJournal: ... + + @property + def current_work_unit_id(self) -> ValueScope[WorkUnitId]: ... + + @property + def current_operation_id(self) -> ValueScope[ArtifactSectionId]: ... + + +_context_var: contextvars.ContextVar[MachineContext | None] = contextvars.ContextVar( + "donna_machine_context", + default=None, +) + + +def set_context(new_context: MachineContext) -> contextvars.Token[MachineContext | None]: + return _context_var.set(new_context) + + +def reset_context(token: contextvars.Token[MachineContext | None]) -> None: + _context_var.reset(token) + + +def context() -> MachineContext: + current = _context_var.get() + if current is None: + raise machine_errors.MachineContextNotSet() + + return current diff --git a/donna/machine/errors.py b/donna/machine/errors.py index a47a6413..4f720edd 100644 --- a/donna/machine/errors.py +++ b/donna/machine/errors.py @@ -18,6 +18,10 @@ class SessionStateStatusInvalid(InternalError): message: str = "Session state status is invalid." +class MachineContextNotSet(InternalError): + message: str = "Machine context is not initialized." + + class UnsupportedFormatterMode(InternalError): message: str = "Formatter for mode '{mode}' is not implemented." diff --git a/donna/machine/state.py b/donna/machine/state.py index c9468882..ab7a5cb9 100644 --- a/donna/machine/state.py +++ b/donna/machine/state.py @@ -4,7 +4,6 @@ import pydantic -from donna.context.context import context from donna.core.entities import BaseEntity from donna.core.errors import ErrorsList from donna.core.result import Err, Ok, Result, unwrap_to_error @@ -20,10 +19,10 @@ ChangeRemoveTask, ChangeRemoveWorkUnit, ) +from donna.machine.context import context from donna.machine.tasks import Task, WorkUnit from donna.protocol.cells import Cell from donna.protocol.nodes import Node -from donna.workspaces.artifacts import RENDER_CONTEXT_VIEW class BaseState(BaseEntity): @@ -176,7 +175,7 @@ def complete_action_request( def start_workflow(self, full_operation_id: ArtifactSectionId) -> Result[None, ErrorsList]: operation_parts = split_artifact_section_id(full_operation_id) assert operation_parts is not None - artifact = context().artifacts.load(operation_parts.artifact_id, RENDER_CONTEXT_VIEW).unwrap() + artifact = context().artifacts.load_for_view(operation_parts.artifact_id).unwrap() workflow = artifact.get_section(operation_parts.section_id).unwrap() context().journal.add( @@ -192,7 +191,7 @@ def finish_workflow(self, task_id: TaskId) -> None: assert task is not None workflow_parts = split_artifact_section_id(task.workflow_id) assert workflow_parts is not None - artifact = context().artifacts.load(workflow_parts.artifact_id, RENDER_CONTEXT_VIEW).unwrap() + artifact = context().artifacts.load_for_view(workflow_parts.artifact_id).unwrap() workflow = artifact.get_section(workflow_parts.section_id).unwrap() context().journal.add( diff --git a/donna/machine/tasks.py b/donna/machine/tasks.py index ea9413ef..351a814a 100644 --- a/donna/machine/tasks.py +++ b/donna/machine/tasks.py @@ -6,6 +6,7 @@ from donna.core.result import Ok, Result, unwrap_to_error from donna.domain.artifact_ids import ArtifactSectionId, split_artifact_section_id from donna.domain.internal_ids import TaskId, WorkUnitId +from donna.machine.context import context if TYPE_CHECKING: from donna.machine.changes import Change @@ -54,20 +55,11 @@ def build( @unwrap_to_error def run(self, task: Task) -> Result[list["Change"], ErrorsList]: - from donna.context.context import context - from donna.workspaces.artifacts import ArtifactRenderContext - from donna.workspaces.templates import RenderMode - - render_context = ArtifactRenderContext( - primary_mode=RenderMode.execute, - current_task=task, - current_work_unit=self, - ) ctx = context() with ctx.current_operation_id.scope(self.operation_id): operation_parts = split_artifact_section_id(self.operation_id) assert operation_parts is not None - artifact = ctx.artifacts.load(operation_parts.artifact_id, render_context).unwrap() + artifact = ctx.artifacts.load_for_execution(operation_parts.artifact_id, task, self).unwrap() operation = artifact.get_section(operation_parts.section_id).unwrap() operation_kind = ctx.primitives.resolve(operation.kind).unwrap() diff --git a/donna/machine/templates.py b/donna/machine/templates.py index 608a245f..3ec385fb 100644 --- a/donna/machine/templates.py +++ b/donna/machine/templates.py @@ -1,3 +1,4 @@ +import enum from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, TypeAlias @@ -15,6 +16,26 @@ PreparedDirectiveResult: TypeAlias = Result[PreparedDirectiveArguments, ErrorsList] +class RenderMode(enum.StrEnum): + """Modes for rendering artifacts. + + Donna can render artifacts for different purposes, for example: + + - to be displayed to the agent when Donna is used via CLI. + - to be used for execution by Donna itself. + - to be used for analysis by Donna itself. + + In each mode Donna can produce different outputs. + + For example, it can output CLI commands in view/execute mode, + tool specifications in tool mode, special markup in analyze mode, etc. + """ + + view = "view" + execute = "execute" + analysis = "analysis" + + class DirectiveUnsupportedRenderMode(machine_errors.InternalError): message: str = "Render mode {render_mode} not implemented in directive {directive_name}." render_mode: Any @@ -30,8 +51,6 @@ def apply_directive( # noqa: E704 *argv: Any, **kwargs: Any, ) -> Result[Any, ErrorsList]: - from donna.workspaces import templates as world_templates - render_mode = context["render_mode"] arguments_result = self._prepare_arguments(context, *argv, **kwargs) if arguments_result.is_err(): @@ -40,11 +59,11 @@ def apply_directive( # noqa: E704 argv = arguments_result.unwrap() match render_mode: - case world_templates.RenderMode.view: + case RenderMode.view: return self.render_view(context, *argv) - case world_templates.RenderMode.execute: + case RenderMode.execute: return self.render_execute(context, *argv) - case world_templates.RenderMode.analysis: + case RenderMode.analysis: return self.render_analyze(context, *argv) case _: raise DirectiveUnsupportedRenderMode(render_mode=render_mode, directive_name=self.__class__.__name__) diff --git a/donna/workspaces/artifacts.py b/donna/workspaces/artifacts.py index 29475c77..21bd2156 100644 --- a/donna/workspaces/artifacts.py +++ b/donna/workspaces/artifacts.py @@ -8,10 +8,10 @@ from donna.domain.constants import DONNA_ARTIFACT_EXTENSION from donna.domain.paths import ProjectPathId, RelativeProjectPath, ResolvedProjectPath, UntrustedPath from donna.machine.tasks import Task, WorkUnit +from donna.machine.templates import RenderMode from donna.workspaces import errors as world_errors from donna.workspaces.files import FileFingerprint from donna.workspaces.paths import normalize_existing_path -from donna.workspaces.templates import RenderMode if TYPE_CHECKING: from donna.machine.artifacts import Artifact diff --git a/donna/workspaces/markdown_parser.py b/donna/workspaces/markdown_parser.py index 39b1df6c..c23ec7ca 100644 --- a/donna/workspaces/markdown_parser.py +++ b/donna/workspaces/markdown_parser.py @@ -8,10 +8,11 @@ from donna.domain.python_path import PythonPath from donna.machine.artifacts import Artifact, ArtifactSection, ArtifactSectionConfig, ArtifactSectionMeta from donna.machine.primitives import Primitive, resolve_primitive +from donna.machine.templates import RenderMode from donna.workspaces import errors as world_errors from donna.workspaces import markdown from donna.workspaces.artifacts import ArtifactRenderContext -from donna.workspaces.templates import RenderMode, render +from donna.workspaces.templates import render class MarkdownSectionConstructor(Protocol): diff --git a/donna/workspaces/templates.py b/donna/workspaces/templates.py index 53c9902c..d02c0d31 100644 --- a/donna/workspaces/templates.py +++ b/donna/workspaces/templates.py @@ -1,6 +1,5 @@ from __future__ import annotations -import enum import importlib import importlib.util from typing import TYPE_CHECKING @@ -11,34 +10,13 @@ from donna.core.errors import EnvironmentErrorsProxy, ErrorsList from donna.core.result import Err, Ok, Result from donna.domain.artifact_ids import ArtifactId -from donna.machine.templates import Directive +from donna.machine.templates import Directive, RenderMode from donna.workspaces import errors as world_errors if TYPE_CHECKING: from donna.workspaces.artifacts import ArtifactRenderContext -class RenderMode(enum.Enum): - """Modes for rendering artifacts. - - Donna could render artifacts for different purposes, for example: - - - to be displayed to the agent when Donna is used via CLI - - TODO: to be displayed to the agent when Donna is used as an agent tool - - TODO: to be displayed to the agent when Donna is used as an MCP server - - to be used for analysis by Donna itself - - In each mode Donna can produce different outputs. - - For example, it can output CLI commands in view/execute mode, tool specifications in tool mode, - special markup in analyze mode, etc. - """ - - view = "view" - execute = "execute" - analysis = "analysis" - - _ENVIRONMENT = None @@ -164,7 +142,7 @@ def env() -> jinja2.Environment: def render(artifact_id: ArtifactId, template: str, render_context: "ArtifactRenderContext") -> Result[str, ErrorsList]: - context = {"render_mode": render_context.primary_mode, "artifact_id": artifact_id} + context: dict[str, object] = {"render_mode": render_context.primary_mode, "artifact_id": artifact_id} if render_context.current_task is not None: context["current_task"] = render_context.current_task From f77fa3efb62807adfe185a1f2f2fd908b2948896 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 22:00:14 +0200 Subject: [PATCH 36/98] better protocol specs --- specs/architecture/modules_layout.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specs/architecture/modules_layout.md b/specs/architecture/modules_layout.md index 0a09e55b..ba05ff4d 100644 --- a/specs/architecture/modules_layout.md +++ b/specs/architecture/modules_layout.md @@ -63,6 +63,7 @@ The following topics are out of scope: - formatter selection. - protocol-specific formatters. - serialized record construction for external output protocols. + - low-level output boundary infrastructure that MAY be used by any top-level module. - `./donna/skills/` — module responsible for built-in skill text loaded by the CLI and renderers. - `./donna/workspaces/` — module responsible for workspace management, including: - finding and parsing config. @@ -189,7 +190,7 @@ Top-level modules MUST NOT import implementation submodules from another top-lev Top-level modules MUST NOT import another top-level module's `operations` submodule. -Top-level modules MAY import protocol-neutral boundary entities from `./donna/protocol/` when they need to expose their owned concepts as Donna output units. +Top-level modules MAY import boundary entities and rendering helpers from `./donna/protocol/` when they need to expose their owned concepts as Donna output units. Top-level modules outside `./donna/protocol/` and `./donna/cli/` SHOULD NOT depend on protocol-specific formatter implementation submodules. From 76def51f590df4406729cf132e59f8f53be566f4 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 22:21:58 +0200 Subject: [PATCH 37/98] two types of entities: domain + data --- specs/architecture/entities.md | 41 ++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/specs/architecture/entities.md b/specs/architecture/entities.md index ddce14b3..d11e39b1 100644 --- a/specs/architecture/entities.md +++ b/specs/architecture/entities.md @@ -21,6 +21,8 @@ The following topics are out of scope: - `entity` — a typed Python object that represents one project concept and can be passed across module boundaries. - `value entity` — an entity whose equality is based on its data rather than object identity. +- `data entity` — an entity that primarily carries validated data across boundaries or into serialization. +- `domain entity` — an entity that owns behavior, invariants, or state transitions for a project concept. - `boundary entity` — an entity that is passed between modules with different responsibilities. - `serialized representation` — a plain data representation prepared for an external protocol such as JSON Lines, Markdown output, logs, or persistent storage. @@ -32,9 +34,32 @@ Boundary entities MUST NOT be represented as untyped dictionaries unless the dat Project entities SHOULD use Pydantic v2 for structured data models. -Project entities SHOULD keep behavior close to their data only when that behavior is pure and local to the entity. +### Data entities -Entities MUST NOT perform: +Data entities SHOULD limit behavior to validating, normalizing, copying, serializing, and deriving values from their +own fields. + +Data entities MUST NOT access invocation-local context, load artifacts, execute primitives, mutate session state, or +perform low-level infrastructure work. + +### Domain entities + +Domain entities MAY be rich domain objects. + +Domain entities SHOULD keep behavior close to the state, invariants, transitions, and domain operations they own. +They MAY: + +- change their own state or apply state changes. +- derive facts from their own state. +- validate related project objects. +- coordinate behavior through typed collaborator or invocation-local context interfaces. + +Domain entity methods SHOULD keep direct infrastructure details behind collaborator interfaces or module boundaries. + +If an entity directly touches an external resource, the entity MUST be owned by the module responsible for that +resource and SHOULD represent that resource explicitly. + +Entities MUST NOT directly perform low-level infrastructure work unrelated to their owned concept, such as: - filesystem access. - process execution. @@ -42,6 +67,11 @@ Entities MUST NOT perform: - configuration file discovery. - workspace loading. +Entities MAY call typed collaborators or context objects that perform those operations when the behavior and +dependency are part of the module's public contract. + +### Value entities + Value entities SHOULD be immutable after construction when practical. Entities that can be used for de-duplication SHOULD be hashable when practical. @@ -197,8 +227,11 @@ Collections that represent stacks, queues, or ordered workflow state MUST preser Parsing layers SHOULD validate external data before creating entities that are used by lower layers. -Pydantic model validation MAY validate local invariants that are always true for the entity. +Pydantic model validation MAY validate local invariants that are always true for the entity, but MUST NOT perform +filesystem access, configuration discovery, workspace loading, artifact discovery, primitive execution, subprocess +execution, or command execution. -Validation that requires filesystem access, configuration discovery, workspace loading, artifact discovery, primitive execution, subprocess execution, or command execution MUST live outside pure entity definitions. +Validation that requires those operations MUST live in domain behavior methods, domain services, runtime +orchestration, or operation functions that return Donna-specific errors. Invalid external input MUST be reported through the error architecture instead of by returning partially valid entities. From c04a9569369af90310670852fc55075ffcbc8f25 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 22:40:05 +0200 Subject: [PATCH 38/98] better protocol specs --- specs/architecture/modules_layout.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/specs/architecture/modules_layout.md b/specs/architecture/modules_layout.md index ba05ff4d..a6bffba6 100644 --- a/specs/architecture/modules_layout.md +++ b/specs/architecture/modules_layout.md @@ -40,7 +40,7 @@ The following topics are out of scope: - state changes. - operation and primitive interfaces. - workflow execution orchestration that is independent of CLI argument parsing and protocol-specific formatting. - - protocol-facing views of machine-owned entities expressed through protocol-neutral boundary entities. + - protocol-facing views of machine-owned entities expressed through protocol-neutral boundary entities from `./donna/protocol/`. - `./donna/runtime/` — module responsible for command-independent runtime orchestration. Contains: - session lifecycle use cases. - workflow execution loop coordination. @@ -57,11 +57,12 @@ The following topics are out of scope: - directive primitives. - primitive-specific validation, rendering, and execution logic. - `./donna/lib/` — module responsible for stable public names of built-in primitive instances used by Donna artifact configuration. -- `./donna/protocol/` — module responsible for Donna output boundary entities and rendering. Contains: - - protocol-neutral output entities such as cells, nodes, and journal records. +- `./donna/protocol/` — module responsible for Donna output boundary entities and protocol formatting. Contains: + - protocol-neutral output boundary entities such as cells, nodes, and journal records. + - helpers for projecting generic Donna errors into output cells. - protocol enums. - formatter selection. - - protocol-specific formatters. + - protocol-specific formatters that serialize protocol entities for human, llm, and automation output. - serialized record construction for external output protocols. - low-level output boundary infrastructure that MAY be used by any top-level module. - `./donna/skills/` — module responsible for built-in skill text loaded by the CLI and renderers. @@ -190,9 +191,11 @@ Top-level modules MUST NOT import implementation submodules from another top-lev Top-level modules MUST NOT import another top-level module's `operations` submodule. -Top-level modules MAY import boundary entities and rendering helpers from `./donna/protocol/` when they need to expose their owned concepts as Donna output units. +Top-level modules MAY import boundary entities and generic projection helpers from `./donna/protocol/` when they need to expose their owned concepts as Donna output units. -Top-level modules outside `./donna/protocol/` and `./donna/cli/` SHOULD NOT depend on protocol-specific formatter implementation submodules. +Top-level modules outside `./donna/protocol/` and `./donna/cli/` MUST NOT depend on protocol-specific formatter implementation submodules. + +Top-level modules outside `./donna/protocol/` SHOULD NOT own protocol-specific serialization details. ## Data structures From 2c18e547ad421a79bfb86be3f59367a88ceb3b1d Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 22:53:07 +0200 Subject: [PATCH 39/98] continue with protocol specs --- specs/architecture/modules_layout.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/specs/architecture/modules_layout.md b/specs/architecture/modules_layout.md index a6bffba6..c531ddd7 100644 --- a/specs/architecture/modules_layout.md +++ b/specs/architecture/modules_layout.md @@ -40,7 +40,7 @@ The following topics are out of scope: - state changes. - operation and primitive interfaces. - workflow execution orchestration that is independent of CLI argument parsing and protocol-specific formatting. - - protocol-facing views of machine-owned entities expressed through protocol-neutral boundary entities from `./donna/protocol/`. + - protocol-facing views of machine-owned concepts expressed as protocol-neutral output values from `./donna/protocol/`. - `./donna/runtime/` — module responsible for command-independent runtime orchestration. Contains: - session lifecycle use cases. - workflow execution loop coordination. @@ -57,12 +57,12 @@ The following topics are out of scope: - directive primitives. - primitive-specific validation, rendering, and execution logic. - `./donna/lib/` — module responsible for stable public names of built-in primitive instances used by Donna artifact configuration. -- `./donna/protocol/` — module responsible for Donna output boundary entities and protocol formatting. Contains: - - protocol-neutral output boundary entities such as cells, nodes, and journal records. - - helpers for projecting generic Donna errors into output cells. +- `./donna/protocol/` — module responsible for Donna output boundary values and protocol formatting. Contains: + - protocol-neutral output value definitions used to communicate Donna results between modules. + - generic helpers for projecting Donna-owned data and errors into output values. - protocol enums. - formatter selection. - - protocol-specific formatters that serialize protocol entities for human, llm, and automation output. + - protocol-specific formatters that serialize output values for human, llm, and automation output. - serialized record construction for external output protocols. - low-level output boundary infrastructure that MAY be used by any top-level module. - `./donna/skills/` — module responsible for built-in skill text loaded by the CLI and renderers. @@ -191,12 +191,18 @@ Top-level modules MUST NOT import implementation submodules from another top-lev Top-level modules MUST NOT import another top-level module's `operations` submodule. -Top-level modules MAY import boundary entities and generic projection helpers from `./donna/protocol/` when they need to expose their owned concepts as Donna output units. +Top-level modules MAY import protocol-owned boundary values and generic projection helpers when they need to expose their owned concepts as Donna output units. + +Constructing protocol-neutral output values is not protocol-specific rendering and MUST NOT be treated as a module-boundary violation. Top-level modules outside `./donna/protocol/` and `./donna/cli/` MUST NOT depend on protocol-specific formatter implementation submodules. Top-level modules outside `./donna/protocol/` SHOULD NOT own protocol-specific serialization details. +Protocol-specific rendering means selecting or implementing concrete external serialization for a protocol, such as terminal text framing, LLM-oriented boundary syntax, automation JSON Lines records, byte output, or formatter-specific ordering rules. + +Constructing protocol-neutral output values with kind, content, media type, and metadata is not protocol-specific rendering. + ## Data structures Project data structures SHOULD inherit from `donna.core.entities.BaseEntity` unless a third-party interface or standard-library protocol requires another type. From 9e84220d93a26cbd82b146cfbf491878e340dbee Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 23:14:42 +0200 Subject: [PATCH 40/98] specs --- specs/architecture/entities.md | 7 +-- specs/architecture/errors.md | 2 + specs/architecture/modules_layout.md | 66 +++++++++++----------------- 3 files changed, 32 insertions(+), 43 deletions(-) diff --git a/specs/architecture/entities.md b/specs/architecture/entities.md index d11e39b1..e1d8a647 100644 --- a/specs/architecture/entities.md +++ b/specs/architecture/entities.md @@ -153,7 +153,9 @@ Shared domain primitive types and universal domain entities MUST belong to the d Module-specific entities MUST belong to the module that owns the corresponding responsibility. -A module MAY expose entities from its public package interface when doing so simplifies imports for callers. +Entity classes that are part of a top-level module's public cross-module API MUST be exported from the owning module's package initializer. + +Top-level modules MUST import public entities owned by another top-level module from the owning module's package root. Public re-exports MUST NOT hide ownership. The defining module MUST remain clear from the source tree. @@ -231,7 +233,6 @@ Pydantic model validation MAY validate local invariants that are always true for filesystem access, configuration discovery, workspace loading, artifact discovery, primitive execution, subprocess execution, or command execution. -Validation that requires those operations MUST live in domain behavior methods, domain services, runtime -orchestration, or operation functions that return Donna-specific errors. +Validation that requires those operations MUST live in behavior methods, runtime orchestration, or functions that return Donna-specific errors. Invalid external input MUST be reported through the error architecture instead of by returning partially valid entities. diff --git a/specs/architecture/errors.md b/specs/architecture/errors.md index c625e5d6..e2530edf 100644 --- a/specs/architecture/errors.md +++ b/specs/architecture/errors.md @@ -68,6 +68,8 @@ The environment root MAY have a more specific name when that name communicates t Each unique error case SHOULD be represented by its own subclass. +Each top-level module that owns an `errors` submodule MUST export it from the module package initializer. + Production errors MUST NOT be defined in test modules. Test-only error classes MAY be defined in test modules when they are required to verify error handling behavior. diff --git a/specs/architecture/modules_layout.md b/specs/architecture/modules_layout.md index c531ddd7..759100a9 100644 --- a/specs/architecture/modules_layout.md +++ b/specs/architecture/modules_layout.md @@ -83,15 +83,15 @@ When a module contains a small closed family of interchangeable components, and Shared package-level code for such component families SHOULD be limited to common types, public unions, selection helpers, and iteration glue. -Some submodules have specific names that reflect their responsibilities and SHOULD be similar across different modules. +Submodules are optional implementation details unless another specification explicitly requires them. + +Some optional submodules have specific names that reflect their responsibilities and SHOULD be similar across different modules when those submodules exist. List of specific submodules: - `utils` — submodule responsible for utility functions that are not related to domain logic. - `errors` — submodule responsible for defining custom exception types. -- `domain` — submodule responsible for internal business logic related to the module's responsibilities. - `entities` — submodule responsible for defining types and entities related to the module's responsibilities. -- `operations` — submodule responsible for low-level communication with storage, local files, external tools, or other systems owned by the module. - `fixtures` — submodule containing reusable configuration, documentation, or data fixtures owned by the module. - `tests` — submodule containing module tests. - `tests.make` — test-only submodule containing constructors for test objects related to the parent module. @@ -109,7 +109,11 @@ The `errors` submodule owns module-specific exception classes and error values. Errors SHOULD express project-level failure modes that callers can handle, not low-level library details. -Top-level modules MAY import another top-level module's `errors` submodule when they need to catch or raise errors owned by that module. +Error classes and error values that are part of a top-level module's public API MUST be exported from that module's package initializer. + +When a top-level module owns an `errors` submodule, its package initializer MUST export that submodule under the name `errors`. + +Top-level modules MAY import another top-level module's exported `errors` submodule from the owning module's package root. #### `entities` @@ -117,39 +121,13 @@ The `entities` submodule owns module-specific types, semantic ids, enums, and en Entities SHOULD describe domain data and boundary data, not storage implementation details unless storage metadata is itself part of the project concept. -Top-level modules MAY import another top-level module's `entities` submodule for shared boundary types. - -#### `domain` - -The `domain` submodule owns the parent module's internal business logic and is the public behavior boundary for other top-level modules. - -Domain functions SHOULD compose lower-level operation functions and other module boundaries into meaningful workflows, own business decisions, convert or raise module-owned errors, and shape results for callers. - -Domain functions SHOULD hide low-level communication details from callers. - -Top-level input layers such as `./donna/cli/` SHOULD call domain boundaries instead of low-level operation helpers when invoking business behavior. - -When a domain-level function only exposes an operation function without adding behavior, the `domain` submodule SHOULD prefer a direct assignment alias instead of a trivial wrapper. - -Domain wrappers SHOULD be used when they add real behavior, such as orchestration, validation, persistence boundary ownership, fallback logic, caching, error conversion, or result shaping. - -#### `operations` - -The `operations` submodule owns low-level communication with systems outside the module's internal logic. - -Operation functions MAY communicate with local files, subprocesses, external tools, or other infrastructure owned by the module's responsibility. - -Operation functions SHOULD contain communication protocol details, low-level error translation, raw response handling, storage idempotency, and technical maintenance helpers owned by the module. - -Operation functions SHOULD NOT own high-level business workflows. They SHOULD provide small communication primitives that the module's `domain` submodule can compose. - -Operation functions SHOULD NOT be imported by other top-level modules. Cross-module callers SHOULD use the owning module's `domain`, `entities`, or `errors` boundary instead. +Entity classes and other shared boundary types that are part of a top-level module's public API MUST be exported from that module's package initializer. #### `utils` -The `utils` submodule owns small helpers that are not domain workflows and do not naturally belong to entities, operations, settings, or integration boundaries. +The `utils` submodule owns small helpers that do not naturally belong to entities, settings, integration boundaries, or a more specific submodule. -Utility functions SHOULD be pure or locally technical when practical. If a helper starts to encode module behavior, it SHOULD move to `domain`, `operations`, or a more specific submodule. +Utility functions SHOULD be pure or locally technical when practical. If a helper starts to encode module behavior, it SHOULD move to a more specific submodule. Top-level modules SHOULD avoid depending on another top-level module's `utils` submodule because utilities are not a stable cross-module boundary. @@ -157,7 +135,7 @@ Top-level modules SHOULD avoid depending on another top-level module's `utils` s The `tests` submodule owns colocated tests for the parent module and its submodules. -Tests SHOULD exercise behavior through public boundaries when practical, while operation tests MAY verify storage-specific behavior owned by the module. +Tests SHOULD exercise behavior through public boundaries when practical, while storage-focused tests MAY verify storage-specific behavior owned by the module. Test files SHOULD mirror implementation module names with the `test_` prefix and organize test classes around the tested function or class. @@ -175,7 +153,7 @@ Production modules MUST NOT import `tests.make`. The `helpers` submodule MAY appear inside tests packages, as `.tests.helpers`. -`tests.helpers` owns reusable test helpers that perform setup, mutate persisted test state, call module operations, or wrap common test workflows. +`tests.helpers` owns reusable test helpers that perform setup, mutate persisted test state, call module behavior, or wrap common test workflows. Object constructors and pure fake data factories SHOULD live in `tests.make`; helpers that perform actions or coordinate multiple calls SHOULD live in `tests.helpers`. @@ -183,15 +161,23 @@ Production modules MUST NOT import `tests.helpers`. ## Cross-module dependencies -Top-level modules that need types or values from another top-level module SHOULD use only that module's `domain`, `entities`, and `errors` submodules when those submodules exist. +Each top-level module MUST define its public cross-module API in its package initializer, `__init__.py`. + +The package initializer MUST explicitly import or define every type, function, constant, and object that another top-level module may import from the module. + +When a top-level module owns an `errors` submodule, the package initializer MUST include the `errors` submodule in the public cross-module API. + +The package initializer SHOULD define `__all__` to list the names that are intended as the module's public cross-module API. + +If a top-level module has no public cross-module API, its package initializer SHOULD be empty or define an empty `__all__`. -Top-level modules MAY import another module's `errors` submodule when they need to catch or raise errors owned by that module. +Top-level modules MUST import another top-level module's public API only from that module's package root. -Top-level modules MUST NOT import implementation submodules from another top-level module when a public boundary exists. +For example, `donna.cli` MAY import from `donna.workspaces`, including `from donna.workspaces import errors as workspace_errors` when `donna.workspaces` exports `errors`, but MUST NOT import from `donna.workspaces.config`, `donna.workspaces.entities`, `donna.workspaces.errors`, or any other `donna.workspaces` submodule. -Top-level modules MUST NOT import another top-level module's `operations` submodule. +Top-level modules MUST NOT import implementation submodules from another top-level module. -Top-level modules MAY import protocol-owned boundary values and generic projection helpers when they need to expose their owned concepts as Donna output units. +Top-level modules MAY import protocol-owned boundary values and generic projection helpers from `donna.protocol` when they need to expose their owned concepts as Donna output units. Constructing protocol-neutral output values is not protocol-specific rendering and MUST NOT be treated as a module-boundary violation. From 33bfae44100e65746d481f89b989ed7400083993 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sun, 17 May 2026 23:48:57 +0200 Subject: [PATCH 41/98] better imports --- donna/cli/__init__.py | 6 ++++ donna/cli/utils.py | 2 +- donna/core/__init__.py | 6 ++++ donna/domain/__init__.py | 19 ++++++++++++ donna/lib/__init__.py | 11 ++----- donna/machine/__init__.py | 23 ++++++++++++++ donna/primitives/__init__.py | 6 ++++ donna/primitives/artifacts/__init__.py | 3 ++ donna/primitives/directives/__init__.py | 4 +++ donna/primitives/sections/__init__.py | 7 +++++ donna/protocol/__init__.py | 9 ++++++ donna/protocol/formatters/__init__.py | 3 ++ donna/runtime/__init__.py | 4 +++ donna/skills/__init__.py | 4 ++- donna/workspaces/__init__.py | 25 +++++++++++++++ specs/architecture/modules_layout.md | 41 ++++++++++++++++--------- 16 files changed, 148 insertions(+), 25 deletions(-) diff --git a/donna/cli/__init__.py b/donna/cli/__init__.py index e69de29b..f3b64be9 100644 --- a/donna/cli/__init__.py +++ b/donna/cli/__init__.py @@ -0,0 +1,6 @@ +from donna.cli import application as application +from donna.cli import entities as entities +from donna.cli import errors as errors +from donna.cli import types as types + +__all__ = ("application", "entities", "errors", "types") diff --git a/donna/cli/utils.py b/donna/cli/utils.py index 76a6de6b..5539f452 100644 --- a/donna/cli/utils.py +++ b/donna/cli/utils.py @@ -14,7 +14,7 @@ from donna.domain.paths import PathInput, ProjectConfigPath, UntrustedPath from donna.protocol.cells import Cell from donna.protocol.errors import environment_error_node -from donna.protocol.formatters.base import Formatter +from donna.protocol.formatters import Formatter from donna.protocol.journal import JournalRecord from donna.protocol.modes import Mode, get_cell_formatter from donna.workspaces import config as workspace_config diff --git a/donna/core/__init__.py b/donna/core/__init__.py index e69de29b..0ef470c6 100644 --- a/donna/core/__init__.py +++ b/donna/core/__init__.py @@ -0,0 +1,6 @@ +from donna.core import entities as entities +from donna.core import errors as errors +from donna.core import result as result +from donna.core import utils as utils + +__all__ = ("entities", "errors", "result", "utils") diff --git a/donna/domain/__init__.py b/donna/domain/__init__.py index e69de29b..c3bfcd90 100644 --- a/donna/domain/__init__.py +++ b/donna/domain/__init__.py @@ -0,0 +1,19 @@ +from donna.domain import artifact_ids as artifact_ids +from donna.domain import constants as constants +from donna.domain import errors as errors +from donna.domain import id_paths as id_paths +from donna.domain import ids as ids +from donna.domain import internal_ids as internal_ids +from donna.domain import paths as paths +from donna.domain import python_path as python_path + +__all__ = ( + "artifact_ids", + "constants", + "errors", + "id_paths", + "ids", + "internal_ids", + "paths", + "python_path", +) diff --git a/donna/lib/__init__.py b/donna/lib/__init__.py index 9d5bc56e..89dc1bad 100644 --- a/donna/lib/__init__.py +++ b/donna/lib/__init__.py @@ -1,13 +1,8 @@ """Shared instances for standard library kind definitions.""" -from donna.primitives.artifacts.workflow import Workflow -from donna.primitives.directives.goto import GoTo -from donna.primitives.directives.task_variable import TaskVariable -from donna.primitives.sections.finish_workflow import FinishWorkflow -from donna.primitives.sections.output import Output -from donna.primitives.sections.request_action import RequestAction -from donna.primitives.sections.run_script import RunScript -from donna.primitives.sections.text import Text +from donna.primitives.artifacts import Workflow +from donna.primitives.directives import GoTo, TaskVariable +from donna.primitives.sections import FinishWorkflow, Output, RequestAction, RunScript, Text workflow = Workflow() text = Text() diff --git a/donna/machine/__init__.py b/donna/machine/__init__.py index e69de29b..7537a028 100644 --- a/donna/machine/__init__.py +++ b/donna/machine/__init__.py @@ -0,0 +1,23 @@ +from donna.machine import action_requests as action_requests +from donna.machine import artifacts as artifacts +from donna.machine import changes as changes +from donna.machine import context as context +from donna.machine import errors as errors +from donna.machine import operations as operations +from donna.machine import primitives as primitives +from donna.machine import state as state +from donna.machine import tasks as tasks +from donna.machine import templates as templates + +__all__ = ( + "action_requests", + "artifacts", + "changes", + "context", + "errors", + "operations", + "primitives", + "state", + "tasks", + "templates", +) diff --git a/donna/primitives/__init__.py b/donna/primitives/__init__.py index 543680c2..b24c5298 100644 --- a/donna/primitives/__init__.py +++ b/donna/primitives/__init__.py @@ -1 +1,7 @@ """Concrete implementations for Donna primitives.""" + +from donna.primitives import artifacts as artifacts +from donna.primitives import directives as directives +from donna.primitives import sections as sections + +__all__ = ("artifacts", "directives", "sections") diff --git a/donna/primitives/artifacts/__init__.py b/donna/primitives/artifacts/__init__.py index e69de29b..94c67867 100644 --- a/donna/primitives/artifacts/__init__.py +++ b/donna/primitives/artifacts/__init__.py @@ -0,0 +1,3 @@ +from donna.primitives.artifacts.workflow import Workflow + +__all__ = ("Workflow",) diff --git a/donna/primitives/directives/__init__.py b/donna/primitives/directives/__init__.py index e69de29b..3dc24786 100644 --- a/donna/primitives/directives/__init__.py +++ b/donna/primitives/directives/__init__.py @@ -0,0 +1,4 @@ +from donna.primitives.directives.goto import GoTo +from donna.primitives.directives.task_variable import TaskVariable + +__all__ = ("GoTo", "TaskVariable") diff --git a/donna/primitives/sections/__init__.py b/donna/primitives/sections/__init__.py index e69de29b..1586a514 100644 --- a/donna/primitives/sections/__init__.py +++ b/donna/primitives/sections/__init__.py @@ -0,0 +1,7 @@ +from donna.primitives.sections.finish_workflow import FinishWorkflow +from donna.primitives.sections.output import Output +from donna.primitives.sections.request_action import RequestAction +from donna.primitives.sections.run_script import RunScript +from donna.primitives.sections.text import Text + +__all__ = ("FinishWorkflow", "Output", "RequestAction", "RunScript", "Text") diff --git a/donna/protocol/__init__.py b/donna/protocol/__init__.py index e69de29b..2a1e3543 100644 --- a/donna/protocol/__init__.py +++ b/donna/protocol/__init__.py @@ -0,0 +1,9 @@ +from donna.protocol import cell_shortcuts as cell_shortcuts +from donna.protocol import cells as cells +from donna.protocol import errors as errors +from donna.protocol import formatters as formatters +from donna.protocol import journal as journal +from donna.protocol import modes as modes +from donna.protocol import nodes as nodes + +__all__ = ("cell_shortcuts", "cells", "errors", "formatters", "journal", "modes", "nodes") diff --git a/donna/protocol/formatters/__init__.py b/donna/protocol/formatters/__init__.py index e69de29b..aa65452f 100644 --- a/donna/protocol/formatters/__init__.py +++ b/donna/protocol/formatters/__init__.py @@ -0,0 +1,3 @@ +from donna.protocol.formatters.base import Formatter + +__all__ = ("Formatter",) diff --git a/donna/runtime/__init__.py b/donna/runtime/__init__.py index e124802f..fc151474 100644 --- a/donna/runtime/__init__.py +++ b/donna/runtime/__init__.py @@ -1 +1,5 @@ """Runtime orchestration for Donna workflow execution.""" + +from donna.runtime import sessions as sessions + +__all__ = ("sessions",) diff --git a/donna/skills/__init__.py b/donna/skills/__init__.py index 79fdb740..f3fdd4cb 100644 --- a/donna/skills/__init__.py +++ b/donna/skills/__init__.py @@ -1,4 +1,6 @@ +from donna.skills import entities as entities +from donna.skills import fixtures as fixtures from donna.skills.entities import SkillDocument from donna.skills.fixtures import load_skill_text -__all__ = ["SkillDocument", "load_skill_text"] +__all__ = ["SkillDocument", "entities", "fixtures", "load_skill_text"] diff --git a/donna/workspaces/__init__.py b/donna/workspaces/__init__.py index e69de29b..0028d528 100644 --- a/donna/workspaces/__init__.py +++ b/donna/workspaces/__init__.py @@ -0,0 +1,25 @@ +from donna.workspaces import artifacts as artifacts +from donna.workspaces import config as config +from donna.workspaces import errors as errors +from donna.workspaces import files as files +from donna.workspaces import initialization as initialization +from donna.workspaces import journal as journal +from donna.workspaces import markdown as markdown +from donna.workspaces import markdown_parser as markdown_parser +from donna.workspaces import paths as paths +from donna.workspaces import sessions as sessions +from donna.workspaces import templates as templates + +__all__ = ( + "artifacts", + "config", + "errors", + "files", + "initialization", + "journal", + "markdown", + "markdown_parser", + "paths", + "sessions", + "templates", +) diff --git a/specs/architecture/modules_layout.md b/specs/architecture/modules_layout.md index 759100a9..47c1d81e 100644 --- a/specs/architecture/modules_layout.md +++ b/specs/architecture/modules_layout.md @@ -109,20 +109,12 @@ The `errors` submodule owns module-specific exception classes and error values. Errors SHOULD express project-level failure modes that callers can handle, not low-level library details. -Error classes and error values that are part of a top-level module's public API MUST be exported from that module's package initializer. - -When a top-level module owns an `errors` submodule, its package initializer MUST export that submodule under the name `errors`. - -Top-level modules MAY import another top-level module's exported `errors` submodule from the owning module's package root. - #### `entities` The `entities` submodule owns module-specific types, semantic ids, enums, and entities that represent the module's concepts. Entities SHOULD describe domain data and boundary data, not storage implementation details unless storage metadata is itself part of the project concept. -Entity classes and other shared boundary types that are part of a top-level module's public API MUST be exported from that module's package initializer. - #### `utils` The `utils` submodule owns small helpers that do not naturally belong to entities, settings, integration boundaries, or a more specific submodule. @@ -161,21 +153,40 @@ Production modules MUST NOT import `tests.helpers`. ## Cross-module dependencies -Each top-level module MUST define its public cross-module API in its package initializer, `__init__.py`. +### Import boundaries + +Top-level modules MUST expose stable cross-module APIs through declared public import boundaries. + +A public import boundary MAY be: + +- the top-level module package root, such as `donna.domain`. +- a declared public submodule, such as `donna.domain.artifact_ids`. -The package initializer MUST explicitly import or define every type, function, constant, and object that another top-level module may import from the module. +Public import boundaries MUST be explicit enough for maintainers to distinguish stable cross-module API from implementation detail. + +A public submodule SHOULD expose cohesive concepts owned by its top-level module. + +A public submodule MUST NOT expose low-level helper functions, temporary implementation details, storage-only helpers, or compatibility shims unless those names are intentionally stable. + +Top-level module package initializers SHOULD export common public names when doing so improves ergonomics. + +Top-level module package initializers MUST NOT be required to re-export every public name from declared public submodules. When a top-level module owns an `errors` submodule, the package initializer MUST include the `errors` submodule in the public cross-module API. -The package initializer SHOULD define `__all__` to list the names that are intended as the module's public cross-module API. +The package initializer SHOULD define `__all__` to list the names that are intended as the module root public cross-module API. + +If a top-level module has no useful module-root public cross-module API, its package initializer MAY be empty or define an empty `__all__`. + +Top-level modules MUST import another top-level module only through that module's declared public import boundaries. -If a top-level module has no public cross-module API, its package initializer SHOULD be empty or define an empty `__all__`. +For example, `donna.cli` MAY import from `donna.workspaces` and from public submodules declared by `donna.workspaces`, but MUST NOT import from undeclared implementation submodules. -Top-level modules MUST import another top-level module's public API only from that module's package root. +Top-level modules MUST NOT import undeclared implementation submodules from another top-level module. -For example, `donna.cli` MAY import from `donna.workspaces`, including `from donna.workspaces import errors as workspace_errors` when `donna.workspaces` exports `errors`, but MUST NOT import from `donna.workspaces.config`, `donna.workspaces.entities`, `donna.workspaces.errors`, or any other `donna.workspaces` submodule. +Submodules inside the same top-level module MAY import each other directly. -Top-level modules MUST NOT import implementation submodules from another top-level module. +The `utils` submodule is not a public import boundary unless a top-level module explicitly declares it as one. Top-level modules MAY import protocol-owned boundary values and generic projection helpers from `donna.protocol` when they need to expose their owned concepts as Donna output units. From d974b6f130ffd420848e7f3bd7a5ce8d3193a0b8 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 00:16:43 +0200 Subject: [PATCH 42/98] core & domain tests --- donna/core/tests/__init__.py | 0 donna/core/tests/test_entities.py | 53 +++++++++++ donna/core/tests/test_errors.py | 47 ++++++++++ donna/core/tests/test_result.py | 95 ++++++++++++++++++++ donna/core/tests/test_utils.py | 11 +++ donna/domain/tests/__init__.py | 0 donna/domain/tests/test_artifact_ids.py | 114 ++++++++++++++++++++++++ donna/domain/tests/test_id_paths.py | 104 +++++++++++++++++++++ donna/domain/tests/test_ids.py | 95 ++++++++++++++++++++ donna/domain/tests/test_internal_ids.py | 73 +++++++++++++++ specs/architecture/tests.md | 28 ++++-- 11 files changed, 615 insertions(+), 5 deletions(-) create mode 100644 donna/core/tests/__init__.py create mode 100644 donna/core/tests/test_entities.py create mode 100644 donna/core/tests/test_errors.py create mode 100644 donna/core/tests/test_result.py create mode 100644 donna/core/tests/test_utils.py create mode 100644 donna/domain/tests/__init__.py create mode 100644 donna/domain/tests/test_artifact_ids.py create mode 100644 donna/domain/tests/test_id_paths.py create mode 100644 donna/domain/tests/test_ids.py create mode 100644 donna/domain/tests/test_internal_ids.py diff --git a/donna/core/tests/__init__.py b/donna/core/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/donna/core/tests/test_entities.py b/donna/core/tests/test_entities.py new file mode 100644 index 00000000..aa3ea2e6 --- /dev/null +++ b/donna/core/tests/test_entities.py @@ -0,0 +1,53 @@ +import pydantic +import pytest + +from donna.core.entities import BaseEntity + + +class _NestedEntity(BaseEntity): + values: list[int] + + +class _SampleEntity(BaseEntity): + name: str + nested: _NestedEntity + + +class TestBaseEntity: + def test_model_config__strips_strings_and_forbids_extra_fields(self) -> None: + entity = _SampleEntity(name=" value ", nested=_NestedEntity(values=[])) + + assert entity.name == "value" + + with pytest.raises(pydantic.ValidationError): + _SampleEntity(name="value", nested=_NestedEntity(values=[]), extra=True) + + def test_model_config__is_frozen(self) -> None: + entity = _SampleEntity(name="value", nested=_NestedEntity(values=[])) + + with pytest.raises(pydantic.ValidationError): + entity.name = "changed" + + def test_replace__returns_deep_copy_with_changes(self) -> None: + entity = _SampleEntity(name="before", nested=_NestedEntity(values=[1])) + + replaced = entity.replace(name="after") + replaced.nested.values.append(2) + + assert entity.name == "before" + assert entity.nested.values == [1] + assert replaced.name == "after" + assert replaced.nested.values == [1, 2] + + def test_to_json__serializes_entity(self) -> None: + entity = _SampleEntity(name="value", nested=_NestedEntity(values=[1, 2])) + + assert ( + entity.to_json() + == '{\n "name": "value",\n "nested": {\n "values": [\n 1,\n 2\n ]\n }\n}' + ) + + def test_from_json__deserializes_entity(self) -> None: + entity = _SampleEntity.from_json('{"name": "value", "nested": {"values": [1]}}') + + assert entity == _SampleEntity(name="value", nested=_NestedEntity(values=[1])) diff --git a/donna/core/tests/test_errors.py b/donna/core/tests/test_errors.py new file mode 100644 index 00000000..32556e75 --- /dev/null +++ b/donna/core/tests/test_errors.py @@ -0,0 +1,47 @@ +from donna.core import errors + + +class _FormattedInternalError(errors.InternalError): + message = "Broken {thing}" + + +class _SampleEnvironmentError(errors.EnvironmentError): + cell_kind: str = "sample_error" + code: str = "sample.error" + message: str = "Sample failed." + + def content_intro(self) -> str: + return "Sample" + + +class TestInternalError: + def test_error_message__formats_keyword_arguments(self) -> None: + error = _FormattedInternalError(thing="state") + + assert error.arguments == {"thing": "state"} + assert error.error_message() == "Broken state" + assert str(error) == "_FormattedInternalError: Broken state" + + +class TestEnvironmentError: + def test_content_intro__uses_subclass_behavior(self) -> None: + error = _SampleEnvironmentError() + + assert error.content_intro() == "Sample" + + def test_ways_to_fix__uses_independent_default_list(self) -> None: + first = _SampleEnvironmentError() + second = _SampleEnvironmentError() + + first.ways_to_fix.append("Fix it.") + + assert first.ways_to_fix == ["Fix it."] + assert second.ways_to_fix == [] + + +class TestEnvironmentErrorsProxy: + def test_init__stores_errors_for_technical_unwrap_bridge(self) -> None: + error = _SampleEnvironmentError() + proxy = errors.EnvironmentErrorsProxy([error]) + + assert proxy.arguments == {"errors": [error]} diff --git a/donna/core/tests/test_result.py b/donna/core/tests/test_result.py new file mode 100644 index 00000000..940a61ff --- /dev/null +++ b/donna/core/tests/test_result.py @@ -0,0 +1,95 @@ +import pytest + +from donna.core.result import Err, Ok, Result, UnwrapErrError, UnwrapError, unwrap_to_error + + +class TestResult: + def test_ok__exposes_success_value(self) -> None: + result: Result[int, str] = Ok(2) + + assert result.is_ok() + assert not result.is_err() + assert result.ok() == 2 + assert result.err() is None + assert result.unwrap() == 2 + assert result.unwrap_or(3) == 2 + + def test_ok__maps_success_value(self) -> None: + result: Result[int, str] = Ok(2) + + mapped = result.map(lambda value: value + 1) + mapped_error = result.map_err(lambda error: f"{error}!") + + assert mapped.unwrap() == 3 + assert mapped_error.unwrap() == 2 + + def test_ok__unwrap_err_raises_internal_error(self) -> None: + result: Result[int, str] = Ok(2) + + with pytest.raises(UnwrapErrError) as exc_info: + result.unwrap_err() + + assert exc_info.value.arguments == {"value": 2} + + def test_err__exposes_error_value(self) -> None: + result: Result[int, str] = Err("failure") + + assert not result.is_ok() + assert result.is_err() + assert result.ok() is None + assert result.err() == "failure" + assert result.unwrap_or(3) == 3 + + def test_err__maps_error_value(self) -> None: + result: Result[int, str] = Err("failure") + + mapped = result.map(lambda value: value + 1) + mapped_error = result.map_err(lambda error: f"{error}!") + + assert mapped.err() == "failure" + assert mapped_error.err() == "failure!" + + def test_err__unwrap_raises_internal_error_with_error_value(self) -> None: + result: Result[int, str] = Err("failure") + + with pytest.raises(UnwrapError) as exc_info: + result.unwrap() + + assert exc_info.value.arguments == {"error": "failure"} + + +class TestOk: + def test_returns_success_result(self) -> None: + assert Ok("value").unwrap() == "value" + + +class TestErr: + def test_returns_error_result(self) -> None: + assert Err("error").unwrap_err() == "error" + + +class TestUnwrapToError: + def test_converts_unwrap_error_to_error_result(self) -> None: + @unwrap_to_error + def composed() -> Result[str, list[str]]: + return Err(["failure"]).unwrap() + + result = composed() + + assert result.is_err() + assert result.unwrap_err() == ["failure"] + + def test_preserves_success_result(self) -> None: + @unwrap_to_error + def composed() -> Result[str, list[str]]: + return Ok("value") + + assert composed().unwrap() == "value" + + def test_does_not_hide_arbitrary_exceptions(self) -> None: + @unwrap_to_error + def composed() -> Result[str, list[str]]: + raise RuntimeError("boom") + + with pytest.raises(RuntimeError): + composed() diff --git a/donna/core/tests/test_utils.py b/donna/core/tests/test_utils.py new file mode 100644 index 00000000..4b1c04a1 --- /dev/null +++ b/donna/core/tests/test_utils.py @@ -0,0 +1,11 @@ +import datetime + +from donna.core import utils + + +class TestNow: + def test_returns_timezone_aware_utc_datetime(self) -> None: + value = utils.now() + + assert isinstance(value, datetime.datetime) + assert value.tzinfo == datetime.UTC diff --git a/donna/domain/tests/__init__.py b/donna/domain/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/donna/domain/tests/test_artifact_ids.py b/donna/domain/tests/test_artifact_ids.py new file mode 100644 index 00000000..ad339e96 --- /dev/null +++ b/donna/domain/tests/test_artifact_ids.py @@ -0,0 +1,114 @@ +import pytest + +from donna.domain import errors +from donna.domain.artifact_ids import ( + ArtifactId, + ArtifactSectionId, + artifact_path_parts, + artifact_section_id, + split_artifact_section_id, + validate_artifact_id, + validate_artifact_section_id, +) +from donna.domain.ids import SectionId + + +class TestValidateArtifactId: + @pytest.mark.parametrize( + "value", + [ + "@/README.md", + "@/workflows/polish.donna.md", + "@/.session/donna/plans/feature.donna.md", + ], + ) + def test_valid_canonical_artifact_id(self, value: str) -> None: + assert validate_artifact_id(value) + + @pytest.mark.parametrize( + "value", + [ + "@", + "@/", + "@/workflows/../README.md", + "@/workflows//polish.donna.md", + "@/workflows/", + "@/---/polish.donna.md", + "@/README", + "/home/user/project/workflows/polish.donna.md", + None, + ], + ) + def test_invalid_canonical_artifact_id(self, value: object) -> None: + assert not validate_artifact_id(value) + + +class TestValidateArtifactSectionId: + def test_valid_section_id(self) -> None: + assert validate_artifact_section_id("@/workflows/polish.donna.md:section-1") + + @pytest.mark.parametrize( + "value", + [ + "@/workflows/polish.donna.md", + "@/workflows/polish.donna.md:", + "@/workflows/polish.donna.md:---", + "@/workflows//polish.donna.md:section", + None, + ], + ) + def test_invalid_section_id(self, value: object) -> None: + assert not validate_artifact_section_id(value) + + +class TestArtifactPathParts: + def test_valid_artifact_id(self) -> None: + assert artifact_path_parts(ArtifactId("@/workflows/polish.donna.md")) == ("workflows", "polish.donna.md") + + def test_invalid_artifact_id(self) -> None: + with pytest.raises(ValueError): + artifact_path_parts(ArtifactId("workflows/polish.donna.md")) + + +class TestArtifactSectionId: + def test_builds_full_section_id(self) -> None: + full_id = artifact_section_id(ArtifactId("@/workflows/polish.donna.md"), SectionId("start")) + + assert full_id == ArtifactSectionId("@/workflows/polish.donna.md:start") + + def test_builds_from_string_section_id(self) -> None: + full_id = artifact_section_id(ArtifactId("@/workflows/polish.donna.md"), "finish") + + assert full_id == ArtifactSectionId("@/workflows/polish.donna.md:finish") + + def test_invalid_artifact_id(self) -> None: + with pytest.raises(ValueError): + artifact_section_id(ArtifactId("workflows/polish.donna.md"), "start") + + def test_invalid_local_section_id(self) -> None: + with pytest.raises(errors.InvalidIdentifier): + artifact_section_id(ArtifactId("@/workflows/polish.donna.md"), "///") + + +class TestSplitArtifactSectionId: + def test_valid_section_id(self) -> None: + parts = split_artifact_section_id("@/workflows/polish.donna.md:section-1") + + assert parts is not None + assert parts.full_id == ArtifactSectionId("@/workflows/polish.donna.md:section-1") + assert parts.artifact_id == ArtifactId("@/workflows/polish.donna.md") + assert parts.section_id == SectionId("section-1") + + @pytest.mark.parametrize( + "value", + [ + "", + "@/workflows/polish.donna.md", + "@/workflows/polish.donna.md:", + "@/workflows/polish.donna.md:---", + "@/workflows//polish.donna.md:section", + 1, + ], + ) + def test_invalid_section_id(self, value: object) -> None: + assert split_artifact_section_id(value) is None diff --git a/donna/domain/tests/test_id_paths.py b/donna/domain/tests/test_id_paths.py new file mode 100644 index 00000000..92f5431f --- /dev/null +++ b/donna/domain/tests/test_id_paths.py @@ -0,0 +1,104 @@ +import copy + +import pydantic +import pytest + +from donna.core.entities import BaseEntity +from donna.domain import errors +from donna.domain.id_paths import IdPath, NormalizedRawIdPath +from donna.domain.python_path import PythonPath + + +class _SlashPath(IdPath): + __slots__ = () + prefix = "$/" + delimiter = "/" + min_parts = 2 + + +class _PathEntity(BaseEntity): + path: PythonPath + + +class TestIdPath: + def test_normalize_raw_value__removes_prefix_and_validates_parts(self) -> None: + assert _SlashPath.normalize_raw_value("$/alpha/beta") == NormalizedRawIdPath("alpha/beta") + assert _SlashPath.normalize_raw_value("alpha/beta") == NormalizedRawIdPath("alpha/beta") + assert _SlashPath.normalize_raw_value("$/alpha") is None + assert _SlashPath.normalize_raw_value("$/alpha//beta") is None + assert _SlashPath.normalize_raw_value("$/alpha/not-valid") is None + + def test_parse__returns_normalized_path(self) -> None: + result = _SlashPath.parse("$/alpha/beta") + + assert result.is_ok() + assert result.unwrap() == _SlashPath(NormalizedRawIdPath("alpha/beta")) + + def test_parse__returns_environment_error_for_invalid_format(self) -> None: + result = _SlashPath.parse("$/alpha") + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, errors.InvalidIdFormat) + assert error.code == "donna.domain.invalid_id_format" + assert error.id_type == "_SlashPath" + assert error.value == "$/alpha" + + def test_init__raises_internal_error_for_invalid_normalized_value(self) -> None: + with pytest.raises(errors.InvalidIdPath): + _SlashPath(NormalizedRawIdPath("alpha")) + + def test_string_representations__use_prefix_and_raw_value(self) -> None: + path = _SlashPath(NormalizedRawIdPath("alpha/beta")) + + assert path.raw_value == "alpha/beta" + assert str(path) == "$/alpha/beta" + assert repr(path) == "_SlashPath('alpha/beta')" + + def test_equality_hashing_and_ordering__are_type_specific_and_part_based(self) -> None: + first = _SlashPath(NormalizedRawIdPath("alpha/beta")) + second = _SlashPath(NormalizedRawIdPath("alpha/gamma")) + + assert first == _SlashPath(NormalizedRawIdPath("alpha/beta")) + assert first != PythonPath("alpha.beta") + assert sorted([second, first]) == [first, second] + assert {first, _SlashPath(NormalizedRawIdPath("alpha/beta"))} == {first} + + def test_copy__preserves_value_semantics(self) -> None: + path = _SlashPath(NormalizedRawIdPath("alpha/beta")) + + assert copy.copy(path) == path + assert copy.deepcopy(path) == path + + def test_setattr__rejects_mutation(self) -> None: + path = _SlashPath(NormalizedRawIdPath("alpha/beta")) + + with pytest.raises(AttributeError): + path.parts = ("changed",) + + +class TestPythonPath: + def test_parse__accepts_dotted_python_path(self) -> None: + result = PythonPath.parse("donna.domain.ids") + + assert result.is_ok() + assert result.unwrap().parts == ("donna", "domain", "ids") + + def test_parse__rejects_empty_and_malformed_parts(self) -> None: + assert PythonPath.parse("").is_err() + assert PythonPath.parse("donna..ids").is_err() + assert PythonPath.parse("donna.domain.1ids").is_err() + + def test_pydantic_validation__accepts_and_serializes_python_path(self) -> None: + entity = _PathEntity(path="donna.domain.ids") + + assert entity.path == PythonPath("donna.domain.ids") + assert entity.model_dump() == {"path": PythonPath("donna.domain.ids")} + assert entity.model_dump_json() == '{"path":"donna.domain.ids"}' + + def test_pydantic_validation__rejects_invalid_python_value(self) -> None: + with pytest.raises(pydantic.ValidationError): + _PathEntity(path="donna..ids") + + with pytest.raises(pydantic.ValidationError): + _PathEntity(path=123) diff --git a/donna/domain/tests/test_ids.py b/donna/domain/tests/test_ids.py new file mode 100644 index 00000000..adc4f933 --- /dev/null +++ b/donna/domain/tests/test_ids.py @@ -0,0 +1,95 @@ +import pydantic +import pytest + +from donna.core.entities import BaseEntity +from donna.domain import errors +from donna.domain.ids import Identifier, SectionId + + +class _IdentifierEntity(BaseEntity): + identifier: Identifier + + +class _SectionEntity(BaseEntity): + section_id: SectionId + + +class TestIdentifier: + def test_validate__accepts_python_identifier(self) -> None: + assert Identifier.validate("valid_name") + + @pytest.mark.parametrize("value", ["", "1invalid", "not-valid", None]) + def test_validate__rejects_non_identifier(self, value: object) -> None: + assert not Identifier.validate(value) + + def test_init__raises_internal_error_for_invalid_value(self) -> None: + with pytest.raises(errors.InvalidIdentifier): + Identifier("not-valid") + + def test_parse__returns_identifier(self) -> None: + result = Identifier.parse("valid_name") + + assert result.is_ok() + assert result.unwrap() == Identifier("valid_name") + + def test_parse__returns_environment_error_for_invalid_format(self) -> None: + result = Identifier.parse("not-valid") + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, errors.InvalidIdFormat) + assert error.code == "donna.domain.invalid_id_format" + assert error.id_type == "Identifier" + assert error.value == "not-valid" + + def test_pydantic_validation__accepts_identifier_value(self) -> None: + entity = _IdentifierEntity(identifier="valid_name") + + assert entity.identifier == Identifier("valid_name") + assert entity.model_dump_json() == '{"identifier":"valid_name"}' + + def test_pydantic_validation__rejects_invalid_identifier_value(self) -> None: + with pytest.raises(pydantic.ValidationError): + _IdentifierEntity(identifier="not-valid") + + with pytest.raises(pydantic.ValidationError): + _IdentifierEntity(identifier=123) + + +class TestSectionId: + @pytest.mark.parametrize("value", ["section", "section-1", "section.name", "section_name"]) + def test_validate__accepts_artifact_slug_part(self, value: str) -> None: + assert SectionId.validate(value) + + @pytest.mark.parametrize("value", ["", "---", "...", "section/id", "section id", None]) + def test_validate__rejects_invalid_artifact_slug_part(self, value: object) -> None: + assert not SectionId.validate(value) + + def test_parse__returns_section_id(self) -> None: + result = SectionId.parse("section-1") + + assert result.is_ok() + assert result.unwrap() == SectionId("section-1") + + def test_parse__returns_environment_error_for_invalid_format(self) -> None: + result = SectionId.parse("---") + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, errors.InvalidIdFormat) + assert error.code == "donna.domain.invalid_id_format" + assert error.id_type == "SectionId" + assert error.value == "---" + + def test_pydantic_validation__accepts_section_id_value(self) -> None: + entity = _SectionEntity(section_id="section-1") + + assert entity.section_id == SectionId("section-1") + assert entity.model_dump_json() == '{"section_id":"section-1"}' + + def test_pydantic_validation__rejects_invalid_section_id_value(self) -> None: + with pytest.raises(pydantic.ValidationError): + _SectionEntity(section_id="---") + + with pytest.raises(pydantic.ValidationError): + _SectionEntity(section_id=123) diff --git a/donna/domain/tests/test_internal_ids.py b/donna/domain/tests/test_internal_ids.py new file mode 100644 index 00000000..8ab75203 --- /dev/null +++ b/donna/domain/tests/test_internal_ids.py @@ -0,0 +1,73 @@ +import pydantic +import pytest + +from donna.core.entities import BaseEntity +from donna.domain import errors +from donna.domain.internal_ids import ActionRequestId, InternalId, TaskId, WorkUnitId + + +class _InternalIdEntity(BaseEntity): + internal_id: InternalId + + +class TestInternalId: + def test_build__creates_crc_protected_identifier(self) -> None: + identifier = InternalId.build("WU", 0) + + assert identifier == InternalId("WU-0-a") + assert identifier.short == "0" + assert InternalId.validate(identifier) + + @pytest.mark.parametrize( + "value", + [ + "WU-0-b", + "WU-zero-a", + "WU", + "", + None, + ], + ) + def test_validate__rejects_invalid_identifier(self, value: object) -> None: + assert not InternalId.validate(value) + + def test_init__raises_internal_error_for_invalid_value(self) -> None: + with pytest.raises(errors.InvalidInternalId): + InternalId("WU-0-b") + + def test_pydantic_validation__accepts_internal_id_value(self) -> None: + entity = _InternalIdEntity(internal_id="WU-0-a") + + assert entity.internal_id == InternalId("WU-0-a") + assert entity.model_dump_json() == '{"internal_id":"WU-0-a"}' + + def test_pydantic_validation__rejects_invalid_internal_id_value(self) -> None: + with pytest.raises(pydantic.ValidationError): + _InternalIdEntity(internal_id="WU-0-b") + + with pytest.raises(pydantic.ValidationError): + _InternalIdEntity(internal_id=123) + + +class TestWorkUnitId: + def test_inherits_internal_id_behavior(self) -> None: + identifier = WorkUnitId.build("WU", 1) + + assert identifier == WorkUnitId("WU-1-b") + assert WorkUnitId.validate(identifier) + + +class TestActionRequestId: + def test_inherits_internal_id_behavior(self) -> None: + identifier = ActionRequestId.build("AR", 1) + + assert identifier == ActionRequestId("AR-1-b") + assert ActionRequestId.validate(identifier) + + +class TestTaskId: + def test_inherits_internal_id_behavior(self) -> None: + identifier = TaskId.build("T", 1) + + assert identifier == TaskId("T-1-b") + assert TaskId.validate(identifier) diff --git a/specs/architecture/tests.md b/specs/architecture/tests.md index 3e16c72f..a6e1f245 100644 --- a/specs/architecture/tests.md +++ b/specs/architecture/tests.md @@ -234,24 +234,42 @@ Entity tests MAY assert `pydantic.ValidationError` for invalid low-level model c Error tests SHOULD verify behavior customized by concrete error classes. -Error tests SHOULD cover: +### Testing `errors.py` modules -- custom error codes. -- custom message formatting. -- custom structured details. -- custom cell metadata. +Error modules that contain only declarative error subclasses SHOULD NOT have unit tests solely for file-to-module layout symmetry. + +Error class unit tests SHOULD be added only when the error class owns behavior beyond inherited construction and static class attributes. + +Error class unit tests MAY cover: + +- custom constructor logic. +- custom `content_intro()` behavior. +- custom validation or normalization. +- non-trivial structured metadata derivation. - behavior added by an intermediate error class. +Leaf error tests MUST NOT assert only that class-level `code`, `message`, `ways_to_fix`, `cell_kind`, or constructor fields are present unchanged. + +Exact production error message text MUST NOT be asserted in ordinary unit tests unless a behavior specification declares the text as a stable external contract. + Error tests SHOULD NOT test unchanged inheritance from project or module root error classes. Error tests SHOULD NOT test behavior inherited unchanged from the shared base error class. +A module-level `tests/test_errors.py` file is optional. + +A module-level `tests/test_errors.py` file SHOULD be omitted when all meaningful error behavior is already covered by tests for the functions or entities that produce those errors. + +### Testing error-producing behavior + Tests for exception boundaries SHOULD verify that expected low-level failures are converted into Donna environment errors. Tests for exception boundaries SHOULD verify that `pydantic.ValidationError` from external input is converted into Donna environment errors. Tests for `Result`-returning functions SHOULD verify error values through `Result` state rather than by expecting environment errors to be raised. +Tests that verify produced environment errors SHOULD assert the expected error type, stable error code, and relevant structured fields through the behavior boundary that returns the error. + Internal error tests MAY assert raised `InternalError` subclasses when the tested behavior is an internal invariant. CLI tests SHOULD verify that rendered environment errors use the expected Donna cell shape when command execution has entered Donna's protocol layer. From f90c4ca662e19fa57e762b170699cb45b910c536 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 10:07:41 +0200 Subject: [PATCH 43/98] tests --- .github/workflows/code-checks.yaml | 5 +- AGENTS.md | 3 +- bin/dev-tests.sh | 7 +++ donna/core/tests/test_entities.py | 4 +- donna/domain/tests/test_artifact_ids.py | 8 ++- donna/domain/tests/test_id_paths.py | 13 +++-- donna/domain/tests/test_ids.py | 18 +++--- donna/domain/tests/test_internal_ids.py | 10 ++-- donna/machine/context.py | 27 ++++++--- donna/workspaces/templates.py | 2 +- poetry.lock | 74 ++++++++++++++++++++++++- pyproject.toml | 2 + workflows/polish.donna.md | 34 +++++++++++- 13 files changed, 168 insertions(+), 39 deletions(-) create mode 100755 bin/dev-tests.sh diff --git a/.github/workflows/code-checks.yaml b/.github/workflows/code-checks.yaml index f5b8ee58..fe646021 100644 --- a/.github/workflows/code-checks.yaml +++ b/.github/workflows/code-checks.yaml @@ -24,7 +24,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ inputs.branch_ref }} + ref: ${{ inputs.branch_ref || github.ref }} - name: Build containers run: ./bin/dev-build-containers.sh @@ -35,5 +35,8 @@ jobs: - name: Check types run: ./bin/dev-check-semantics.sh + - name: Run tests + run: ./bin/dev-tests.sh + - name: Check runtime run: ./bin/dev-check-runtime.sh diff --git a/AGENTS.md b/AGENTS.md index 6195e41e..1c255996 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,8 @@ Agents MUST NOT perform development-related operations directly on the host mach Allowed development commands: -- `./bin/dev.sh` — run development utilities inside the container, for example `./bin/dev.sh uv run -- pytest`. +- `./bin/dev-tests.sh` — run all Python tests inside the container. +- `./bin/dev.sh` — run development utilities inside the container, for example `./bin/dev.sh poetry run pytest`. - `./bin/dev-build-containers.sh` — build base Docker images for development; use only after approved Docker or dependency changes. Searching, reading, and editing repository files MAY be done on the host machine. diff --git a/bin/dev-tests.sh b/bin/dev-tests.sh new file mode 100755 index 00000000..b377b1f6 --- /dev/null +++ b/bin/dev-tests.sh @@ -0,0 +1,7 @@ +#!/usr/bin/bash + +set -e + +echo "run tests" + +./bin/dev.sh poetry run pytest donna -o cache_dir=/tmp/donna-pytest-cache diff --git a/donna/core/tests/test_entities.py b/donna/core/tests/test_entities.py index aa3ea2e6..86406923 100644 --- a/donna/core/tests/test_entities.py +++ b/donna/core/tests/test_entities.py @@ -20,13 +20,13 @@ def test_model_config__strips_strings_and_forbids_extra_fields(self) -> None: assert entity.name == "value" with pytest.raises(pydantic.ValidationError): - _SampleEntity(name="value", nested=_NestedEntity(values=[]), extra=True) + _SampleEntity.model_validate({"name": "value", "nested": {"values": []}, "extra": True}) def test_model_config__is_frozen(self) -> None: entity = _SampleEntity(name="value", nested=_NestedEntity(values=[])) with pytest.raises(pydantic.ValidationError): - entity.name = "changed" + setattr(entity, "name", "changed") def test_replace__returns_deep_copy_with_changes(self) -> None: entity = _SampleEntity(name="before", nested=_NestedEntity(values=[1])) diff --git a/donna/domain/tests/test_artifact_ids.py b/donna/domain/tests/test_artifact_ids.py index ad339e96..791c0dc8 100644 --- a/donna/domain/tests/test_artifact_ids.py +++ b/donna/domain/tests/test_artifact_ids.py @@ -1,3 +1,5 @@ +from typing import Any, cast + import pytest from donna.domain import errors @@ -40,7 +42,7 @@ def test_valid_canonical_artifact_id(self, value: str) -> None: ], ) def test_invalid_canonical_artifact_id(self, value: object) -> None: - assert not validate_artifact_id(value) + assert not validate_artifact_id(cast(Any, value)) class TestValidateArtifactSectionId: @@ -58,7 +60,7 @@ def test_valid_section_id(self) -> None: ], ) def test_invalid_section_id(self, value: object) -> None: - assert not validate_artifact_section_id(value) + assert not validate_artifact_section_id(cast(Any, value)) class TestArtifactPathParts: @@ -111,4 +113,4 @@ def test_valid_section_id(self) -> None: ], ) def test_invalid_section_id(self, value: object) -> None: - assert split_artifact_section_id(value) is None + assert split_artifact_section_id(cast(Any, value)) is None diff --git a/donna/domain/tests/test_id_paths.py b/donna/domain/tests/test_id_paths.py index 92f5431f..4409f32c 100644 --- a/donna/domain/tests/test_id_paths.py +++ b/donna/domain/tests/test_id_paths.py @@ -1,4 +1,5 @@ import copy +from typing import Any, cast import pydantic import pytest @@ -60,7 +61,7 @@ def test_equality_hashing_and_ordering__are_type_specific_and_part_based(self) - second = _SlashPath(NormalizedRawIdPath("alpha/gamma")) assert first == _SlashPath(NormalizedRawIdPath("alpha/beta")) - assert first != PythonPath("alpha.beta") + assert first != PythonPath(NormalizedRawIdPath("alpha.beta")) assert sorted([second, first]) == [first, second] assert {first, _SlashPath(NormalizedRawIdPath("alpha/beta"))} == {first} @@ -90,15 +91,15 @@ def test_parse__rejects_empty_and_malformed_parts(self) -> None: assert PythonPath.parse("donna.domain.1ids").is_err() def test_pydantic_validation__accepts_and_serializes_python_path(self) -> None: - entity = _PathEntity(path="donna.domain.ids") + entity = _PathEntity.model_validate({"path": "donna.domain.ids"}) - assert entity.path == PythonPath("donna.domain.ids") - assert entity.model_dump() == {"path": PythonPath("donna.domain.ids")} + assert entity.path == PythonPath(NormalizedRawIdPath("donna.domain.ids")) + assert entity.model_dump() == {"path": PythonPath(NormalizedRawIdPath("donna.domain.ids"))} assert entity.model_dump_json() == '{"path":"donna.domain.ids"}' def test_pydantic_validation__rejects_invalid_python_value(self) -> None: with pytest.raises(pydantic.ValidationError): - _PathEntity(path="donna..ids") + _PathEntity.model_validate({"path": "donna..ids"}) with pytest.raises(pydantic.ValidationError): - _PathEntity(path=123) + _PathEntity.model_validate({"path": cast(Any, 123)}) diff --git a/donna/domain/tests/test_ids.py b/donna/domain/tests/test_ids.py index adc4f933..118c3bbb 100644 --- a/donna/domain/tests/test_ids.py +++ b/donna/domain/tests/test_ids.py @@ -1,3 +1,5 @@ +from typing import Any, cast + import pydantic import pytest @@ -20,7 +22,7 @@ def test_validate__accepts_python_identifier(self) -> None: @pytest.mark.parametrize("value", ["", "1invalid", "not-valid", None]) def test_validate__rejects_non_identifier(self, value: object) -> None: - assert not Identifier.validate(value) + assert not Identifier.validate(cast(Any, value)) def test_init__raises_internal_error_for_invalid_value(self) -> None: with pytest.raises(errors.InvalidIdentifier): @@ -43,17 +45,17 @@ def test_parse__returns_environment_error_for_invalid_format(self) -> None: assert error.value == "not-valid" def test_pydantic_validation__accepts_identifier_value(self) -> None: - entity = _IdentifierEntity(identifier="valid_name") + entity = _IdentifierEntity.model_validate({"identifier": "valid_name"}) assert entity.identifier == Identifier("valid_name") assert entity.model_dump_json() == '{"identifier":"valid_name"}' def test_pydantic_validation__rejects_invalid_identifier_value(self) -> None: with pytest.raises(pydantic.ValidationError): - _IdentifierEntity(identifier="not-valid") + _IdentifierEntity.model_validate({"identifier": "not-valid"}) with pytest.raises(pydantic.ValidationError): - _IdentifierEntity(identifier=123) + _IdentifierEntity.model_validate({"identifier": cast(Any, 123)}) class TestSectionId: @@ -63,7 +65,7 @@ def test_validate__accepts_artifact_slug_part(self, value: str) -> None: @pytest.mark.parametrize("value", ["", "---", "...", "section/id", "section id", None]) def test_validate__rejects_invalid_artifact_slug_part(self, value: object) -> None: - assert not SectionId.validate(value) + assert not SectionId.validate(cast(Any, value)) def test_parse__returns_section_id(self) -> None: result = SectionId.parse("section-1") @@ -82,14 +84,14 @@ def test_parse__returns_environment_error_for_invalid_format(self) -> None: assert error.value == "---" def test_pydantic_validation__accepts_section_id_value(self) -> None: - entity = _SectionEntity(section_id="section-1") + entity = _SectionEntity.model_validate({"section_id": "section-1"}) assert entity.section_id == SectionId("section-1") assert entity.model_dump_json() == '{"section_id":"section-1"}' def test_pydantic_validation__rejects_invalid_section_id_value(self) -> None: with pytest.raises(pydantic.ValidationError): - _SectionEntity(section_id="---") + _SectionEntity.model_validate({"section_id": "---"}) with pytest.raises(pydantic.ValidationError): - _SectionEntity(section_id=123) + _SectionEntity.model_validate({"section_id": cast(Any, 123)}) diff --git a/donna/domain/tests/test_internal_ids.py b/donna/domain/tests/test_internal_ids.py index 8ab75203..33fbbf1a 100644 --- a/donna/domain/tests/test_internal_ids.py +++ b/donna/domain/tests/test_internal_ids.py @@ -1,3 +1,5 @@ +from typing import Any, cast + import pydantic import pytest @@ -29,24 +31,24 @@ def test_build__creates_crc_protected_identifier(self) -> None: ], ) def test_validate__rejects_invalid_identifier(self, value: object) -> None: - assert not InternalId.validate(value) + assert not InternalId.validate(cast(Any, value)) def test_init__raises_internal_error_for_invalid_value(self) -> None: with pytest.raises(errors.InvalidInternalId): InternalId("WU-0-b") def test_pydantic_validation__accepts_internal_id_value(self) -> None: - entity = _InternalIdEntity(internal_id="WU-0-a") + entity = _InternalIdEntity.model_validate({"internal_id": "WU-0-a"}) assert entity.internal_id == InternalId("WU-0-a") assert entity.model_dump_json() == '{"internal_id":"WU-0-a"}' def test_pydantic_validation__rejects_invalid_internal_id_value(self) -> None: with pytest.raises(pydantic.ValidationError): - _InternalIdEntity(internal_id="WU-0-b") + _InternalIdEntity.model_validate({"internal_id": "WU-0-b"}) with pytest.raises(pydantic.ValidationError): - _InternalIdEntity(internal_id=123) + _InternalIdEntity.model_validate({"internal_id": cast(Any, 123)}) class TestWorkUnitId: diff --git a/donna/machine/context.py b/donna/machine/context.py index 1b355400..77c16765 100644 --- a/donna/machine/context.py +++ b/donna/machine/context.py @@ -40,39 +40,48 @@ def scope(self, value: TScopedValue | None) -> Iterator[None]: class MachineArtifacts(Protocol): - def load_for_view(self, artifact_id: ArtifactId) -> Result["Artifact", ErrorsList]: ... + def load_for_view(self, artifact_id: ArtifactId) -> Result["Artifact", ErrorsList]: + pass def load_for_execution( self, artifact_id: ArtifactId, task: "Task", work_unit: "WorkUnit", - ) -> Result["Artifact", ErrorsList]: ... + ) -> Result["Artifact", ErrorsList]: + pass class MachinePrimitives(Protocol): - def resolve(self, primitive_id: PythonPath) -> Result["Primitive", ErrorsList]: ... + def resolve(self, primitive_id: PythonPath) -> Result["Primitive", ErrorsList]: + pass class MachineJournal(Protocol): - def add(self, message: str, actor_id: str | None = None) -> Result[Any, ErrorsList]: ... + def add(self, message: str, actor_id: str | None = None) -> Result[Any, ErrorsList]: + pass class MachineContext(Protocol): @property - def artifacts(self) -> MachineArtifacts: ... + def artifacts(self) -> MachineArtifacts: + pass @property - def primitives(self) -> MachinePrimitives: ... + def primitives(self) -> MachinePrimitives: + pass @property - def journal(self) -> MachineJournal: ... + def journal(self) -> MachineJournal: + pass @property - def current_work_unit_id(self) -> ValueScope[WorkUnitId]: ... + def current_work_unit_id(self) -> ValueScope[WorkUnitId]: + pass @property - def current_operation_id(self) -> ValueScope[ArtifactSectionId]: ... + def current_operation_id(self) -> ValueScope[ArtifactSectionId]: + pass _context_var: contextvars.ContextVar[MachineContext | None] = contextvars.ContextVar( diff --git a/donna/workspaces/templates.py b/donna/workspaces/templates.py index d02c0d31..a774e9b8 100644 --- a/donna/workspaces/templates.py +++ b/donna/workspaces/templates.py @@ -10,7 +10,7 @@ from donna.core.errors import EnvironmentErrorsProxy, ErrorsList from donna.core.result import Err, Ok, Result from donna.domain.artifact_ids import ArtifactId -from donna.machine.templates import Directive, RenderMode +from donna.machine.templates import Directive from donna.workspaces import errors as world_errors if TYPE_CHECKING: diff --git a/poetry.lock b/poetry.lock index a234b8c3..36208626 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -187,11 +187,11 @@ description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main", "dev"] -markers = "platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "eradicate" @@ -414,6 +414,18 @@ gitdb = ">=4.0.1,<5" doc = ["sphinx (>=7.4.7,<8)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy (==1.18.2) ; python_version >= \"3.9\"", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + [[package]] name = "isort" version = "7.0.0" @@ -851,6 +863,22 @@ docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx- test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] type = ["mypy (>=1.18.2)"] +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -1140,6 +1168,46 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pytest" +version = "9.0.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, + {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, + {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "python-dotenv" version = "1.2.1" @@ -1539,4 +1607,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.12,<4.0" -content-hash = "d46dfa8fef715bfadde72ce163380fb5c5e500c8d715c057fd65c8648fa5c12c" +content-hash = "32d87af3c1081121c7550768997104a0c3cca15e3cf46f20d6eb477f5e1a0fad" diff --git a/pyproject.toml b/pyproject.toml index 4819df89..bc73b9f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,8 @@ flake8-eradicate = "1.5.*" mypy = "1.19.*" types-PyYAML = "6.0.*" +pytest = "9.0.*" +pytest-mock = "3.15.*" [tool.isort] profile = "black" diff --git a/workflows/polish.donna.md b/workflows/polish.donna.md index 5ef3b17d..c58b4efa 100644 --- a/workflows/polish.donna.md +++ b/workflows/polish.donna.md @@ -203,7 +203,7 @@ Instructions on fixing special cases: id = "run_mypy_script" kind = "donna.lib.run_script" save_stdout_to = "mypy_output" -goto_on_success = "finish" +goto_on_success = "run_tests_script" goto_on_failure = "fix_mypy" ``` @@ -247,6 +247,38 @@ Changes you are not allowed to make: - Adding `type: ignore[import-untyped]`. If you need to use it, ask the developer to install the missing types first or to fix the issue manually. - Adding or removing attributes to classes. If you need to do it, ask the developer to fix the problem manually. +## Run Tests + +```toml donna +id = "run_tests_script" +kind = "donna.lib.run_script" +save_stdout_to = "tests_output" +goto_on_success = "finish" +goto_on_failure = "fix_tests" +``` + +```bash donna script +#!/usr/bin/env bash + +pytest donna -o cache_dir=/tmp/donna-pytest-cache 2>&1 +``` + +## Fix Test Issues + +```toml donna +id = "fix_tests" +kind = "donna.lib.request_action" +``` + +``` +{{ donna.lib.task_variable("tests_output") }} +``` + +1. Fix the test failures based on the output above that you are allowed to fix. +2. Ask the developer to fix any remaining failures manually. +3. Ensure your changes are saved. +4. `{{ donna.lib.goto("run_tach_script") }}` + ## Finish ```toml donna From 4a50bde74e44968f088bb3e7d18eff3a63398775 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 10:19:35 +0200 Subject: [PATCH 44/98] skill tests --- donna/skills/tests/__init__.py | 1 + donna/skills/tests/test_entities.py | 11 +++++++++++ donna/skills/tests/test_fixtures.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 donna/skills/tests/__init__.py create mode 100644 donna/skills/tests/test_entities.py create mode 100644 donna/skills/tests/test_fixtures.py diff --git a/donna/skills/tests/__init__.py b/donna/skills/tests/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/donna/skills/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/donna/skills/tests/test_entities.py b/donna/skills/tests/test_entities.py new file mode 100644 index 00000000..000d3369 --- /dev/null +++ b/donna/skills/tests/test_entities.py @@ -0,0 +1,11 @@ +from donna.skills import SkillDocument + + +class TestSkillDocument: + def test_values__match_public_document_names(self) -> None: + assert [document.value for document in SkillDocument] == [ + "usage", + "configuration", + "initialization", + "workflows", + ] diff --git a/donna/skills/tests/test_fixtures.py b/donna/skills/tests/test_fixtures.py new file mode 100644 index 00000000..cef40ae6 --- /dev/null +++ b/donna/skills/tests/test_fixtures.py @@ -0,0 +1,29 @@ +import importlib.resources + +import pytest + +from donna.skills import SkillDocument, load_skill_text + +EXPECTED_DOCUMENT_HEADINGS = { + SkillDocument.usage: "# `donna` Usage", + SkillDocument.configuration: "# `donna` Configuration", + SkillDocument.initialization: "# `donna` Initialization", + SkillDocument.workflows: "# `donna` Workflows", +} + + +class TestLoadSkillText: + def test_default_document_is_usage(self) -> None: + assert load_skill_text() == load_skill_text(SkillDocument.usage) + + @pytest.mark.parametrize(("document", "heading"), EXPECTED_DOCUMENT_HEADINGS.items()) + def test_loads_document_text(self, document: SkillDocument, heading: str) -> None: + assert load_skill_text(document).startswith(f"{heading}\n") + + +def test_fixture_resources__match_public_document_names() -> None: + fixture_dir = importlib.resources.files("donna.skills").joinpath("fixtures") + + fixture_names = sorted(resource.name for resource in fixture_dir.iterdir() if resource.name.endswith(".md")) + + assert fixture_names == sorted(f"{document.value}.md" for document in SkillDocument) From cf984d4bfbc17b0edee9524dd0f90108d9b1085c Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 11:54:05 +0200 Subject: [PATCH 45/98] machine tests --- donna/machine/primitives.py | 8 +- donna/machine/tests/__init__.py | 1 + donna/machine/tests/helpers.py | 96 ++++++++ donna/machine/tests/make.py | 89 ++++++++ donna/machine/tests/test_action_requests.py | 35 +++ donna/machine/tests/test_artifacts.py | 215 ++++++++++++++++++ donna/machine/tests/test_changes.py | 121 ++++++++++ donna/machine/tests/test_context.py | 47 ++++ donna/machine/tests/test_errors.py | 15 ++ donna/machine/tests/test_operations.py | 12 + donna/machine/tests/test_primitives.py | 71 ++++++ donna/machine/tests/test_state.py | 231 ++++++++++++++++++++ donna/machine/tests/test_tasks.py | 89 ++++++++ donna/machine/tests/test_templates.py | 74 +++++++ donna/workspaces/markdown_parser.py | 21 +- 15 files changed, 1108 insertions(+), 17 deletions(-) create mode 100644 donna/machine/tests/__init__.py create mode 100644 donna/machine/tests/helpers.py create mode 100644 donna/machine/tests/make.py create mode 100644 donna/machine/tests/test_action_requests.py create mode 100644 donna/machine/tests/test_artifacts.py create mode 100644 donna/machine/tests/test_changes.py create mode 100644 donna/machine/tests/test_context.py create mode 100644 donna/machine/tests/test_errors.py create mode 100644 donna/machine/tests/test_operations.py create mode 100644 donna/machine/tests/test_primitives.py create mode 100644 donna/machine/tests/test_state.py create mode 100644 donna/machine/tests/test_tasks.py create mode 100644 donna/machine/tests/test_templates.py diff --git a/donna/machine/primitives.py b/donna/machine/primitives.py index f5748db7..46a9065a 100644 --- a/donna/machine/primitives.py +++ b/donna/machine/primitives.py @@ -39,12 +39,8 @@ def apply_directive(self, context: Context, *argv: Any, **kwargs: Any) -> Result @unwrap_to_error -def resolve_primitive(primitive_id: PythonPath | str) -> Result[Primitive, ErrorsList]: # noqa: CCR001 - if isinstance(primitive_id, PythonPath): - import_path = str(primitive_id) - else: - import_path = str(PythonPath.parse(primitive_id).unwrap()) - +def resolve_primitive(primitive_id: PythonPath) -> Result[Primitive, ErrorsList]: # noqa: CCR001 + import_path = str(primitive_id) if "." not in import_path: return Err([machine_errors.PrimitiveInvalidImportPath(import_path=import_path)]) diff --git a/donna/machine/tests/__init__.py b/donna/machine/tests/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/donna/machine/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/donna/machine/tests/helpers.py b/donna/machine/tests/helpers.py new file mode 100644 index 00000000..c2f8e20d --- /dev/null +++ b/donna/machine/tests/helpers.py @@ -0,0 +1,96 @@ +from typing import Any + +from donna.core.errors import ErrorsList +from donna.core.result import Err, Ok, Result +from donna.domain.artifact_ids import ArtifactId +from donna.domain.python_path import PythonPath +from donna.machine.artifacts import Artifact +from donna.machine.context import ValueScope +from donna.machine.primitives import Primitive +from donna.machine.tasks import Task, WorkUnit + + +class FakeArtifacts: + def __init__(self, artifact: Artifact | None = None, error: ErrorsList | None = None) -> None: + self.artifact = artifact + self.error = error + self.viewed: list[ArtifactId] = [] + self.executed: list[tuple[ArtifactId, Task, WorkUnit]] = [] + + def load_for_view(self, artifact_id: ArtifactId) -> Result[Artifact, ErrorsList]: + self.viewed.append(artifact_id) + if self.error is not None: + return Err(self.error) + assert self.artifact is not None + return Ok(self.artifact) + + def load_for_execution( + self, + artifact_id: ArtifactId, + task: Task, + work_unit: WorkUnit, + ) -> Result[Artifact, ErrorsList]: + self.executed.append((artifact_id, task, work_unit)) + if self.error is not None: + return Err(self.error) + assert self.artifact is not None + return Ok(self.artifact) + + +class FakePrimitives: + def __init__(self, primitive: Primitive | None = None, error: ErrorsList | None = None) -> None: + self.primitive = primitive + self.error = error + self.resolved: list[PythonPath] = [] + + def resolve(self, primitive_id: PythonPath) -> Result[Primitive, ErrorsList]: + self.resolved.append(primitive_id) + if self.error is not None: + return Err(self.error) + assert self.primitive is not None + return Ok(self.primitive) + + +class FakeJournal: + def __init__(self) -> None: + self.records: list[dict[str, Any]] = [] + + def add(self, message: str, actor_id: str | None = None) -> Result[Any, ErrorsList]: + self.records.append({"message": message, "actor_id": actor_id}) + return Ok(None) + + +class FakeMachineContext: + def __init__( + self, + *, + artifact: Artifact | None = None, + primitive: Primitive | None = None, + artifact_error: ErrorsList | None = None, + primitive_error: ErrorsList | None = None, + ) -> None: + self._artifacts = FakeArtifacts(artifact=artifact, error=artifact_error) + self._primitives = FakePrimitives(primitive=primitive, error=primitive_error) + self._journal = FakeJournal() + self._current_work_unit_id: ValueScope[Any] = ValueScope() + self._current_operation_id: ValueScope[Any] = ValueScope() + + @property + def artifacts(self) -> FakeArtifacts: + return self._artifacts + + @property + def primitives(self) -> FakePrimitives: + return self._primitives + + @property + def journal(self) -> FakeJournal: + return self._journal + + @property + def current_work_unit_id(self) -> ValueScope[Any]: + return self._current_work_unit_id + + @property + def current_operation_id(self) -> ValueScope[Any]: + return self._current_operation_id diff --git a/donna/machine/tests/make.py b/donna/machine/tests/make.py new file mode 100644 index 00000000..9a112637 --- /dev/null +++ b/donna/machine/tests/make.py @@ -0,0 +1,89 @@ +from typing import Any + +from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId +from donna.domain.ids import SectionId +from donna.domain.internal_ids import ActionRequestId, TaskId, WorkUnitId +from donna.domain.python_path import PythonPath +from donna.machine.action_requests import ActionRequest +from donna.machine.artifacts import Artifact, ArtifactSection, ArtifactSectionMeta +from donna.machine.state import MutableState +from donna.machine.tasks import Task, WorkUnit + +ARTIFACT_ID = ArtifactId("@/workflows/test.donna.md") +PRIMARY_SECTION_ID = SectionId("workflow") +SECONDARY_SECTION_ID = SectionId("next") +PRIMITIVE_PATH = PythonPath("donna.machine.tests.test_primitives.sample_primitive") +PRIMARY_OPERATION_ID = ArtifactSectionId("@/workflows/test.donna.md:workflow") +SECONDARY_OPERATION_ID = ArtifactSectionId("@/workflows/test.donna.md:next") +TASK_ID = TaskId("T-1-b") +WORK_UNIT_ID = WorkUnitId("WU-2-c") +ACTION_REQUEST_ID = ActionRequestId("AR-3-d") + + +def artifact_section( + *, + id: SectionId = PRIMARY_SECTION_ID, + artifact_id: ArtifactId = ARTIFACT_ID, + kind: PythonPath = PRIMITIVE_PATH, + title: str = "Workflow", + description: str = "Workflow description", + primary: bool = False, + meta: ArtifactSectionMeta | None = None, +) -> ArtifactSection: + return ArtifactSection( + id=id, + artifact_id=artifact_id, + kind=kind, + title=title, + description=description, + primary=primary, + meta=meta or ArtifactSectionMeta(), + ) + + +def artifact(sections: list[ArtifactSection] | None = None) -> Artifact: + if sections is None: + sections = [artifact_section(primary=True)] + + return Artifact(id=ARTIFACT_ID, sections=sections) + + +def task(*, id: TaskId = TASK_ID, workflow_id: ArtifactSectionId = PRIMARY_OPERATION_ID) -> Task: + return Task.build(id=id, workflow_id=workflow_id) + + +def work_unit( + *, + id: WorkUnitId = WORK_UNIT_ID, + task_id: TaskId = TASK_ID, + operation_id: ArtifactSectionId = PRIMARY_OPERATION_ID, + context: dict[str, Any] | None = None, +) -> WorkUnit: + return WorkUnit.build(id=id, task_id=task_id, operation_id=operation_id, context=context) + + +def action_request( + *, + id: ActionRequestId | None = ACTION_REQUEST_ID, + title: str = "Action title", + request: str = "Do the thing", + operation_id: ArtifactSectionId = PRIMARY_OPERATION_ID, +) -> ActionRequest: + return ActionRequest(id=id, title=title, request=request, operation_id=operation_id) + + +def mutable_state( + *, + tasks: list[Task] | None = None, + work_units: list[WorkUnit] | None = None, + action_requests: list[ActionRequest] | None = None, + started: bool = True, + last_id: int = 0, +) -> MutableState: + return MutableState( + tasks=tasks or [], + work_units=work_units or [], + action_requests=action_requests or [], + started=started, + last_id=last_id, + ) diff --git a/donna/machine/tests/test_action_requests.py b/donna/machine/tests/test_action_requests.py new file mode 100644 index 00000000..5a80b004 --- /dev/null +++ b/donna/machine/tests/test_action_requests.py @@ -0,0 +1,35 @@ +from donna.machine.action_requests import ActionRequest, ActionRequestNode +from donna.machine.tests import make + + +class TestActionRequest: + def test_build__creates_pending_request_without_id(self) -> None: + request = ActionRequest.build( + title="Need input", + request="Choose next step", + operation_id=make.PRIMARY_OPERATION_ID, + ) + + assert request.id is None + assert request.title == "Need input" + assert request.request == "Choose next step" + assert request.operation_id == make.PRIMARY_OPERATION_ID + + def test_node__returns_action_request_node(self) -> None: + request = make.action_request() + + node = request.node() + + assert isinstance(node, ActionRequestNode) + + +class TestActionRequestNode: + def test_status__returns_action_request_status_cell(self) -> None: + request = make.action_request() + + cell = ActionRequestNode(request).status() + + assert cell.kind == "action_request" + assert cell.media_type == "text/markdown" + assert "Do the thing" in cell.content + assert cell.meta == {"action_request_id": str(make.ACTION_REQUEST_ID)} diff --git a/donna/machine/tests/test_artifacts.py b/donna/machine/tests/test_artifacts.py new file mode 100644 index 00000000..583253ad --- /dev/null +++ b/donna/machine/tests/test_artifacts.py @@ -0,0 +1,215 @@ +from typing import Any + +from donna.core.errors import ErrorsList +from donna.core.result import Err, Result +from donna.domain.ids import SectionId +from donna.domain.python_path import PythonPath +from donna.machine import errors as machine_errors +from donna.machine.artifacts import Artifact, ArtifactNode, ArtifactSection, ArtifactSectionMeta, ArtifactSectionNode +from donna.machine.context import reset_context, set_context +from donna.machine.primitives import Primitive +from donna.machine.tests import make +from donna.machine.tests.helpers import FakeMachineContext + + +class _Meta(ArtifactSectionMeta): + def cells_meta(self) -> dict[str, Any]: + return {"custom": "value"} + + +class _RejectingPrimitive(Primitive): + def validate_section(self, artifact: Artifact, section_id: SectionId) -> Result[None, ErrorsList]: + return Err([machine_errors.PrimitiveInvalidImportPath(import_path="bad")]) + + +class TestArtifactSectionMeta: + def test_cells_meta__returns_empty_metadata(self) -> None: + assert ArtifactSectionMeta().cells_meta() == {} + + +class TestArtifactSection: + def test_markdown_blocks__uses_h2_title_and_description(self) -> None: + section = make.artifact_section(title="Step", description="Description") + + assert section.markdown_blocks() == ["## Step", "Description"] + + def test_node__returns_artifact_section_node(self) -> None: + section = make.artifact_section() + + assert isinstance(section.node(), ArtifactSectionNode) + + +class TestArtifact: + def test_primary_section__returns_single_primary_section(self) -> None: + primary = make.artifact_section(primary=True) + secondary = make.artifact_section(id=make.SECONDARY_SECTION_ID) + + result = make.artifact([secondary, primary]).primary_section() + + assert result.is_ok() + assert result.unwrap() == primary + + def test_primary_section__reports_missing_primary_section(self) -> None: + result = make.artifact([make.artifact_section(primary=False)]).primary_section() + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, machine_errors.ArtifactPrimarySectionMissing) + assert error.artifact_id == make.ARTIFACT_ID + + def test_primary_section__reports_multiple_primary_sections(self) -> None: + first = make.artifact_section(id=SectionId("z"), primary=True) + second = make.artifact_section(id=SectionId("a"), primary=True) + + result = make.artifact([first, second]).primary_section() + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, machine_errors.MultiplePrimarySectionsError) + assert error.primary_sections == [SectionId("a"), SectionId("z")] + + def test_get_section__returns_primary_section_for_none(self) -> None: + artifact = make.artifact() + + result = artifact.get_section(None) + + assert result.is_ok() + assert result.unwrap().id == make.PRIMARY_SECTION_ID + + def test_get_section__returns_requested_section(self) -> None: + secondary = make.artifact_section(id=make.SECONDARY_SECTION_ID) + artifact = make.artifact([make.artifact_section(primary=True), secondary]) + + result = artifact.get_section(make.SECONDARY_SECTION_ID) + + assert result.is_ok() + assert result.unwrap() == secondary + + def test_get_section__reports_missing_section(self) -> None: + artifact = make.artifact() + + result = artifact.get_section(make.SECONDARY_SECTION_ID) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, machine_errors.ArtifactSectionNotFound) + assert error.section_id == make.SECONDARY_SECTION_ID + + def test_get_section_number__returns_zero_based_index(self) -> None: + artifact = make.artifact( + [ + make.artifact_section(primary=True), + make.artifact_section(id=make.SECONDARY_SECTION_ID), + ] + ) + + assert artifact.get_section_number(make.SECONDARY_SECTION_ID) == 1 + assert artifact.get_section_number(SectionId("missing")) is None + + def test_markdown_blocks__uses_primary_as_h1_and_other_sections_as_h2(self) -> None: + artifact = make.artifact( + [ + make.artifact_section(primary=True, title="Workflow", description="Intro"), + make.artifact_section(id=make.SECONDARY_SECTION_ID, title="Next", description="Body"), + ] + ) + + result = artifact.markdown_blocks() + + assert result.is_ok() + assert result.unwrap() == ["# Workflow", "Intro", "## Next", "Body"] + + def test_markdown_blocks__returns_primary_section_error(self) -> None: + result = make.artifact([make.artifact_section(primary=False)]).markdown_blocks() + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], machine_errors.ArtifactPrimarySectionMissing) + + def test_validate_artifact__accepts_valid_artifact(self) -> None: + artifact = make.artifact() + machine_context = FakeMachineContext(primitive=Primitive()) + token = set_context(machine_context) + + try: + result = artifact.validate_artifact() + finally: + reset_context(token) + + assert result.is_ok() + assert machine_context.primitives.resolved == [make.PRIMITIVE_PATH] + + def test_validate_artifact__collects_primary_and_section_errors(self) -> None: + artifact = make.artifact([make.artifact_section(primary=False)]) + machine_context = FakeMachineContext(primitive=_RejectingPrimitive()) + token = set_context(machine_context) + + try: + result = artifact.validate_artifact() + finally: + reset_context(token) + + assert result.is_err() + errors = result.unwrap_err() + assert [type(error) for error in errors] == [ + machine_errors.ArtifactPrimarySectionMissing, + machine_errors.PrimitiveInvalidImportPath, + ] + + def test_node__returns_artifact_node(self) -> None: + assert isinstance(make.artifact().node(), ArtifactNode) + + +class TestArtifactNode: + def test_status__returns_primary_section_summary(self) -> None: + cell = make.artifact().node().status() + + assert cell.kind == "artifact_status" + assert cell.content == "Workflow description" + assert cell.meta == { + "artifact_id": str(make.ARTIFACT_ID), + "artifact_kind": str(make.PRIMITIVE_PATH), + "artifact_title": "Workflow", + } + + def test_info__returns_artifact_markdown(self) -> None: + cell = ( + make.artifact( + [ + make.artifact_section(primary=True), + make.artifact_section(id=make.SECONDARY_SECTION_ID, title="Next", description="Body"), + ] + ) + .node() + .info() + ) + + assert cell.kind == "artifact_info" + assert cell.content == "# Workflow\nWorkflow description\n## Next\nBody" + assert cell.meta == { + "artifact_id": str(make.ARTIFACT_ID), + "artifact_kind": str(make.PRIMITIVE_PATH), + } + + def test_components__returns_section_nodes(self) -> None: + components = make.artifact().node().components() + + assert len(components) == 1 + assert isinstance(components[0], ArtifactSectionNode) + + +class TestArtifactSectionNode: + def test_status__returns_section_status_cell(self) -> None: + section = make.artifact_section(primary=True, meta=_Meta()) + + cell = ArtifactSectionNode(section).status() + + assert cell.kind == "artifact_section_status" + assert cell.media_type == "text/markdown" + assert cell.content == "## Workflow\nWorkflow description" + assert cell.meta == { + "artifact_id": str(make.ARTIFACT_ID), + "section_id": str(make.PRIMARY_SECTION_ID), + "section_kind": str(make.PRIMITIVE_PATH), + "section_primary": True, + "custom": "value", + } diff --git a/donna/machine/tests/test_changes.py b/donna/machine/tests/test_changes.py new file mode 100644 index 00000000..42add80b --- /dev/null +++ b/donna/machine/tests/test_changes.py @@ -0,0 +1,121 @@ +import pytest + +from donna.domain.internal_ids import TaskId +from donna.machine.changes import ( + ChangeAddActionRequest, + ChangeAddTask, + ChangeAddWorkUnit, + ChangeFinishTask, + ChangeRemoveActionRequest, + ChangeRemoveTask, + ChangeRemoveWorkUnit, + ChangeSetTaskContext, +) +from donna.machine.context import reset_context, set_context +from donna.machine.tests import make +from donna.machine.tests.helpers import FakeMachineContext + + +class TestChangeAddTask: + def test_apply_to__adds_task_initial_work_unit_and_marks_started(self) -> None: + state = make.mutable_state(started=False) + + ChangeAddTask(operation_id=make.PRIMARY_OPERATION_ID).apply_to(state) + + assert state.started + assert len(state.tasks) == 1 + assert state.tasks[0].id == "T-1-b" + assert len(state.work_units) == 1 + assert state.work_units[0].id == "WU-2-c" + assert state.work_units[0].task_id == state.tasks[0].id + assert state.work_units[0].operation_id == make.PRIMARY_OPERATION_ID + + +class TestChangeFinishTask: + def test_apply_to__finishes_workflow_for_task(self) -> None: + task = make.task() + state = make.mutable_state(tasks=[task]) + machine_context = FakeMachineContext(artifact=make.artifact()) + token = set_context(machine_context) + + try: + ChangeFinishTask(task_id=task.id).apply_to(state) + finally: + reset_context(token) + + assert state.tasks == [] + assert machine_context.journal.records == [{"message": "Finish workflow `Workflow`", "actor_id": None}] + + +class TestChangeAddWorkUnit: + def test_apply_to__adds_work_unit_with_next_id(self) -> None: + state = make.mutable_state(last_id=2) + + ChangeAddWorkUnit(task_id=make.TASK_ID, operation_id=make.SECONDARY_OPERATION_ID).apply_to(state) + + assert len(state.work_units) == 1 + assert state.work_units[0].id == "WU-3-d" + assert state.work_units[0].task_id == make.TASK_ID + assert state.work_units[0].operation_id == make.SECONDARY_OPERATION_ID + + +class TestChangeAddActionRequest: + def test_apply_to__delegates_to_state_action_request_addition(self) -> None: + state = make.mutable_state(last_id=2) + request = make.action_request(id=None) + machine_context = FakeMachineContext() + token = set_context(machine_context) + + try: + ChangeAddActionRequest(action_request=request).apply_to(state) + finally: + reset_context(token) + + assert state.action_requests == [request.replace(id=make.ACTION_REQUEST_ID)] + + +class TestChangeRemoveActionRequest: + def test_apply_to__removes_matching_action_request(self) -> None: + state = make.mutable_state(action_requests=[make.action_request()]) + + ChangeRemoveActionRequest(action_request_id=make.ACTION_REQUEST_ID).apply_to(state) + + assert state.action_requests == [] + + +class TestChangeRemoveWorkUnit: + def test_apply_to__removes_matching_work_unit(self) -> None: + state = make.mutable_state(work_units=[make.work_unit()]) + + ChangeRemoveWorkUnit(work_unit_id=make.WORK_UNIT_ID).apply_to(state) + + assert state.work_units == [] + + +class TestChangeRemoveTask: + def test_apply_to__removes_matching_task(self) -> None: + state = make.mutable_state(tasks=[make.task()]) + + ChangeRemoveTask(task_id=make.TASK_ID).apply_to(state) + + assert state.tasks == [] + + +class TestChangeSetTaskContext: + def test_apply_to__updates_matching_task_context(self) -> None: + target = make.task(id=make.TASK_ID) + other = make.task(id=TaskId("T-2-c")) + state = make.mutable_state(tasks=[target, other]) + + ChangeSetTaskContext(task_id=make.TASK_ID, key="answer", value=42).apply_to(state) + + assert state.tasks[0].context == {"answer": 42} + assert state.tasks[1].context == {} + + def test_apply_to__raises_when_task_is_missing(self) -> None: + state = make.mutable_state(tasks=[make.task(id=TaskId("T-2-c"))]) + + with pytest.raises(AssertionError): + ChangeSetTaskContext(task_id=make.TASK_ID, key="answer", value=42).apply_to(state) + + assert state.tasks[0].context == {} diff --git a/donna/machine/tests/test_context.py b/donna/machine/tests/test_context.py new file mode 100644 index 00000000..be89a985 --- /dev/null +++ b/donna/machine/tests/test_context.py @@ -0,0 +1,47 @@ +import pytest + +from donna.machine import errors as machine_errors +from donna.machine.context import ValueScope, context, reset_context, set_context +from donna.machine.tests.helpers import FakeMachineContext + + +class TestValueScope: + def test_get__returns_initial_value(self) -> None: + scope = ValueScope("initial") + + assert scope.get() == "initial" + + def test_scope__restores_previous_value(self) -> None: + scope = ValueScope("outer") + + with scope.scope("inner"): + assert scope.get() == "inner" + + assert scope.get() == "outer" + + def test_scope__restores_previous_value_after_exception(self) -> None: + scope = ValueScope("outer") + + with pytest.raises(ValueError): + with scope.scope("inner"): + raise ValueError + + assert scope.get() == "outer" + + +class TestContext: + def test_context__raises_when_not_set(self) -> None: + with pytest.raises(machine_errors.MachineContextNotSet): + context() + + def test_context__returns_current_context_until_reset(self) -> None: + machine_context = FakeMachineContext() + token = set_context(machine_context) + + try: + assert context() == machine_context + finally: + reset_context(token) + + with pytest.raises(machine_errors.MachineContextNotSet): + context() diff --git a/donna/machine/tests/test_errors.py b/donna/machine/tests/test_errors.py new file mode 100644 index 00000000..ba7baeeb --- /dev/null +++ b/donna/machine/tests/test_errors.py @@ -0,0 +1,15 @@ +from donna.domain.ids import SectionId +from donna.machine.errors import ArtifactPrimarySectionMissing +from donna.machine.tests import make + + +class TestArtifactValidationError: + def test_content_intro__describes_artifact_error(self) -> None: + error = ArtifactPrimarySectionMissing(artifact_id=make.ARTIFACT_ID) + + assert error.content_intro() == "Error in artifact '@/workflows/test.donna.md'" + + def test_content_intro__describes_section_error(self) -> None: + error = ArtifactPrimarySectionMissing(artifact_id=make.ARTIFACT_ID, section_id=SectionId("section")) + + assert error.content_intro() == "Error in artifact '@/workflows/test.donna.md', section 'section'" diff --git a/donna/machine/tests/test_operations.py b/donna/machine/tests/test_operations.py new file mode 100644 index 00000000..351e80ab --- /dev/null +++ b/donna/machine/tests/test_operations.py @@ -0,0 +1,12 @@ +from donna.domain.ids import SectionId +from donna.machine.operations import FsmMode, OperationMeta + + +class TestOperationMeta: + def test_cells_meta__serializes_fsm_mode_and_allowed_transitions(self) -> None: + meta = OperationMeta(fsm_mode=FsmMode.final, allowed_transtions={SectionId("next"), SectionId("done")}) + + cell_meta = meta.cells_meta() + + assert cell_meta["fsm_mode"] == "final" + assert set(cell_meta["allowed_transtions"]) == {"next", "done"} diff --git a/donna/machine/tests/test_primitives.py b/donna/machine/tests/test_primitives.py new file mode 100644 index 00000000..3e0da76d --- /dev/null +++ b/donna/machine/tests/test_primitives.py @@ -0,0 +1,71 @@ +import pytest + +from donna.domain.python_path import PythonPath +from donna.machine import errors as machine_errors +from donna.machine.primitives import Primitive, resolve_primitive +from donna.machine.tests import make + +sample_primitive = Primitive() +sample_non_primitive = object() + + +class TestPrimitive: + def test_validate_section__allows_section_by_default(self) -> None: + result = sample_primitive.validate_section(make.artifact(), make.PRIMARY_SECTION_ID) + + assert result.is_ok() + + def test_execute_section__raises_unsupported_method(self) -> None: + with pytest.raises(machine_errors.PrimitiveMethodUnsupported) as exception_info: + sample_primitive.execute_section(make.task(), make.work_unit(), make.artifact(), make.PRIMARY_SECTION_ID) + + error = exception_info.value + assert error.arguments == {"primitive_name": "Primitive", "method_name": "execute_section()"} + + def test_apply_directive__raises_unsupported_method(self) -> None: + with pytest.raises(machine_errors.PrimitiveMethodUnsupported) as exception_info: + sample_primitive.apply_directive({}) + + error = exception_info.value + assert error.arguments == {"primitive_name": "Primitive", "method_name": "apply_directive()"} + + +class TestResolvePrimitive: + def test_success_from_python_path(self) -> None: + result = resolve_primitive(make.PRIMITIVE_PATH) + + assert result.is_ok() + assert result.unwrap() == sample_primitive + + def test_invalid_import_path_without_attribute_name(self) -> None: + result = resolve_primitive(PythonPath("primitive")) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, machine_errors.PrimitiveInvalidImportPath) + assert error.import_path == "primitive" + + def test_module_not_importable(self) -> None: + result = resolve_primitive(PythonPath("donna.machine.tests.missing.sample_primitive")) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, machine_errors.PrimitiveModuleNotImportable) + assert error.module_path == "donna.machine.tests.missing" + + def test_primitive_not_available(self) -> None: + result = resolve_primitive(PythonPath("donna.machine.tests.test_primitives.missing_primitive")) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, machine_errors.PrimitiveNotAvailable) + assert error.import_path == "donna.machine.tests.test_primitives.missing_primitive" + assert error.module_path == "donna.machine.tests.test_primitives" + + def test_object_is_not_primitive(self) -> None: + result = resolve_primitive(PythonPath("donna.machine.tests.test_primitives.sample_non_primitive")) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, machine_errors.PrimitiveNotPrimitive) + assert error.import_path == "donna.machine.tests.test_primitives.sample_non_primitive" diff --git a/donna/machine/tests/test_state.py b/donna/machine/tests/test_state.py new file mode 100644 index 00000000..1e872fcc --- /dev/null +++ b/donna/machine/tests/test_state.py @@ -0,0 +1,231 @@ +from donna.core.errors import ErrorsList +from donna.core.result import Ok, Result +from donna.domain.internal_ids import TaskId, WorkUnitId +from donna.machine import errors as machine_errors +from donna.machine.changes import Change, ChangeSetTaskContext +from donna.machine.context import reset_context, set_context +from donna.machine.operations import OperationKind +from donna.machine.state import MutableState +from donna.machine.tasks import Task, WorkUnit +from donna.machine.tests import make +from donna.machine.tests.helpers import FakeMachineContext + + +class _StateOperation(OperationKind): + def execute_section( + self, + task: Task, + unit: WorkUnit, + artifact: object, + section_id: object, + ) -> Result[list[Change], ErrorsList]: + return Ok([ChangeSetTaskContext(task_id=task.id, key="status", value="done")]) + + +class TestBaseState: + def test_has_work__depends_on_queued_work_units(self) -> None: + assert not make.mutable_state(work_units=[]).has_work() + assert make.mutable_state(work_units=[make.work_unit()]).has_work() + + def test_current_task__returns_last_task(self) -> None: + first = make.task(id=make.TASK_ID) + second = make.task(id=TaskId("T-2-c")) + state = make.mutable_state(tasks=[first, second]) + + assert state.current_task == second + + def test_current_task__returns_none_without_tasks(self) -> None: + assert make.mutable_state(tasks=[]).current_task is None + + def test_get_action_request__returns_matching_request(self) -> None: + request = make.action_request() + state = make.mutable_state(action_requests=[request]) + + result = state.get_action_request(make.ACTION_REQUEST_ID) + + assert result.is_ok() + assert result.unwrap() == request + + def test_get_action_request__reports_missing_request(self) -> None: + result = make.mutable_state(action_requests=[]).get_action_request(make.ACTION_REQUEST_ID) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, machine_errors.ActionRequestNotFound) + assert error.request_id == make.ACTION_REQUEST_ID + + def test_get_next_work_unit__returns_first_unit_for_current_task(self) -> None: + older_task = make.task(id=make.TASK_ID) + current_task = make.task(id=TaskId("T-2-c")) + older_unit = make.work_unit(task_id=older_task.id) + current_unit = make.work_unit(id=WorkUnitId("WU-3-d"), task_id=current_task.id) + state = make.mutable_state(tasks=[older_task, current_task], work_units=[older_unit, current_unit]) + + assert state.get_next_work_unit() == current_unit + + +class TestMutableState: + def test_build__creates_empty_not_started_state(self) -> None: + state = MutableState.build() + + assert state.tasks == [] + assert state.work_units == [] + assert state.action_requests == [] + assert not state.started + assert state.last_id == 0 + + def test_next_ids__increment_shared_counter(self) -> None: + state = MutableState.build() + + assert state.next_task_id() == "T-1-b" + assert state.next_work_unit_id() == "WU-2-c" + assert state.next_action_request_id() == "AR-3-d" + assert state.last_id == 3 + + def test_freeze_and_mutator__deep_copy_state(self) -> None: + state = make.mutable_state(tasks=[make.task()]) + + frozen = state.freeze() + state.tasks[0].context["changed"] = True + mutable = frozen.mutator() + mutable.tasks[0].context["mutable"] = True + + assert frozen.tasks[0].context == {} + assert mutable.tasks[0].context == {"mutable": True} + + def test_add_action_request__assigns_id_and_logs_request(self) -> None: + state = make.mutable_state(last_id=2) + request = make.action_request(id=None) + machine_context = FakeMachineContext() + token = set_context(machine_context) + + try: + state.add_action_request(request) + finally: + reset_context(token) + + assert state.action_requests == [request.replace(id=make.ACTION_REQUEST_ID)] + assert machine_context.journal.records == [ + {"message": "Request agent action `Action title`", "actor_id": "donna"} + ] + + def test_apply_changes__applies_changes_in_order(self) -> None: + state = make.mutable_state(tasks=[make.task()]) + + state.apply_changes([ChangeSetTaskContext(task_id=make.TASK_ID, key="first", value=1)]) + + assert state.tasks[0].context == {"first": 1} + + def test_complete_action_request__queues_next_work_unit_and_removes_request(self) -> None: + task = make.task() + request = make.action_request() + state = make.mutable_state(tasks=[task], action_requests=[request], last_id=2) + machine_context = FakeMachineContext() + token = set_context(machine_context) + + try: + result = state.complete_action_request(make.ACTION_REQUEST_ID, make.SECONDARY_OPERATION_ID) + finally: + reset_context(token) + + assert result.is_ok() + assert state.action_requests == [] + assert len(state.work_units) == 1 + assert state.work_units[0].id == "WU-3-d" + assert state.work_units[0].task_id == task.id + assert state.work_units[0].operation_id == make.SECONDARY_OPERATION_ID + assert machine_context.journal.records == [ + {"message": "Complete agent action `Action title`", "actor_id": None} + ] + + def test_start_workflow__creates_task_and_initial_work_unit(self) -> None: + state = MutableState.build() + artifact = make.artifact() + machine_context = FakeMachineContext(artifact=artifact) + token = set_context(machine_context) + + try: + result = state.start_workflow(make.PRIMARY_OPERATION_ID) + finally: + reset_context(token) + + assert result.is_ok() + assert state.started + assert len(state.tasks) == 1 + assert state.tasks[0].id == "T-1-b" + assert state.tasks[0].workflow_id == make.PRIMARY_OPERATION_ID + assert len(state.work_units) == 1 + assert state.work_units[0].id == "WU-2-c" + assert state.work_units[0].task_id == state.tasks[0].id + assert machine_context.artifacts.viewed == [make.ARTIFACT_ID] + assert machine_context.journal.records == [{"message": "Start workflow `Workflow`", "actor_id": None}] + + def test_finish_workflow__removes_task_and_logs_workflow_title(self) -> None: + task = make.task() + state = make.mutable_state(tasks=[task]) + machine_context = FakeMachineContext(artifact=make.artifact()) + token = set_context(machine_context) + + try: + state.finish_workflow(task.id) + finally: + reset_context(token) + + assert state.tasks == [] + assert machine_context.journal.records == [{"message": "Finish workflow `Workflow`", "actor_id": None}] + + def test_execute_next_work_unit__applies_operation_changes_and_removes_unit(self) -> None: + task = make.task() + unit = make.work_unit() + state = make.mutable_state(tasks=[task], work_units=[unit]) + machine_context = FakeMachineContext(artifact=make.artifact(), primitive=_StateOperation()) + token = set_context(machine_context) + + try: + result = state.execute_next_work_unit() + finally: + reset_context(token) + + assert result.is_ok() + assert state.work_units == [] + assert state.tasks[0].context == {"status": "done"} + assert machine_context.current_work_unit_id.get() is None + + +class TestStateNode: + def test_status__reports_new_session(self) -> None: + cell = MutableState.build().node().status() + + assert cell.kind == "session_state_status" + assert "new session" in cell.content + assert cell.meta == {"tasks": 0, "queued_work_units": 0, "pending_action_requests": 0} + + def test_status__reports_idle_session(self) -> None: + cell = make.mutable_state(tasks=[], work_units=[], action_requests=[], started=True).node().status() + + assert "IDLE" in cell.content + assert cell.meta["tasks"] == 0 + + def test_status__reports_pending_work_units(self) -> None: + cell = make.mutable_state(tasks=[make.task()], work_units=[make.work_unit()]).node().status() + + assert "PENDING WORK UNITS" in cell.content + assert cell.meta["queued_work_units"] == 1 + + def test_status__reports_pending_action_requests(self) -> None: + cell = make.mutable_state(tasks=[make.task()], action_requests=[make.action_request()]).node().status() + + assert "AWAITING YOUR ACTION" in cell.content + assert cell.meta["pending_action_requests"] == 1 + + def test_status__reports_unfinished_tasks(self) -> None: + cell = make.mutable_state(tasks=[make.task()]).node().status() + + assert "unfinished TASKS" in cell.content + assert cell.meta["tasks"] == 1 + + def test_references__returns_action_request_nodes(self) -> None: + references = make.mutable_state(action_requests=[make.action_request()]).node().references() + + assert len(references) == 1 + assert references[0].status().kind == "action_request" diff --git a/donna/machine/tests/test_tasks.py b/donna/machine/tests/test_tasks.py new file mode 100644 index 00000000..6cb09480 --- /dev/null +++ b/donna/machine/tests/test_tasks.py @@ -0,0 +1,89 @@ +from donna.core.errors import ErrorsList +from donna.core.result import Ok, Result +from donna.domain.internal_ids import TaskId, WorkUnitId +from donna.machine.changes import Change, ChangeSetTaskContext +from donna.machine.context import reset_context, set_context +from donna.machine.operations import OperationKind +from donna.machine.tasks import Task, WorkUnit +from donna.machine.tests import make +from donna.machine.tests.helpers import FakeMachineContext + + +class _ContextSettingOperation(OperationKind): + def execute_section( + self, + task: Task, + unit: WorkUnit, + artifact: object, + section_id: object, + ) -> Result[list[Change], ErrorsList]: + return Ok( + [ + ChangeSetTaskContext( + task_id=task.id, + key="executed", + value={ + "unit_id": str(unit.id), + "section_id": str(section_id), + }, + ) + ] + ) + + +class TestTask: + def test_build__creates_empty_context(self) -> None: + task = Task.build(id=TaskId("T-1-b"), workflow_id=make.PRIMARY_OPERATION_ID) + + assert task.context == {} + + +class TestWorkUnit: + def test_build__uses_empty_context_by_default(self) -> None: + unit = WorkUnit.build( + id=WorkUnitId("WU-1-b"), + task_id=TaskId("T-1-b"), + operation_id=make.PRIMARY_OPERATION_ID, + ) + + assert unit.context == {} + + def test_build__deep_copies_context(self) -> None: + source_context = {"items": [1]} + + unit = WorkUnit.build( + id=WorkUnitId("WU-1-b"), + task_id=TaskId("T-1-b"), + operation_id=make.PRIMARY_OPERATION_ID, + context=source_context, + ) + source_context["items"].append(2) + + assert unit.context == {"items": [1]} + + def test_run__executes_operation_and_restores_scope(self) -> None: + task = make.task() + unit = make.work_unit() + artifact = make.artifact() + primitive = _ContextSettingOperation() + machine_context = FakeMachineContext(artifact=artifact, primitive=primitive) + token = set_context(machine_context) + + try: + result = unit.run(task) + finally: + reset_context(token) + + assert result.is_ok() + changes = result.unwrap() + assert changes == [ + ChangeSetTaskContext( + task_id=task.id, + key="executed", + value={"unit_id": str(unit.id), "section_id": str(make.PRIMARY_SECTION_ID)}, + ) + ] + assert machine_context.artifacts.executed == [(make.ARTIFACT_ID, task, unit)] + assert machine_context.primitives.resolved == [make.PRIMITIVE_PATH] + assert machine_context.journal.records == [{"message": "Workflow", "actor_id": "donna"}] + assert machine_context.current_operation_id.get() is None diff --git a/donna/machine/tests/test_templates.py b/donna/machine/tests/test_templates.py new file mode 100644 index 00000000..624d11be --- /dev/null +++ b/donna/machine/tests/test_templates.py @@ -0,0 +1,74 @@ +from typing import Any + +import pytest + +from donna.core.errors import ErrorsList +from donna.core.result import Err, Ok, Result +from donna.machine import errors as machine_errors +from donna.machine.templates import Directive, DirectiveUnsupportedRenderMode, RenderMode + + +class _Directive(Directive): + analyze_id: str = "sample" + + def render_view(self, context: dict[str, Any], *argv: Any) -> Result[Any, ErrorsList]: + return Ok({"mode": context["render_mode"], "argv": argv}) + + +class _PreparingDirective(_Directive): + def _prepare_arguments( + self, context: dict[str, Any], *argv: Any, **kwargs: Any + ) -> Result[tuple[Any, ...], ErrorsList]: + return Ok(("prepared", *argv, kwargs["extra"])) + + +class _FailingDirective(_Directive): + def _prepare_arguments( + self, context: dict[str, Any], *argv: Any, **kwargs: Any + ) -> Result[tuple[Any, ...], ErrorsList]: + return Err([machine_errors.PrimitiveInvalidImportPath(import_path="bad")]) + + +class TestDirective: + def test_apply_directive__renders_view_mode(self) -> None: + result = _Directive().apply_directive({"render_mode": RenderMode.view}, "value") + + assert result.is_ok() + assert result.unwrap() == {"mode": RenderMode.view, "argv": ("value",)} + + def test_apply_directive__renders_execute_mode_with_view_default(self) -> None: + result = _Directive().apply_directive({"render_mode": RenderMode.execute}, "value") + + assert result.is_ok() + assert result.unwrap() == {"mode": RenderMode.execute, "argv": ("value",)} + + def test_apply_directive__renders_analysis_mode(self) -> None: + result = _Directive().apply_directive({"render_mode": RenderMode.analysis}, "a", 2) + + assert result.is_ok() + assert result.unwrap() == "$$donna sample a 2 donna$$" + + def test_apply_directive__uses_prepared_arguments(self) -> None: + result = _PreparingDirective().apply_directive({"render_mode": RenderMode.view}, "value", extra="tail") + + assert result.is_ok() + assert result.unwrap() == {"mode": RenderMode.view, "argv": ("prepared", "value", "tail")} + + def test_apply_directive__returns_argument_preparation_error(self) -> None: + result = _FailingDirective().apply_directive({"render_mode": RenderMode.view}) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], machine_errors.PrimitiveInvalidImportPath) + + def test_apply_directive__rejects_unsupported_render_mode(self) -> None: + with pytest.raises(DirectiveUnsupportedRenderMode) as exception_info: + _Directive().apply_directive({"render_mode": "unsupported"}) + + error = exception_info.value + assert error.arguments == {"render_mode": "unsupported", "directive_name": "_Directive"} + + def test_render_analyze__omits_empty_argument_gap(self) -> None: + result = _Directive().render_analyze({"render_mode": RenderMode.analysis}) + + assert result.is_ok() + assert result.unwrap() == "$$donna sample donna$$" diff --git a/donna/workspaces/markdown_parser.py b/donna/workspaces/markdown_parser.py index c23ec7ca..4f8d2bd0 100644 --- a/donna/workspaces/markdown_parser.py +++ b/donna/workspaces/markdown_parser.py @@ -65,7 +65,7 @@ def markdown_construct_section( # noqa: CCR001 config: dict[str, Any], primary: bool = False, ) -> Result[ArtifactSection, ErrorsList]: - section_config = self.config_class.parse_obj(config) + section_config = self.config_class.model_validate(config) title = self.markdown_build_title( artifact_id=artifact_id, @@ -165,11 +165,7 @@ def construct_artifact_from_markdown_source( # noqa: CCR001 if "kind" not in head_config or head_config["kind"] is None: head_config["kind"] = default_primary_section_kind - head_kind_value = head_config["kind"] - if isinstance(head_kind_value, PythonPath): - head_kind = head_kind_value - else: - head_kind = PythonPath.parse(head_kind_value).unwrap() + head_kind = _parse_primitive_id(head_config["kind"]).unwrap() if "id" not in head_config or head_config["id"] is None: head_config["id"] = default_primary_section_id @@ -216,11 +212,7 @@ def construct_sections_from_markdown( # noqa: CCR001 if "kind" not in data: data["kind"] = default_section_kind - kind_value = data["kind"] - if isinstance(kind_value, str): - primitive_id = PythonPath.parse(kind_value).unwrap() - else: - primitive_id = kind_value + primitive_id = _parse_primitive_id(data["kind"]).unwrap() primitive = _resolve_primitive(primitive_id, primitive_overrides).unwrap() _ensure_markdown_constructible(primitive, primitive_id).unwrap() @@ -248,6 +240,13 @@ def _resolve_primitive( return resolve_primitive(primitive_id) +def _parse_primitive_id(value: PythonPath | str) -> Result[PythonPath, ErrorsList]: + if isinstance(value, PythonPath): + return Ok(value) + + return PythonPath.parse(value) + + def _ensure_markdown_constructible( primitive: Primitive, primitive_id: PythonPath | str | None = None, From 1b3145285811bd116073132b0a1155f3455d9f0b Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 12:30:40 +0200 Subject: [PATCH 46/98] workspaces tests --- donna/workspaces/markdown.py | 3 +- donna/workspaces/tests/__init__.py | 0 donna/workspaces/tests/make.py | 68 ++++ donna/workspaces/tests/test_artifacts.py | 323 ++++++++++++++++++ donna/workspaces/tests/test_config.py | 201 +++++++++++ donna/workspaces/tests/test_errors.py | 204 +++++++++++ donna/workspaces/tests/test_files.py | 24 ++ donna/workspaces/tests/test_initialization.py | 134 ++++++++ donna/workspaces/tests/test_journal.py | 191 +++++++++++ donna/workspaces/tests/test_markdown.py | 215 ++++++++++++ .../workspaces/tests/test_markdown_parser.py | 260 ++++++++++++++ donna/workspaces/tests/test_paths.py | 229 +++++++++++++ donna/workspaces/tests/test_sessions.py | 102 ++++++ .../workspaces/tests/test_symbol_coverage.py | 67 ++++ donna/workspaces/tests/test_templates.py | 185 ++++++++++ donna/workspaces/tests/test_utils.py | 40 +++ 16 files changed, 2245 insertions(+), 1 deletion(-) create mode 100644 donna/workspaces/tests/__init__.py create mode 100644 donna/workspaces/tests/make.py create mode 100644 donna/workspaces/tests/test_artifacts.py create mode 100644 donna/workspaces/tests/test_config.py create mode 100644 donna/workspaces/tests/test_errors.py create mode 100644 donna/workspaces/tests/test_files.py create mode 100644 donna/workspaces/tests/test_initialization.py create mode 100644 donna/workspaces/tests/test_journal.py create mode 100644 donna/workspaces/tests/test_markdown.py create mode 100644 donna/workspaces/tests/test_markdown_parser.py create mode 100644 donna/workspaces/tests/test_paths.py create mode 100644 donna/workspaces/tests/test_sessions.py create mode 100644 donna/workspaces/tests/test_symbol_coverage.py create mode 100644 donna/workspaces/tests/test_templates.py create mode 100644 donna/workspaces/tests/test_utils.py diff --git a/donna/workspaces/markdown.py b/donna/workspaces/markdown.py index 0688cb5d..1a2de02b 100644 --- a/donna/workspaces/markdown.py +++ b/donna/workspaces/markdown.py @@ -262,7 +262,8 @@ def parse( # noqa: CCR001, CFQ001 tokens = md.parse(text) # we do not need root node - node: SyntaxTreeNode | None = SyntaxTreeNode(tokens).children[0] + root = SyntaxTreeNode(tokens) + node: SyntaxTreeNode | None = root.children[0] if root.children else None sections: list[SectionSource] = [] diff --git a/donna/workspaces/tests/__init__.py b/donna/workspaces/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/donna/workspaces/tests/make.py b/donna/workspaces/tests/make.py new file mode 100644 index 00000000..6265ec7f --- /dev/null +++ b/donna/workspaces/tests/make.py @@ -0,0 +1,68 @@ +import datetime +import pathlib + +from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId +from donna.domain.internal_ids import TaskId, WorkUnitId +from donna.domain.ids import SectionId +from donna.domain.paths import ProjectConfigPath, ProjectRootPath +from donna.domain.python_path import PythonPath +from donna.machine.artifacts import ArtifactSectionConfig +from donna.protocol.journal import JournalRecord +from donna.workspaces.config import Config, Workspace +from donna.workspaces.markdown import CodeSource, SectionLevel, SectionSource + +ARTIFACT_ID = ArtifactId("@/workflows/test.donna.md") +OPERATION_ID = ArtifactSectionId("@/workflows/test.donna.md:primary") +TEXT_KIND = PythonPath("donna.primitives.sections.text.Text") + + +def workspace(root: pathlib.Path, config: Config | None = None) -> Workspace: + return Workspace( + root=ProjectRootPath(root), + config_path=ProjectConfigPath(root / "donna.toml"), + config=config or Config(), + ) + + +def journal_record( + *, + actor_id: str | None = "agent", + current_task_id: TaskId | None = TaskId("T-1-b"), + current_work_unit_id: WorkUnitId | None = WorkUnitId("WU-2-c"), + current_operation_id: ArtifactSectionId | None = OPERATION_ID, +) -> JournalRecord: + return JournalRecord( + timestamp=datetime.datetime(2026, 5, 18, 10, 30, tzinfo=datetime.UTC), + actor_id=actor_id, + message="message", + current_task_id=current_task_id, + current_work_unit_id=current_work_unit_id, + current_operation_id=current_operation_id, + ) + + +def section_source( + *, + level: SectionLevel = SectionLevel.h2, + title: str | None = "Section", + configs: list[CodeSource] | None = None, +) -> SectionSource: + return SectionSource( + level=level, + title=title, + configs=configs or [], + original_tokens=[], + analysis_tokens=[], + ) + + +def code_source(format: str, content: str, **properties: str | bool) -> CodeSource: + return CodeSource(format=format, content=content, properties=properties) + + +def section_config( + *, + id: SectionId = SectionId("section"), + kind: PythonPath = TEXT_KIND, +) -> ArtifactSectionConfig: + return ArtifactSectionConfig(id=id, kind=kind) diff --git a/donna/workspaces/tests/test_artifacts.py b/donna/workspaces/tests/test_artifacts.py new file mode 100644 index 00000000..6aa82a64 --- /dev/null +++ b/donna/workspaces/tests/test_artifacts.py @@ -0,0 +1,323 @@ +import pathlib + +from donna.core.result import Err, Ok +from donna.domain.artifact_ids import ArtifactId +from donna.domain.paths import RelativeProjectPath, ResolvedProjectPath +from donna.machine.artifacts import Artifact +from donna.workspaces import artifacts +from donna.workspaces import errors as workspace_errors +from donna.workspaces.config import Config +from donna.workspaces.files import FileFingerprint +from donna.workspaces.tests import make + + +class TestHasDonnaArtifactExtension: + def test_matches_donna_markdown_suffix_case_insensitively(self) -> None: + assert artifacts.has_donna_artifact_extension("workflow.donna.md") + assert artifacts.has_donna_artifact_extension("workflow.DONNA.MD") + assert not artifacts.has_donna_artifact_extension("workflow.md") + + +class TestArtifactRenderContext: + def test_defaults__omit_current_work(self) -> None: + context = artifacts.ArtifactRenderContext(primary_mode=artifacts.RenderMode.view) + + assert context.primary_mode == artifacts.RenderMode.view + assert context.current_task is None + assert context.current_work_unit is None + + +class TestArtifactIdFromParts: + def test_returns_artifact_id_for_valid_parts(self) -> None: + assert artifacts._artifact_id_from_parts(["workflows", "test.donna.md"]) == make.ARTIFACT_ID + + def test_returns_none_for_invalid_parts(self) -> None: + assert artifacts._artifact_id_from_parts(["invalid name.donna.md"]) is None + + +class TestWorkflowDirParts: + def test_returns_posix_parts(self) -> None: + path = RelativeProjectPath(pathlib.Path("workflows") / "nested") + + assert artifacts._workflow_dir_parts(path) == ("workflows", "nested") + + +class TestArtifactIsInWorkflowDirs: + def test_accepts_artifacts_under_configured_workflow_dirs(self) -> None: + assert artifacts._artifact_is_in_workflow_dirs( + make.ARTIFACT_ID, + [RelativeProjectPath(pathlib.Path("workflows"))], + ) + + def test_rejects_workflow_dir_itself_and_other_dirs(self) -> None: + assert not artifacts._artifact_is_in_workflow_dirs( + ArtifactId("@/workflows.donna.md"), + [RelativeProjectPath(pathlib.Path("workflows"))], + ) + assert not artifacts._artifact_is_in_workflow_dirs( + make.ARTIFACT_ID, + [RelativeProjectPath(pathlib.Path("other"))], + ) + + +class TestArtifactIsVisibleInWorkspace: + def test_uses_configured_workflow_dirs(self, mocker: object) -> None: + mocker.patch("donna.workspaces.config.config", return_value=Config(workflow_dirs=["workflows"])) + + assert artifacts._artifact_is_visible_in_workspace(make.ARTIFACT_ID) + + +class TestArtifactIdFromFilesystemEntry: + def test_returns_artifact_id_for_regular_donna_file(self, tmp_path: pathlib.Path) -> None: + path = tmp_path / "test.donna.md" + path.write_text("", encoding="utf-8") + + assert ( + artifacts._artifact_id_from_filesystem_entry(ResolvedProjectPath(path), ["workflows"]) == make.ARTIFACT_ID + ) + + def test_returns_none_for_dirs_non_artifacts_and_invalid_names(self, tmp_path: pathlib.Path) -> None: + directory = tmp_path / "directory" + directory.mkdir() + ordinary_markdown = tmp_path / "ordinary.md" + ordinary_markdown.write_text("", encoding="utf-8") + invalid_name = tmp_path / "invalid name.donna.md" + invalid_name.write_text("", encoding="utf-8") + + assert artifacts._artifact_id_from_filesystem_entry(ResolvedProjectPath(directory), ["workflows"]) is None + assert ( + artifacts._artifact_id_from_filesystem_entry(ResolvedProjectPath(ordinary_markdown), ["workflows"]) is None + ) + assert artifacts._artifact_id_from_filesystem_entry(ResolvedProjectPath(invalid_name), ["workflows"]) is None + + +class TestWalkWorkflowDir: + def test_walks_directory_recursively_in_name_order(self, tmp_path: pathlib.Path) -> None: + nested = tmp_path / "nested" + nested.mkdir() + (tmp_path / "b.donna.md").write_text("", encoding="utf-8") + (nested / "a.donna.md").write_text("", encoding="utf-8") + + assert list(artifacts._walk_workflow_dir(ResolvedProjectPath(tmp_path), ["workflows"])) == [ + ArtifactId("@/workflows/b.donna.md"), + ArtifactId("@/workflows/nested/a.donna.md"), + ] + + +class TestWalkFilesystem: + def test_walk_filesystem__lists_artifacts_in_workflow_dirs(self, mocker: object, tmp_path: pathlib.Path) -> None: + workflows = tmp_path / "workflows" + nested = workflows / "nested" + nested.mkdir(parents=True) + (workflows / "b.donna.md").write_text("", encoding="utf-8") + (workflows / "ignored.md").write_text("", encoding="utf-8") + (workflows / "invalid name.donna.md").write_text("", encoding="utf-8") + (nested / "a.donna.md").write_text("", encoding="utf-8") + mocker.patch("donna.workspaces.config.project_dir", return_value=tmp_path) + + assert list(artifacts.walk_filesystem([RelativeProjectPath(pathlib.Path("workflows"))])) == [ + ArtifactId("@/workflows/b.donna.md"), + ArtifactId("@/workflows/nested/a.donna.md"), + ] + + def test_walk_filesystem__preserves_workflow_dir_order(self, mocker: object, tmp_path: pathlib.Path) -> None: + first = tmp_path / "first" + second = tmp_path / "second" + first.mkdir() + second.mkdir() + (first / "b.donna.md").write_text("", encoding="utf-8") + (second / "a.donna.md").write_text("", encoding="utf-8") + mocker.patch("donna.workspaces.config.project_dir", return_value=tmp_path) + + assert list( + artifacts.walk_filesystem( + [ + RelativeProjectPath(pathlib.Path("second")), + RelativeProjectPath(pathlib.Path("first")), + ] + ) + ) == [ + ArtifactId("@/second/a.donna.md"), + ArtifactId("@/first/b.donna.md"), + ] + + def test_walk_filesystem__ignores_missing_workflow_dirs(self, mocker: object, tmp_path: pathlib.Path) -> None: + mocker.patch("donna.workspaces.config.project_dir", return_value=tmp_path) + + assert list(artifacts.walk_filesystem([RelativeProjectPath(pathlib.Path("missing"))])) == [] + + +class TestListArtifactIds: + def test_list_artifact_ids__deduplicates_discovered_artifacts(self, mocker: object) -> None: + config = Config(workflow_dirs=["workflows"]) + mocker.patch("donna.workspaces.config.config", return_value=config) + mocker.patch.object( + artifacts, + "walk_filesystem", + return_value=iter([make.ARTIFACT_ID, make.ARTIFACT_ID, ArtifactId("@/workflows/other.donna.md")]), + ) + + assert artifacts.list_artifact_ids() == [make.ARTIFACT_ID, ArtifactId("@/workflows/other.donna.md")] + + +class TestResolveArtifactPath: + def test_resolve_artifact_path__returns_existing_visible_file( + self, mocker: object, tmp_path: pathlib.Path + ) -> None: + path = tmp_path / "workflows" / "test.donna.md" + path.parent.mkdir() + path.write_text("", encoding="utf-8") + mocker.patch("donna.workspaces.config.project_dir", return_value=tmp_path) + + result = artifacts.resolve_artifact_path(make.ARTIFACT_ID) + + assert result.is_ok() + assert result.unwrap() == path + + def test_resolve_artifact_path__returns_none_for_missing_file( + self, mocker: object, tmp_path: pathlib.Path + ) -> None: + mocker.patch("donna.workspaces.config.project_dir", return_value=tmp_path) + + result = artifacts.resolve_artifact_path(make.ARTIFACT_ID) + + assert result.is_ok() + assert result.unwrap() is None + + +class TestFilesystemRawArtifact: + def test_get_bytes__returns_file_bytes(self, tmp_path: pathlib.Path) -> None: + path = tmp_path / "workflow.donna.md" + path.write_bytes(b"content") + raw_artifact = artifacts.FilesystemRawArtifact(path=ResolvedProjectPath(path)) + + assert raw_artifact.get_bytes() == b"content" + + def test_render__renders_markdown_from_file_bytes(self, mocker: object, tmp_path: pathlib.Path) -> None: + path = tmp_path / "workflow.donna.md" + path.write_bytes(b"# Workflow") + expected_artifact = Artifact(id=make.ARTIFACT_ID, sections=[]) + render_markdown_artifact = mocker.patch.object( + artifacts, + "render_markdown_artifact", + return_value=Ok(expected_artifact), + ) + raw_artifact = artifacts.FilesystemRawArtifact(path=ResolvedProjectPath(path)) + + result = raw_artifact.render(make.ARTIFACT_ID, artifacts.RENDER_CONTEXT_VIEW) + + assert result.is_ok() + assert result.unwrap() == expected_artifact + render_markdown_artifact.assert_called_once_with( + make.ARTIFACT_ID, + b"# Workflow", + artifacts.RENDER_CONTEXT_VIEW, + ) + + def test_render__returns_markdown_render_errors(self, mocker: object, tmp_path: pathlib.Path) -> None: + path = tmp_path / "workflow.donna.md" + path.write_bytes(b"# Workflow") + error = workspace_errors.MarkdownArtifactWithoutSections(artifact_id=make.ARTIFACT_ID) + mocker.patch.object(artifacts, "render_markdown_artifact", return_value=Err([error])) + raw_artifact = artifacts.FilesystemRawArtifact(path=ResolvedProjectPath(path)) + + result = raw_artifact.render(make.ARTIFACT_ID, artifacts.RENDER_CONTEXT_VIEW) + + assert result.is_err() + assert result.unwrap_err() == [error] + + +class TestFetchRawArtifact: + def test_fetch_raw_artifact__returns_filesystem_artifact(self, mocker: object, tmp_path: pathlib.Path) -> None: + path = tmp_path / "workflows" / "test.donna.md" + path.parent.mkdir() + path.write_text("content", encoding="utf-8") + mocker.patch("donna.workspaces.config.project_dir", return_value=tmp_path) + mocker.patch("donna.workspaces.config.config", return_value=Config(workflow_dirs=["workflows"])) + + result = artifacts.fetch_raw_artifact(make.ARTIFACT_ID) + + assert result.is_ok() + assert result.unwrap().get_bytes() == b"content" + + def test_fetch_raw_artifact__rejects_artifacts_outside_workflow_dirs( + self, mocker: object, tmp_path: pathlib.Path + ) -> None: + mocker.patch("donna.workspaces.config.config", return_value=Config(workflow_dirs=["other"])) + + result = artifacts.fetch_raw_artifact(make.ARTIFACT_ID) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.ArtifactNotFound) + + def test_fetch_raw_artifact__rejects_unsupported_artifact_extension( + self, mocker: object, tmp_path: pathlib.Path + ) -> None: + artifact_id = ArtifactId("@/workflows/test.md") + path = tmp_path / "workflows" / "test.md" + path.parent.mkdir() + path.write_text("content", encoding="utf-8") + mocker.patch("donna.workspaces.config.project_dir", return_value=tmp_path) + mocker.patch("donna.workspaces.config.config", return_value=Config(workflow_dirs=["workflows"])) + + result = artifacts.fetch_raw_artifact(artifact_id) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, workspace_errors.UnsupportedArtifactExtension) + assert error.extension == ".md" + + +class TestFetchArtifactBytes: + def test_fetch_artifact_bytes__returns_raw_bytes(self, mocker: object) -> None: + raw_artifact = mocker.Mock() + raw_artifact.get_bytes.return_value = b"content" + mocker.patch.object(artifacts, "fetch_raw_artifact", return_value=Ok(raw_artifact)) + + result = artifacts.fetch_artifact_bytes(make.ARTIFACT_ID) + + assert result.is_ok() + assert result.unwrap() == b"content" + + +class TestRenderMarkdownArtifact: + def test_render_markdown_artifact__uses_workspace_defaults(self, mocker: object) -> None: + expected_artifact = Artifact(id=make.ARTIFACT_ID, sections=[]) + mocker.patch("donna.workspaces.config.config", return_value=Config()) + construct = mocker.patch( + "donna.workspaces.markdown_parser.construct_artifact_from_bytes", + return_value=Ok(expected_artifact), + ) + + result = artifacts.render_markdown_artifact(make.ARTIFACT_ID, b"# Workflow", artifacts.RENDER_CONTEXT_VIEW) + + assert result.is_ok() + assert result.unwrap() == expected_artifact + construct.assert_called_once_with( + make.ARTIFACT_ID, + b"# Workflow", + artifacts.RENDER_CONTEXT_VIEW, + default_section_kind=Config().defaults.tail_section_kind, + default_primary_section_kind=Config().defaults.primary_section_kind, + default_primary_section_id=Config().defaults.primary_section_id, + ) + + +class TestArtifactFingerprint: + def test_artifact_fingerprint__returns_file_fingerprint(self, mocker: object, tmp_path: pathlib.Path) -> None: + path = tmp_path / "workflow.donna.md" + path.write_text("data", encoding="utf-8") + mocker.patch.object(artifacts, "resolve_artifact_path", return_value=Ok(path)) + + result = artifacts.artifact_fingerprint(make.ARTIFACT_ID) + + assert result.is_ok() + assert result.unwrap() == FileFingerprint.from_path(path) + + def test_artifact_fingerprint__returns_none_for_missing_artifact(self, mocker: object) -> None: + mocker.patch.object(artifacts, "resolve_artifact_path", return_value=Ok(None)) + + result = artifacts.artifact_fingerprint(make.ARTIFACT_ID) + + assert result.is_ok() + assert result.unwrap() is None diff --git a/donna/workspaces/tests/test_config.py b/donna/workspaces/tests/test_config.py new file mode 100644 index 00000000..8730f25a --- /dev/null +++ b/donna/workspaces/tests/test_config.py @@ -0,0 +1,201 @@ +import pathlib + +import pydantic +import pytest + +from donna.domain.id_paths import NormalizedRawIdPath +from donna.domain.paths import ProjectConfigPath, ProjectRootPath, RelativeProjectPath +from donna.domain.python_path import PythonPath +from donna.workspaces import config as workspace_config +from donna.workspaces import errors as workspace_errors +from donna.workspaces.config import Config, DefaultsConfig, GlobalConfig, JournalConfig, JournalRecordAttribute +from donna.workspaces.tests import make + + +class TestJournalRecordAttribute: + def test_has_attribute__accepts_supported_attribute_names(self) -> None: + assert JournalRecordAttribute.has_attribute("message") + assert not JournalRecordAttribute.has_attribute("missing") + + +class TestIsJournalVariableArgument: + def test_detects_whole_argument_placeholders(self) -> None: + assert workspace_config._is_journal_variable_argument("{message}") + assert not workspace_config._is_journal_variable_argument("literal:{message}") + assert not workspace_config._is_journal_variable_argument("{message") + + +class TestJournalConfig: + def test_validate_cmd__accepts_none_and_supported_placeholders(self) -> None: + assert JournalConfig(cmd=None).cmd is None + assert JournalConfig(cmd=["tool", "{message}", "literal:{message}"]).cmd == [ + "tool", + "{message}", + "literal:{message}", + ] + + @pytest.mark.parametrize("cmd", [[], ["tool", "{missing}"], ["tool", 1]]) + def test_validate_cmd__rejects_invalid_command(self, cmd: list[object]) -> None: + with pytest.raises(pydantic.ValidationError): + JournalConfig(cmd=cmd) + + +class TestDefaultsConfig: + def test_defaults__match_configuration_spec(self) -> None: + defaults = DefaultsConfig() + + assert defaults.tail_section_kind == PythonPath(NormalizedRawIdPath("donna.lib.text")) + assert defaults.primary_section_kind == PythonPath(NormalizedRawIdPath("donna.lib.workflow")) + assert defaults.primary_section_id == "primary" + + @pytest.mark.parametrize( + "data", + [ + {"tail_section_kind": "not a python path"}, + {"primary_section_kind": "not a python path"}, + {"primary_section_id": "---"}, + ], + ) + def test_validation__rejects_invalid_defaults(self, data: dict[str, str]) -> None: + with pytest.raises(pydantic.ValidationError): + DefaultsConfig.model_validate(data) + + +class TestDefaultWorkflowDirs: + def test_returns_spec_defaults(self) -> None: + assert workspace_config._default_workflow_dirs() == [ + pathlib.Path("workflows"), + pathlib.Path(".session/donna"), + ] + + +class TestSerializeWorkflowDir: + def test_serializes_as_project_relative_string(self) -> None: + assert ( + workspace_config._serialize_workflow_dir(RelativeProjectPath(pathlib.Path("workflows"))) == "./workflows" + ) + + +class TestValidateRelativeProjectPath: + def test_returns_relative_project_path(self) -> None: + path = RelativeProjectPath(pathlib.Path("workflows")) + + assert workspace_config._validate_relative_project_path(path) == path + + @pytest.mark.parametrize("path", [pathlib.Path("/outside"), pathlib.Path("../outside")]) + def test_rejects_invalid_project_paths(self, path: pathlib.Path) -> None: + with pytest.raises(ValueError): + workspace_config._validate_relative_project_path(RelativeProjectPath(path)) + + +class TestConfig: + def test_defaults__match_configuration_spec(self) -> None: + config = Config() + + assert config.version == 1 + assert config.session_dir == pathlib.Path(".session/donna") + assert config.workflow_dirs == [pathlib.Path("workflows"), pathlib.Path(".session/donna")] + assert config.journal == JournalConfig() + + def test_validate_workflow_dirs__deduplicates_preserving_order(self) -> None: + config = Config(workflow_dirs=["workflows", "./workflows", ".session/donna"]) + + assert config.workflow_dirs == [pathlib.Path("workflows"), pathlib.Path(".session/donna")] + + @pytest.mark.parametrize("field", ["session_dir", "workflow_dirs"]) + def test_validation__rejects_absolute_project_paths(self, field: str) -> None: + value = pathlib.Path("/outside") + data = {field: [value] if field == "workflow_dirs" else value} + + with pytest.raises(pydantic.ValidationError): + Config.model_validate(data) + + @pytest.mark.parametrize("field", ["session_dir", "workflow_dirs"]) + def test_validation__rejects_parent_directory_references(self, field: str) -> None: + value = pathlib.Path("../outside") + data = {field: [value] if field == "workflow_dirs" else value} + + with pytest.raises(pydantic.ValidationError): + Config.model_validate(data) + + @pytest.mark.parametrize( + "data", + [ + {"unknown": True}, + {"defaults": {"unknown": True}}, + {"journal": {"unknown": True}}, + ], + ) + def test_validation__rejects_unknown_fields(self, data: dict[str, object]) -> None: + with pytest.raises(pydantic.ValidationError): + Config.model_validate(data) + + def test_model_dump__serializes_workflow_dirs_as_project_relative_strings(self) -> None: + config = Config(workflow_dirs=["workflows", ".session/donna"]) + + assert config.model_dump(mode="json")["workflow_dirs"] == ["./workflows", "./.session/donna"] + + +class TestGlobalConfig: + def test_get__raises_when_value_is_not_set(self) -> None: + global_config = GlobalConfig[str]() + + with pytest.raises(workspace_errors.GlobalConfigNotSet): + global_config.get() + + def test_set__stores_single_value(self) -> None: + global_config = GlobalConfig[str]() + + global_config.set("value") + + assert global_config.get() == "value" + assert global_config() + assert global_config.is_set() + + with pytest.raises(workspace_errors.GlobalConfigAlreadySet): + global_config.set("other") + + +class TestWorkspace: + def test_builds_validated_workspace_entity(self, tmp_path: pathlib.Path) -> None: + workspace = make.workspace(tmp_path) + + assert workspace.root == ProjectRootPath(tmp_path) + assert workspace.config_path == ProjectConfigPath(tmp_path / "donna.toml") + assert workspace.config == Config() + + +class TestInstallWorkspace: + def test_install_workspace__sets_unset_globals(self, mocker: object, tmp_path: pathlib.Path) -> None: + project_dir = GlobalConfig[ProjectRootPath]() + config_path = GlobalConfig[ProjectConfigPath]() + config = GlobalConfig[Config]() + mocker.patch.object(workspace_config, "project_dir", project_dir) + mocker.patch.object(workspace_config, "config_path", config_path) + mocker.patch.object(workspace_config, "config", config) + workspace = make.workspace(tmp_path) + + workspace_config.install_workspace(workspace) + + assert project_dir.get() == ProjectRootPath(tmp_path) + assert config_path.get() == ProjectConfigPath(tmp_path / "donna.toml") + assert config.get() == workspace.config + + def test_install_workspace__does_not_replace_existing_globals( + self, mocker: object, tmp_path: pathlib.Path + ) -> None: + project_dir = GlobalConfig[ProjectRootPath]() + config_path = GlobalConfig[ProjectConfigPath]() + config = GlobalConfig[Config]() + project_dir.set(ProjectRootPath(tmp_path / "existing")) + config_path.set(ProjectConfigPath(tmp_path / "existing.toml")) + config.set(Config(session_dir="custom")) + mocker.patch.object(workspace_config, "project_dir", project_dir) + mocker.patch.object(workspace_config, "config_path", config_path) + mocker.patch.object(workspace_config, "config", config) + + workspace_config.install_workspace(make.workspace(tmp_path)) + + assert project_dir.get() == ProjectRootPath(tmp_path / "existing") + assert config_path.get() == ProjectConfigPath(tmp_path / "existing.toml") + assert config.get().session_dir == pathlib.Path("custom") diff --git a/donna/workspaces/tests/test_errors.py b/donna/workspaces/tests/test_errors.py new file mode 100644 index 00000000..a85407dc --- /dev/null +++ b/donna/workspaces/tests/test_errors.py @@ -0,0 +1,204 @@ +import pathlib + +from donna.workspaces import errors as workspace_errors +from donna.workspaces.tests import make + + +class TestWorkspaceConfigError: + def test_content_intro__includes_config_path(self, tmp_path: pathlib.Path) -> None: + error = workspace_errors.ConfigParseFailed(config_path=tmp_path / "donna.toml", details="bad") + + assert error.content_intro() == f"Error in Donna config file '{tmp_path / 'donna.toml'}'" + + +class TestInternalError: + def test_error_message__uses_workspace_internal_error_base(self) -> None: + assert workspace_errors.InternalError().error_message() == "An internal error occurred" + + +class TestWorkspaceError: + def test_cell_kind__uses_workspace_boundary(self) -> None: + assert workspace_errors.WorkspaceError.model_fields["cell_kind"].default == "workspace_error" + + +class _EnvironmentErrorCase: + def error(self) -> workspace_errors.WorkspaceError: + raise NotImplementedError + + def test_model_dump__includes_structured_context(self) -> None: + error = self.error() + + assert error.model_dump(mode="json") + + +class _InternalErrorCase: + def error(self) -> workspace_errors.InternalError: + raise NotImplementedError + + def test_error_message__formats_without_failure(self) -> None: + assert self.error().error_message() + + +class TestConfigParseFailed(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.ConfigParseFailed(config_path=pathlib.Path("donna.toml"), details="bad") + + +class TestConfigValidationFailed(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.ConfigValidationFailed(config_path=pathlib.Path("donna.toml"), details="bad") + + +class TestWorkspaceConfigNotDiscovered(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.WorkspaceConfigNotDiscovered(config_name="donna.toml") + + +class TestWorkspaceAlreadyInitialized(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.WorkspaceAlreadyInitialized(config_path=pathlib.Path("donna.toml")) + + +class TestWorkspaceConfigNotFound(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.WorkspaceConfigNotFound(config_path=pathlib.Path("donna.toml")) + + +class TestWorkspaceConfigDirNotFound(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.WorkspaceConfigDirNotFound(config_path=pathlib.Path("donna.toml")) + + +class TestJournalCommandConfigInvalid(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.JournalCommandConfigInvalid(argument="{missing}", details="bad") + + +class TestJournalCommandFailed(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.JournalCommandFailed(command=["tool"], returncode=1, details="bad") + + +class TestArtifactError: + def test_content_intro__includes_artifact_id(self) -> None: + error = workspace_errors.ArtifactNotFound(artifact_id=make.ARTIFACT_ID) + + assert error.content_intro() == "Error for artifact '@/workflows/test.donna.md'" + + +class TestArtifactNotFound(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.ArtifactNotFound(artifact_id=make.ARTIFACT_ID) + + +class TestArtifactMultipleFiles(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.ArtifactMultipleFiles(artifact_id=make.ARTIFACT_ID) + + +class TestUnsupportedArtifactExtension(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.UnsupportedArtifactExtension(artifact_id=make.ARTIFACT_ID, extension=".md") + + +class TestMarkdownError: + def test_content_intro__describes_source_without_artifact_id(self) -> None: + error = workspace_errors.MarkdownArtifactWithoutSections() + + assert error.content_intro() == "Error in markdown source" + + def test_content_intro__describes_artifact_when_artifact_id_is_set(self) -> None: + error = workspace_errors.MarkdownArtifactWithoutSections(artifact_id=make.ARTIFACT_ID) + + assert error.content_intro() == "Error in markdown artifact '@/workflows/test.donna.md'" + + +class TestMarkdownUnsupportedCodeFormat(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.MarkdownUnsupportedCodeFormat(format="ini") + + +class TestMarkdownMultipleH1Sections(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.MarkdownMultipleH1Sections(artifact_id=make.ARTIFACT_ID) + + +class TestMarkdownH1SectionMustBeFirst(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.MarkdownH1SectionMustBeFirst(artifact_id=make.ARTIFACT_ID) + + +class TestMarkdownArtifactWithoutSections(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.MarkdownArtifactWithoutSections(artifact_id=make.ARTIFACT_ID) + + +class TestMarkdownMultipleConfigBlocksInSection(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.MarkdownMultipleConfigBlocksInSection(section_title="Section") + + +class TestMarkdownMultipleScriptBlocksInSection(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.MarkdownMultipleScriptBlocksInSection(section_title="Section") + + +class TestPrimitiveDoesNotSupportMarkdown(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.PrimitiveDoesNotSupportMarkdown(primitive_id="kind") + + +class TestTemplateDirectiveError: + def test_content_intro__describes_directive_without_artifact_id(self) -> None: + error = workspace_errors.DirectivePathIncomplete(path="directive") + + assert error.content_intro() == "Error in template directive" + + def test_content_intro__describes_directive_with_artifact_id(self) -> None: + error = workspace_errors.DirectivePathIncomplete(path="directive", artifact_id=make.ARTIFACT_ID) + + assert error.content_intro() == "Error in template directive for artifact '@/workflows/test.donna.md'" + + +class TestDirectivePathIncomplete(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.DirectivePathIncomplete(path="directive") + + +class TestDirectiveModuleNotImportable(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.DirectiveModuleNotImportable(module_path="missing") + + +class TestDirectiveNotAvailable(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.DirectiveNotAvailable(module_path="module", directive_name="missing") + + +class TestDirectiveNotDirective(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.DirectiveNotDirective(module_path="module", directive_name="name") + + +class TestDirectiveUnexpectedError(_EnvironmentErrorCase): + def error(self) -> workspace_errors.WorkspaceError: + return workspace_errors.DirectiveUnexpectedError(directive_path="module.name", details="bad") + + +class TestMarkdownSectionsCountMismatch(_InternalErrorCase): + def error(self) -> workspace_errors.InternalError: + return workspace_errors.MarkdownSectionsCountMismatch( + artifact_id=make.ARTIFACT_ID, + original_count=1, + analyzed_count=2, + ) + + +class TestGlobalConfigAlreadySet(_InternalErrorCase): + def error(self) -> workspace_errors.InternalError: + return workspace_errors.GlobalConfigAlreadySet() + + +class TestGlobalConfigNotSet(_InternalErrorCase): + def error(self) -> workspace_errors.InternalError: + return workspace_errors.GlobalConfigNotSet() diff --git a/donna/workspaces/tests/test_files.py b/donna/workspaces/tests/test_files.py new file mode 100644 index 00000000..e5f69f34 --- /dev/null +++ b/donna/workspaces/tests/test_files.py @@ -0,0 +1,24 @@ +import pathlib + +from donna.workspaces.files import FileFingerprint + + +class TestFileFingerprint: + def test_from_path__returns_fingerprint_for_regular_file(self, tmp_path: pathlib.Path) -> None: + path = tmp_path / "file.txt" + path.write_text("data", encoding="utf-8") + + fingerprint = FileFingerprint.from_path(path) + + assert fingerprint is not None + assert fingerprint.size == 4 + assert fingerprint.mtime_ns == path.stat().st_mtime_ns + + def test_from_path__returns_none_for_missing_path_or_directory(self, tmp_path: pathlib.Path) -> None: + assert FileFingerprint.from_path(tmp_path / "missing.txt") is None + assert FileFingerprint.from_path(tmp_path) is None + + def test_eq__compares_mtime_and_size(self) -> None: + assert FileFingerprint(mtime_ns=1, size=2) == FileFingerprint(mtime_ns=1, size=2) + assert FileFingerprint(mtime_ns=1, size=2) != FileFingerprint(mtime_ns=2, size=2) + assert FileFingerprint(mtime_ns=1, size=2) != object() diff --git a/donna/workspaces/tests/test_initialization.py b/donna/workspaces/tests/test_initialization.py new file mode 100644 index 00000000..b0aa3f5b --- /dev/null +++ b/donna/workspaces/tests/test_initialization.py @@ -0,0 +1,134 @@ +import pathlib + +from donna.core.result import Ok +from donna.domain.constants import DONNA_CONFIG_NAME +from donna.workspaces import errors as workspace_errors +from donna.protocol.modes import Mode +from donna.workspaces import config as workspace_config +from donna.workspaces.config import GlobalConfig +from donna.workspaces.initialization import initialize_runtime, initialize_workspace, load_workspace + + +class TestLoadWorkspace: + def test_explicit_config_path__loads_workspace(self, tmp_path: pathlib.Path) -> None: + config_path = tmp_path / DONNA_CONFIG_NAME + config_path.write_text( + 'version = 1\nsession_dir = ".session/custom"\nworkflow_dirs = ["workflows", "workflows"]\n', + encoding="utf-8", + ) + + result = load_workspace(config_path=config_path) + + assert result.is_ok() + workspace = result.unwrap() + assert workspace.root == tmp_path + assert workspace.config_path == config_path + assert workspace.config.session_dir == pathlib.Path(".session/custom") + assert workspace.config.workflow_dirs == [pathlib.Path("workflows")] + + def test_discovery__uses_nearest_project_config(self, mocker: object, tmp_path: pathlib.Path) -> None: + config_path = tmp_path / DONNA_CONFIG_NAME + config_path.write_text("version = 1", encoding="utf-8") + mocker.patch("donna.workspaces.utils.discover_project_dir", return_value=Ok(tmp_path)) + + result = load_workspace() + + assert result.is_ok() + assert result.unwrap().root == tmp_path + + def test_missing_explicit_config__returns_not_found_error(self, tmp_path: pathlib.Path) -> None: + result = load_workspace(config_path=tmp_path / DONNA_CONFIG_NAME) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.WorkspaceConfigNotFound) + + def test_invalid_toml__returns_parse_error(self, tmp_path: pathlib.Path) -> None: + config_path = tmp_path / DONNA_CONFIG_NAME + config_path.write_text("version = ", encoding="utf-8") + + result = load_workspace(config_path=config_path) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.ConfigParseFailed) + + def test_invalid_config_schema__returns_validation_error(self, tmp_path: pathlib.Path) -> None: + config_path = tmp_path / DONNA_CONFIG_NAME + config_path.write_text("version = 2", encoding="utf-8") + + result = load_workspace(config_path=config_path) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.ConfigValidationFailed) + + def test_unknown_config_fields__return_validation_error(self, tmp_path: pathlib.Path) -> None: + config_path = tmp_path / DONNA_CONFIG_NAME + config_path.write_text("[defaults]\nunknown = true\n", encoding="utf-8") + + result = load_workspace(config_path=config_path) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.ConfigValidationFailed) + + +class TestInitializeRuntime: + def test_loads_workspace_installs_protocol_and_workspace(self, mocker: object, tmp_path: pathlib.Path) -> None: + protocol = GlobalConfig[Mode]() + mocker.patch.object(workspace_config, "protocol", protocol) + install_workspace = mocker.patch("donna.workspaces.config.install_workspace") + config_path = tmp_path / DONNA_CONFIG_NAME + config_path.write_text("version = 1", encoding="utf-8") + + result = initialize_runtime(config_path=config_path, protocol=Mode.llm) + + assert result.is_ok() + workspace = result.unwrap() + assert protocol.get() == Mode.llm + install_workspace.assert_called_once_with(workspace) + + def test_loads_workspace_without_protocol_override(self, mocker: object, tmp_path: pathlib.Path) -> None: + protocol = GlobalConfig[Mode]() + mocker.patch.object(workspace_config, "protocol", protocol) + config_path = tmp_path / DONNA_CONFIG_NAME + config_path.write_text("version = 1", encoding="utf-8") + + result = initialize_runtime(config_path=config_path) + + assert result.is_ok() + assert not protocol.is_set() + + +class TestInitializeWorkspace: + def test_creates_starter_config_and_loads_workspace(self, mocker: object, tmp_path: pathlib.Path) -> None: + install_workspace = mocker.patch("donna.workspaces.config.install_workspace") + config_path = tmp_path / DONNA_CONFIG_NAME + + result = initialize_workspace(config_path) + + assert result.is_ok() + assert config_path.is_file() + config_text = config_path.read_text(encoding="utf-8") + assert "version = 1" in config_text + assert 'session_dir = ".session/donna"' in config_text + assert '"./workflows"' in config_text + assert '"./.session/donna"' in config_text + assert "# [defaults]" in config_text + assert "# [journal]" in config_text + assert "# cmd = [" in config_text + workspace = result.unwrap() + assert workspace.root == tmp_path + install_workspace.assert_called_once_with(workspace) + + def test_rejects_missing_config_directory(self, tmp_path: pathlib.Path) -> None: + result = initialize_workspace(tmp_path / "missing" / DONNA_CONFIG_NAME) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.WorkspaceConfigDirNotFound) + + def test_rejects_existing_config(self, tmp_path: pathlib.Path) -> None: + config_path = tmp_path / DONNA_CONFIG_NAME + config_path.write_text("version = 1", encoding="utf-8") + + result = initialize_workspace(config_path) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.WorkspaceAlreadyInitialized) diff --git a/donna/workspaces/tests/test_journal.py b/donna/workspaces/tests/test_journal.py new file mode 100644 index 00000000..97ef2eae --- /dev/null +++ b/donna/workspaces/tests/test_journal.py @@ -0,0 +1,191 @@ +import subprocess # noqa: S404 + +from donna.workspaces import errors as workspace_errors +from donna.workspaces import journal +from donna.workspaces.config import Config, JournalConfig, JournalRecordAttribute +from donna.workspaces.tests import make + + +class TestIsVariableArgument: + def test_detects_whole_argument_placeholders(self) -> None: + assert journal._is_variable_argument("{message}") + assert not journal._is_variable_argument("literal:{message}") + assert not journal._is_variable_argument("{message") + + +class TestParseRecordAttribute: + def test_returns_supported_attribute(self) -> None: + result = journal._parse_record_attribute("message", "{message}") + + assert result.is_ok() + assert result.unwrap() == JournalRecordAttribute.message + + def test_reports_unsupported_attribute(self) -> None: + result = journal._parse_record_attribute("missing", "{missing}") + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, workspace_errors.JournalCommandConfigInvalid) + assert error.argument == "{missing}" + + +class TestFormatRecordAttribute: + def test_formats_record_attributes(self) -> None: + record = make.journal_record() + + assert ( + journal._format_record_attribute(JournalRecordAttribute.timestamp, record) == "2026-05-18T10:30:00+00:00" + ) + assert journal._format_record_attribute(JournalRecordAttribute.actor_id, record) == "agent" + assert journal._format_record_attribute(JournalRecordAttribute.message, record) == "message" + assert journal._format_record_attribute(JournalRecordAttribute.current_task_id, record) == "T-1-b" + assert journal._format_record_attribute(JournalRecordAttribute.current_work_unit_id, record) == "WU-2-c" + assert ( + journal._format_record_attribute(JournalRecordAttribute.current_operation_id, record) + == "@/workflows/test.donna.md:primary" + ) + + def test_formats_missing_optional_attributes_as_empty_strings(self) -> None: + record = make.journal_record( + actor_id=None, + current_task_id=None, + current_work_unit_id=None, + current_operation_id=None, + ) + + assert journal._format_record_attribute(JournalRecordAttribute.actor_id, record) == "" + assert journal._format_record_attribute(JournalRecordAttribute.current_task_id, record) == "" + assert journal._format_record_attribute(JournalRecordAttribute.current_work_unit_id, record) == "" + assert journal._format_record_attribute(JournalRecordAttribute.current_operation_id, record) == "" + + +class TestResolveCommandArgument: + def test_returns_literal_argument(self) -> None: + result = journal._resolve_command_argument("literal:{message}", make.journal_record()) + + assert result.is_ok() + assert result.unwrap() == "literal:{message}" + + def test_resolves_placeholder_argument(self) -> None: + result = journal._resolve_command_argument("{message}", make.journal_record()) + + assert result.is_ok() + assert result.unwrap() == "message" + + +class TestBuildCommandArgs: + def test_replaces_whole_argument_placeholders(self) -> None: + record = make.journal_record() + + result = journal._build_command_args( + [ + "tool", + "{timestamp}", + "{actor_id}", + "{current_task_id}", + "{current_work_unit_id}", + "{current_operation_id}", + "{message}", + "literal:{message}", + ], + record, + ) + + assert result.is_ok() + assert result.unwrap() == [ + "tool", + "2026-05-18T10:30:00+00:00", + "agent", + "T-1-b", + "WU-2-c", + "@/workflows/test.donna.md:primary", + "message", + "literal:{message}", + ] + + def test_replaces_missing_optional_record_values_with_empty_strings(self) -> None: + record = make.journal_record( + actor_id=None, + current_task_id=None, + current_work_unit_id=None, + current_operation_id=None, + ) + + result = journal._build_command_args( + ["{actor_id}", "{current_task_id}", "{current_work_unit_id}", "{current_operation_id}"], + record, + ) + + assert result.is_ok() + assert result.unwrap() == ["", "", "", ""] + + def test_unsupported_placeholder_returns_config_error(self) -> None: + result = journal._build_command_args(["{missing}"], make.journal_record()) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, workspace_errors.JournalCommandConfigInvalid) + assert error.argument == "{missing}" + + +class TestWriteRecord: + def test_no_configured_command_is_noop(self, mocker: object) -> None: + mocker.patch( + "donna.workspaces.config.config", + return_value=Config(journal=JournalConfig(cmd=None)), + ) + run = mocker.patch("subprocess.run") + + result = journal.write_record(make.journal_record()) + + assert result.is_ok() + run.assert_not_called() + + def test_runs_configured_command(self, mocker: object) -> None: + mocker.patch( + "donna.workspaces.config.config", + return_value=Config(journal=JournalConfig(cmd=["tool", "{message}"])), + ) + run = mocker.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess(args=["tool", "message"], returncode=0, stdout="", stderr=""), + ) + + result = journal.write_record(make.journal_record()) + + assert result.is_ok() + run.assert_called_once_with(["tool", "message"], check=False, capture_output=True, text=True) + + def test_reports_command_os_error(self, mocker: object) -> None: + mocker.patch( + "donna.workspaces.config.config", + return_value=Config(journal=JournalConfig(cmd=["tool"])), + ) + mocker.patch("subprocess.run", side_effect=OSError("missing")) + + result = journal.write_record(make.journal_record()) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, workspace_errors.JournalCommandFailed) + assert error.command == ["tool"] + assert error.returncode is None + + def test_reports_nonzero_command_exit(self, mocker: object) -> None: + mocker.patch( + "donna.workspaces.config.config", + return_value=Config(journal=JournalConfig(cmd=["tool"])), + ) + mocker.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess(args=["tool"], returncode=2, stdout="", stderr="bad"), + ) + + result = journal.write_record(make.journal_record()) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, workspace_errors.JournalCommandFailed) + assert error.command == ["tool"] + assert error.returncode == 2 + assert error.details == "exit code 2; stderr: bad" diff --git a/donna/workspaces/tests/test_markdown.py b/donna/workspaces/tests/test_markdown.py new file mode 100644 index 00000000..74f2aae6 --- /dev/null +++ b/donna/workspaces/tests/test_markdown.py @@ -0,0 +1,215 @@ +from markdown_it import MarkdownIt + +from donna.workspaces import errors as workspace_errors +from donna.workspaces import markdown +from donna.workspaces.markdown import SectionLevel +from donna.workspaces.tests import make + + +class TestSectionLevel: + def test_values__match_supported_heading_levels(self) -> None: + assert SectionLevel.h1 == "h1" + assert SectionLevel.h2 == "h2" + + +class TestCodeSource: + def test_structured_data__parses_supported_formats(self) -> None: + assert make.code_source("json", '{"value": 1}').structured_data().unwrap() == {"value": 1} + assert make.code_source("yaml", "value: 1").structured_data().unwrap() == {"value": 1} + assert make.code_source("yml", "value: 1").structured_data().unwrap() == {"value": 1} + assert make.code_source("toml", "value = 1").structured_data().unwrap() == {"value": 1} + + def test_structured_data__script_blocks_return_empty_config(self) -> None: + assert make.code_source("python", "print(1)", script=True).structured_data().unwrap() == {} + + def test_structured_data__rejects_unsupported_format(self) -> None: + result = make.code_source("ini", "value=1", config=True).structured_data() + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, workspace_errors.MarkdownUnsupportedCodeFormat) + assert error.format == "ini" + + +class TestSectionSource: + def test_as_original_markdown__renders_title_and_original_tokens(self) -> None: + tokens = MarkdownIt("commonmark").parse("Body\n") + section = make.section_source(level=SectionLevel.h1, title="Workflow") + section.original_tokens.extend(tokens) + + assert section.as_original_markdown(with_title=True).startswith("# Workflow\n") + assert "Body" in section.as_original_markdown(with_title=False) + + def test_as_analysis_markdown__renders_title_and_analysis_tokens(self) -> None: + tokens = MarkdownIt("commonmark").parse("Analysis\n") + section = make.section_source(level=SectionLevel.h2, title="Step") + section.analysis_tokens.extend(tokens) + + assert section.as_analysis_markdown(with_title=True).startswith("## Step\n") + assert "Analysis" in section.as_analysis_markdown(with_title=False) + + def test_config__returns_empty_dict_without_config_blocks(self) -> None: + assert make.section_source().config().unwrap() == {} + + def test_config__returns_single_config_block_data(self) -> None: + section = make.section_source(configs=[make.code_source("toml", "id = 'section'", config=True)]) + + assert section.config().unwrap() == {"id": "section"} + + def test_config__rejects_multiple_config_blocks(self) -> None: + section = make.section_source( + configs=[ + make.code_source("toml", "id = 'first'", config=True), + make.code_source("toml", "id = 'second'", config=True), + ] + ) + + result = section.config() + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.MarkdownMultipleConfigBlocksInSection) + + def test_script__returns_none_without_script_blocks(self) -> None: + assert make.section_source().script().unwrap() is None + + def test_script__returns_single_script_block_content(self) -> None: + section = make.section_source(configs=[make.code_source("python", "print(1)", script=True)]) + + assert section.script().unwrap() == "print(1)" + + def test_script__rejects_multiple_script_blocks(self) -> None: + section = make.section_source( + configs=[ + make.code_source("python", "print(1)", script=True), + make.code_source("python", "print(2)", script=True), + ] + ) + + result = section.script() + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.MarkdownMultipleScriptBlocksInSection) + + +class TestRenderBack: + def test_renders_tokens_to_markdown(self) -> None: + tokens = MarkdownIt("commonmark").parse("Body\n") + + assert "Body" in markdown.render_back(tokens) + + +class TestClearHeading: + def test_removes_heading_markers_and_surrounding_whitespace(self) -> None: + assert markdown.clear_heading("## Step ") == "Step" + + +class TestParseH1: + def test_parse_h1__creates_primary_section(self) -> None: + result = markdown.parse("# Workflow\n", artifact_id=make.ARTIFACT_ID) + + assert result.is_ok() + section = result.unwrap()[0] + assert section.level == SectionLevel.h1 + assert section.title == "Workflow" + + +class TestParseH2: + def test_parse_h2__creates_tail_section_after_h1(self) -> None: + result = markdown.parse("# Workflow\n\n## Step\n", artifact_id=make.ARTIFACT_ID) + + assert result.is_ok() + section = result.unwrap()[1] + assert section.level == SectionLevel.h2 + assert section.title == "Step" + + +class TestParseHeading: + def test_parse_heading__stores_lower_headings_as_section_content(self) -> None: + result = markdown.parse("# Workflow\n\n### Detail\n", artifact_id=make.ARTIFACT_ID) + + assert result.is_ok() + assert "### Detail" in result.unwrap()[0].as_original_markdown(with_title=False) + + +class TestParseFence: + def test_parse_fence__keeps_non_donna_fences_in_original_tokens(self) -> None: + result = markdown.parse("# Workflow\n\n```python\nprint(1)\n```\n", artifact_id=make.ARTIFACT_ID) + + assert result.is_ok() + section = result.unwrap()[0] + assert section.configs == [] + assert "print(1)" in section.as_original_markdown(with_title=False) + + def test_parse_fence__treats_plain_donna_marker_as_config(self) -> None: + result = markdown.parse("# Workflow\n\n```toml donna\nid = 'primary'\n```\n", artifact_id=make.ARTIFACT_ID) + + assert result.is_ok() + assert result.unwrap()[0].config().unwrap() == {"id": "primary"} + + def test_parse_fence__parses_marker_key_values(self) -> None: + result = markdown.parse( + "# Workflow\n\n```toml donna name=value\nid = 'primary'\n```\n", artifact_id=make.ARTIFACT_ID + ) + + assert result.is_ok() + assert result.unwrap()[0].configs[0].properties["name"] == "value" + + +class TestParseNested: + def test_parse_nested__keeps_nested_blocks_in_section_content(self) -> None: + result = markdown.parse("# Workflow\n\n> Quote\n", artifact_id=make.ARTIFACT_ID) + + assert result.is_ok() + assert "> Quote" in result.unwrap()[0].as_original_markdown(with_title=False) + + +class TestParseOthers: + def test_parse_others__keeps_paragraphs_in_section_content(self) -> None: + result = markdown.parse("# Workflow\n\nBody\n", artifact_id=make.ARTIFACT_ID) + + assert result.is_ok() + assert "Body" in result.unwrap()[0].as_original_markdown(with_title=False) + + +class TestParse: + def test_parse__extracts_h1_h2_content_and_donna_config_blocks(self) -> None: + result = markdown.parse( + """# Workflow + +Intro + +```toml donna +id = "primary" +``` + +## Step + +Body + +```python donna script +print("run") +``` +""", + artifact_id=make.ARTIFACT_ID, + ) + + assert result.is_ok() + sections = result.unwrap() + assert [section.level for section in sections] == [SectionLevel.h1, SectionLevel.h2] + assert [section.title for section in sections] == ["Workflow", "Step"] + assert sections[0].config().unwrap() == {"id": "primary"} + assert sections[1].script().unwrap() == 'print("run")' + assert "Intro" in sections[0].as_original_markdown(with_title=False) + assert "Body" in sections[1].as_original_markdown(with_title=False) + + def test_parse__rejects_h2_before_h1(self) -> None: + result = markdown.parse("## Step\n", artifact_id=make.ARTIFACT_ID) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.MarkdownH1SectionMustBeFirst) + + def test_parse__rejects_multiple_h1_sections(self) -> None: + result = markdown.parse("# First\n\n# Second\n", artifact_id=make.ARTIFACT_ID) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.MarkdownMultipleH1Sections) diff --git a/donna/workspaces/tests/test_markdown_parser.py b/donna/workspaces/tests/test_markdown_parser.py new file mode 100644 index 00000000..f9afc2cf --- /dev/null +++ b/donna/workspaces/tests/test_markdown_parser.py @@ -0,0 +1,260 @@ +from typing import Any, ClassVar + +from donna.core.errors import ErrorsList +from donna.core.result import Err, Ok, Result +from donna.domain.artifact_ids import ArtifactId +from donna.domain.ids import SectionId +from donna.domain.python_path import PythonPath +from donna.machine.artifacts import Artifact, ArtifactSection, ArtifactSectionConfig +from donna.machine.primitives import Primitive +from donna.primitives.sections.text import Text +from donna.workspaces import errors as workspace_errors +from donna.workspaces.markdown import CodeSource, SectionLevel, SectionSource +from donna.workspaces import markdown_parser +from donna.workspaces.artifacts import RENDER_CONTEXT_VIEW +from donna.workspaces.markdown_parser import MarkdownSectionMixin, construct_sections_from_markdown +from donna.workspaces.tests import make + + +TEXT_KIND = PythonPath("donna.primitives.sections.text.Text") +LIB_TEXT_KIND = PythonPath("donna.lib.text") + + +class _MarkdownPrimitive(MarkdownSectionMixin, Primitive): + config_class: ClassVar[type[ArtifactSectionConfig]] = ArtifactSectionConfig + + +class _FailingMarkdownPrimitive(_MarkdownPrimitive): + def markdown_construct_section( + self, + artifact_id: ArtifactId, + source: SectionSource, + config: dict[str, Any], + primary: bool = False, + ) -> Result[ArtifactSection, ErrorsList]: + return Err([workspace_errors.MarkdownArtifactWithoutSections(artifact_id=artifact_id)]) + + +class TestMarkdownSectionConstructor: + def test_protocol__documents_markdown_construct_section_contract(self) -> None: + assert hasattr(markdown_parser.MarkdownSectionConstructor, "markdown_construct_section") + + +class TestMarkdownSectionMixin: + def test_markdown_build_title__uses_source_title_or_empty_string(self) -> None: + primitive = _MarkdownPrimitive() + + assert ( + primitive.markdown_build_title(make.ARTIFACT_ID, make.section_source(title="Title"), make.section_config()) + == "Title" + ) + assert ( + primitive.markdown_build_title(make.ARTIFACT_ID, make.section_source(title=None), make.section_config()) + == "" + ) + + def test_markdown_build_description__uses_original_markdown_without_title(self) -> None: + primitive = _MarkdownPrimitive() + source = make.section_source() + + assert primitive.markdown_build_description(make.ARTIFACT_ID, source, make.section_config()) == "" + + def test_markdown_construct_meta__returns_empty_meta(self) -> None: + primitive = _MarkdownPrimitive() + + result = primitive.markdown_construct_meta(make.ARTIFACT_ID, make.section_source(), make.section_config(), "") + + assert result.is_ok() + assert result.unwrap().cells_meta() == {} + + def test_markdown_construct_section__builds_artifact_section(self) -> None: + primitive = _MarkdownPrimitive() + + result = primitive.markdown_construct_section( + artifact_id=make.ARTIFACT_ID, + source=make.section_source(title="Section"), + config={"id": "section", "kind": str(TEXT_KIND)}, + ) + + assert result.is_ok() + section = result.unwrap() + assert section.id == SectionId("section") + assert section.kind == TEXT_KIND + assert section.title == "Section" + assert not section.primary + + +class TestParseArtifactContent: + def test_returns_original_sections_with_analysis_tokens(self) -> None: + result = markdown_parser.parse_artifact_content(make.ARTIFACT_ID, "# Workflow\n\nBody\n", RENDER_CONTEXT_VIEW) + + assert result.is_ok() + section = result.unwrap()[0] + assert section.title == "Workflow" + assert section.analysis_tokens + + def test_returns_error_for_source_without_sections(self) -> None: + result = markdown_parser.parse_artifact_content(make.ARTIFACT_ID, "", RENDER_CONTEXT_VIEW) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.MarkdownArtifactWithoutSections) + + def test_raises_internal_error_for_analysis_section_count_mismatch(self, mocker: object) -> None: + mocker.patch.object( + markdown_parser, + "render", + side_effect=[Ok("# Workflow\n"), Ok("# Workflow\n\n## Step\n")], + ) + + try: + markdown_parser.parse_artifact_content(make.ARTIFACT_ID, "# Workflow\n", RENDER_CONTEXT_VIEW) + except workspace_errors.MarkdownSectionsCountMismatch as error: + assert error.arguments["original_count"] == 1 + assert error.arguments["analyzed_count"] == 2 + else: + raise AssertionError("Expected MarkdownSectionsCountMismatch") + + +class TestConstructArtifactFromBytes: + def test_decodes_bytes_and_constructs_artifact_from_markdown(self, mocker: object) -> None: + expected = Artifact(id=make.ARTIFACT_ID, sections=[]) + construct = mocker.patch.object( + markdown_parser, + "construct_artifact_from_markdown_source", + return_value=Ok(expected), + ) + + result = markdown_parser.construct_artifact_from_bytes( + make.ARTIFACT_ID, + b"# Workflow", + RENDER_CONTEXT_VIEW, + default_section_kind=TEXT_KIND, + default_primary_section_kind=TEXT_KIND, + default_primary_section_id=SectionId("primary"), + ) + + assert result.is_ok() + assert result.unwrap() == expected + construct.assert_called_once() + + +class TestConstructArtifactFromMarkdownSource: + def test_constructs_artifact_with_default_primary_and_tail_config(self) -> None: + result = markdown_parser.construct_artifact_from_markdown_source( + make.ARTIFACT_ID, + "# Workflow\n\n## Step\n", + RENDER_CONTEXT_VIEW, + default_section_kind=LIB_TEXT_KIND, + default_primary_section_kind=LIB_TEXT_KIND, + default_primary_section_id=SectionId("primary"), + ) + + assert result.is_ok() + artifact = result.unwrap() + assert artifact.id == make.ARTIFACT_ID + assert [section.title for section in artifact.sections] == ["Workflow", "Step"] + assert artifact.sections[0].primary + + +class TestConstructSectionsFromMarkdown: + def test_parses_raw_string_kind_at_workspace_boundary(self) -> None: + section = SectionSource( + level=SectionLevel.h2, + title="Section", + configs=[ + CodeSource( + format="toml", + properties={"config": True}, + content='id = "section"\nkind = "donna.primitives.sections.text.Text"', + ) + ], + original_tokens=[], + analysis_tokens=[], + ) + + result = construct_sections_from_markdown( + artifact_id=ArtifactId("@/workflow.donna.md"), + sections=[section], + default_section_kind=TEXT_KIND, + primitive_overrides={TEXT_KIND: Text()}, + ) + + assert result.is_ok() + constructed_section = result.unwrap()[0] + assert constructed_section.id == SectionId("section") + assert constructed_section.kind == TEXT_KIND + assert constructed_section.title == "Section" + + def test_generates_missing_section_id(self, mocker: object) -> None: + mocker.patch("uuid.uuid4", return_value=type("FakeUuid", (), {"hex": "abc"})()) + section = make.section_source(configs=[CodeSource(format="toml", properties={"config": True}, content="")]) + + result = construct_sections_from_markdown( + artifact_id=make.ARTIFACT_ID, + sections=[section], + default_section_kind=TEXT_KIND, + primitive_overrides={TEXT_KIND: Text()}, + ) + + assert result.is_ok() + assert result.unwrap()[0].id == SectionId("markdownabc") + + def test_collects_section_construction_errors(self) -> None: + section = make.section_source(configs=[CodeSource(format="toml", properties={"config": True}, content="")]) + + result = construct_sections_from_markdown( + artifact_id=make.ARTIFACT_ID, + sections=[section], + default_section_kind=TEXT_KIND, + primitive_overrides={TEXT_KIND: _FailingMarkdownPrimitive()}, + ) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.MarkdownArtifactWithoutSections) + + +class TestResolvePrimitive: + def test_returns_override_when_present(self) -> None: + primitive = Text() + + result = markdown_parser._resolve_primitive(TEXT_KIND, {TEXT_KIND: primitive}) + + assert result.is_ok() + assert result.unwrap() == primitive + + def test_uses_machine_resolver_without_override(self, mocker: object) -> None: + primitive = Text() + resolve_primitive = mocker.patch.object(markdown_parser, "resolve_primitive", return_value=Ok(primitive)) + + result = markdown_parser._resolve_primitive(TEXT_KIND) + + assert result.is_ok() + assert result.unwrap() == primitive + resolve_primitive.assert_called_once_with(TEXT_KIND) + + +class TestParsePrimitiveId: + def test_returns_python_path_unchanged(self) -> None: + result = markdown_parser._parse_primitive_id(TEXT_KIND) + + assert result.is_ok() + assert result.unwrap() == TEXT_KIND + + def test_parses_string_python_path(self) -> None: + result = markdown_parser._parse_primitive_id(str(TEXT_KIND)) + + assert result.is_ok() + assert result.unwrap() == TEXT_KIND + + +class TestEnsureMarkdownConstructible: + def test_accepts_markdown_section_mixin(self) -> None: + result = markdown_parser._ensure_markdown_constructible(Text(), TEXT_KIND) + + assert result.is_ok() + + def test_rejects_primitive_without_markdown_support(self) -> None: + result = markdown_parser._ensure_markdown_constructible(Primitive(), TEXT_KIND) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.PrimitiveDoesNotSupportMarkdown) diff --git a/donna/workspaces/tests/test_paths.py b/donna/workspaces/tests/test_paths.py new file mode 100644 index 00000000..93b4da6a --- /dev/null +++ b/donna/workspaces/tests/test_paths.py @@ -0,0 +1,229 @@ +import pathlib + +from donna.domain.artifact_ids import ArtifactId +from donna.domain.paths import ProjectRootPath, ResolvedProjectPath, UntrustedPath +from donna.workspaces import paths +from donna.workspaces.paths import ( + normalize_artifact_id, + normalize_artifact_path, + normalize_artifact_section_id, + normalize_existing_path, + normalize_path, + resolve_project_path, + resolve_project_root, +) + + +class TestAppendNormalizedPart: + def test_appends_regular_parts_and_skips_current_dir(self) -> None: + parts = ["workflows"] + + assert paths._append_normalized_part(parts, ".") + assert paths._append_normalized_part(parts, "nested") + assert parts == ["workflows", "nested"] + + def test_rejects_empty_part_and_root_escape(self) -> None: + assert not paths._append_normalized_part([], "") + assert not paths._append_normalized_part([], "..") + + def test_parent_part_removes_previous_part(self) -> None: + parts = ["workflows", "nested"] + + assert paths._append_normalized_part(parts, "..") + assert parts == ["workflows"] + + +class TestNormalizeParts: + def test_returns_canonical_project_path(self) -> None: + assert paths._normalize_parts("workflows/./nested/../test.donna.md") == "@/workflows/test.donna.md" + + def test_uses_initial_parts_for_relative_artifact_paths(self) -> None: + assert ( + paths._normalize_parts("../plan.donna.md", initial_parts=("workflows", "rfc")) + == "@/workflows/plan.donna.md" + ) + + def test_rejects_empty_root_and_invalid_artifact_paths(self) -> None: + assert paths._normalize_parts("") is None + assert paths._normalize_parts(".") is None + assert paths._normalize_parts("invalid name.donna.md") is None + + +class TestResolveProjectRoot: + def test_returns_resolved_root_path(self, tmp_path: pathlib.Path) -> None: + project = tmp_path / "project" + project.mkdir() + root = project / ".." / "project" + + assert resolve_project_root(UntrustedPath(root)) == project + + +class TestNormalizeRootAnchored: + def test_normalizes_root_anchored_path(self) -> None: + assert paths._normalize_root_anchored("@/workflows/../plan.donna.md") == "@/plan.donna.md" + + def test_rejects_non_root_anchored_path(self) -> None: + assert paths._normalize_root_anchored("workflow.donna.md") is None + + +class TestResolveInsideProject: + def test_returns_resolved_project_path_inside_root(self, tmp_path: pathlib.Path) -> None: + project_file = tmp_path / "workflow.donna.md" + project_file.write_text("", encoding="utf-8") + + assert paths._resolve_inside_project( + UntrustedPath(project_file), + ProjectRootPath(tmp_path), + ) == ResolvedProjectPath(project_file) + + def test_rejects_project_root_and_outside_paths(self, tmp_path: pathlib.Path) -> None: + assert paths._resolve_inside_project(UntrustedPath(tmp_path), ProjectRootPath(tmp_path)) is None + assert paths._resolve_inside_project(UntrustedPath(tmp_path.parent), ProjectRootPath(tmp_path)) is None + + +class TestCanonicalFromResolved: + def test_returns_canonical_path_for_valid_resolved_path(self, tmp_path: pathlib.Path) -> None: + project_file = tmp_path / "workflow.donna.md" + + assert ( + paths._canonical_from_resolved(ResolvedProjectPath(project_file), ProjectRootPath(tmp_path)) + == "@/workflow.donna.md" + ) + + def test_rejects_invalid_artifact_path(self, tmp_path: pathlib.Path) -> None: + project_file = tmp_path / "invalid name.donna.md" + + assert paths._canonical_from_resolved(ResolvedProjectPath(project_file), ProjectRootPath(tmp_path)) is None + + +class TestResolveRootAnchoredPath: + def test_resolves_root_anchored_path_inside_project(self, tmp_path: pathlib.Path) -> None: + project_file = tmp_path / "workflow.donna.md" + project_file.write_text("", encoding="utf-8") + + assert paths._resolve_root_anchored_path("@/workflow.donna.md", ProjectRootPath(tmp_path)) == project_file + + def test_rejects_invalid_root_anchored_path(self, tmp_path: pathlib.Path) -> None: + assert paths._resolve_root_anchored_path("@/invalid name.donna.md", ProjectRootPath(tmp_path)) is None + + +class TestResolveProjectPath: + def test_resolves_root_anchored_path_inside_project(self, tmp_path: pathlib.Path) -> None: + project_file = tmp_path / "workflows" / "test.donna.md" + project_file.parent.mkdir() + project_file.write_text("", encoding="utf-8") + + assert resolve_project_path("@/workflows/test.donna.md", tmp_path) == project_file + + def test_resolves_absolute_path_inside_project(self, tmp_path: pathlib.Path) -> None: + project_file = tmp_path / "workflows" / "test.donna.md" + project_file.parent.mkdir() + project_file.write_text("", encoding="utf-8") + + assert resolve_project_path(str(project_file), tmp_path) == project_file + + def test_rejects_root_escape_and_project_root(self, tmp_path: pathlib.Path) -> None: + assert resolve_project_path("@/../outside.donna.md", tmp_path) is None + assert resolve_project_path("@/.", tmp_path) is None + + def test_rejects_absolute_path_when_not_allowed(self, tmp_path: pathlib.Path) -> None: + project_file = tmp_path / "workflow.donna.md" + project_file.write_text("", encoding="utf-8") + + assert resolve_project_path(str(project_file), tmp_path, allow_absolute=False) is None + + +class TestNormalizePath: + def test_normalizes_root_anchored_path(self, tmp_path: pathlib.Path) -> None: + assert normalize_path("@/workflows/./nested/../test.donna.md", tmp_path) == "@/workflows/test.donna.md" + + def test_normalizes_absolute_path_inside_project(self, tmp_path: pathlib.Path) -> None: + project_file = tmp_path / "workflows" / "test.donna.md" + project_file.parent.mkdir() + project_file.write_text("", encoding="utf-8") + + assert normalize_path(str(project_file), tmp_path) == "@/workflows/test.donna.md" + + def test_normalizes_relative_path_from_cwd(self, tmp_path: pathlib.Path) -> None: + cwd = tmp_path / "workflows" + cwd.mkdir() + + assert normalize_path("test.donna.md", tmp_path, cwd=cwd) == "@/workflows/test.donna.md" + + def test_rejects_path_outside_project(self, tmp_path: pathlib.Path) -> None: + outside = tmp_path.parent / "outside.donna.md" + + assert normalize_path(str(outside), tmp_path) is None + assert normalize_path("../outside.donna.md", tmp_path, cwd=tmp_path) is None + + +class TestNormalizeExistingPath: + def test_normalizes_existing_file(self, tmp_path: pathlib.Path) -> None: + project_file = tmp_path / "workflows" / "test.donna.md" + project_file.parent.mkdir() + project_file.write_text("", encoding="utf-8") + + assert normalize_existing_path(UntrustedPath(project_file), tmp_path) == "@/workflows/test.donna.md" + + def test_rejects_project_root(self, tmp_path: pathlib.Path) -> None: + assert normalize_existing_path(UntrustedPath(tmp_path), tmp_path) is None + + +class TestNormalizeArtifactPath: + def test_normalizes_relative_to_artifact_file(self, tmp_path: pathlib.Path) -> None: + relative_to = ArtifactId("@/workflows/rfc/do.donna.md") + + assert ( + normalize_artifact_path("../plan.donna.md", tmp_path, relative_to=relative_to) + == "@/workflows/plan.donna.md" + ) + + def test_uses_normalize_path_without_artifact_base(self, tmp_path: pathlib.Path) -> None: + assert normalize_artifact_path("@/workflow.donna.md", tmp_path) == "@/workflow.donna.md" + + def test_rejects_invalid_artifact_path(self, tmp_path: pathlib.Path) -> None: + assert ( + normalize_artifact_path("../outside.donna.md", tmp_path, relative_to=ArtifactId("@/file.donna.md")) is None + ) + + assert normalize_artifact_path("", tmp_path) is None + assert normalize_artifact_path(None, tmp_path) is None # type: ignore[arg-type] + + +class TestNormalizeFromArtifact: + def test_normalizes_root_anchored_and_artifact_relative_paths(self) -> None: + relative_to = ArtifactId("@/workflows/rfc/do.donna.md") + + assert paths._normalize_from_artifact("@/plan.donna.md", relative_to) == "@/plan.donna.md" + assert paths._normalize_from_artifact("../plan.donna.md", relative_to) == "@/workflows/plan.donna.md" + + +class TestNormalizeArtifactId: + def test_returns_artifact_id_for_valid_path(self, tmp_path: pathlib.Path) -> None: + assert normalize_artifact_id("@/workflow.donna.md", tmp_path) == ArtifactId("@/workflow.donna.md") + + def test_returns_artifact_id_for_relative_path_from_cwd(self, tmp_path: pathlib.Path) -> None: + cwd = tmp_path / "workflows" + cwd.mkdir() + + assert normalize_artifact_id("test.donna.md", tmp_path, cwd=cwd) == ArtifactId("@/workflows/test.donna.md") + + def test_rejects_invalid_artifact_id_path(self, tmp_path: pathlib.Path) -> None: + assert normalize_artifact_id("@/workflow", tmp_path) is None + + +class TestNormalizeArtifactSectionId: + def test_returns_artifact_section_id_for_valid_input(self, tmp_path: pathlib.Path) -> None: + assert normalize_artifact_section_id("@/workflow.donna.md:step", tmp_path) == "@/workflow.donna.md:step" + + def test_returns_artifact_section_id_relative_to_artifact_file(self, tmp_path: pathlib.Path) -> None: + relative_to = ArtifactId("@/workflows/rfc/do.donna.md") + + assert ( + normalize_artifact_section_id("../plan.donna.md:step", tmp_path, relative_to=relative_to) + == "@/workflows/plan.donna.md:step" + ) + + def test_rejects_missing_or_invalid_section(self, tmp_path: pathlib.Path) -> None: + assert normalize_artifact_section_id("@/workflow.donna.md", tmp_path) is None + assert normalize_artifact_section_id("@/workflow.donna.md:---", tmp_path) is None diff --git a/donna/workspaces/tests/test_sessions.py b/donna/workspaces/tests/test_sessions.py new file mode 100644 index 00000000..26d4a265 --- /dev/null +++ b/donna/workspaces/tests/test_sessions.py @@ -0,0 +1,102 @@ +import pathlib + +from donna.domain.constants import STATE_FILE_NAME +from donna.workspaces import sessions +from donna.workspaces.config import Config +from donna.workspaces.files import FileFingerprint + + +def _patch_session_globals(mocker: object, tmp_path: pathlib.Path) -> None: + mocker.patch("donna.workspaces.sessions.project_dir", return_value=tmp_path) + mocker.patch("donna.workspaces.sessions.config", return_value=Config(session_dir=".session/donna")) + + +class TestDir: + def test_dir__creates_session_directory(self, mocker: object, tmp_path: pathlib.Path) -> None: + _patch_session_globals(mocker, tmp_path) + + session_dir = sessions.dir() + + assert session_dir == tmp_path / ".session" / "donna" + assert session_dir.is_dir() + + +class TestPath: + def test_returns_configured_session_path_without_creating_it(self, mocker: object, tmp_path: pathlib.Path) -> None: + _patch_session_globals(mocker, tmp_path) + + assert sessions._path() == tmp_path / ".session" / "donna" + assert not sessions._path().exists() + + +class TestStatePath: + def test_returns_state_file_path_under_session_dir(self, mocker: object, tmp_path: pathlib.Path) -> None: + _patch_session_globals(mocker, tmp_path) + + assert sessions._state_path() == tmp_path / ".session" / "donna" / STATE_FILE_NAME + + +class TestEnsureDir: + def test_ensure_dir__creates_session_directory(self, mocker: object, tmp_path: pathlib.Path) -> None: + _patch_session_globals(mocker, tmp_path) + + sessions.ensure_dir() + + assert (tmp_path / ".session" / "donna").is_dir() + + +class TestResetDir: + def test_reset_dir__removes_existing_content_and_recreates_directory( + self, mocker: object, tmp_path: pathlib.Path + ) -> None: + _patch_session_globals(mocker, tmp_path) + state_path = tmp_path / ".session" / "donna" / STATE_FILE_NAME + state_path.parent.mkdir(parents=True) + state_path.write_bytes(b"state") + + sessions.reset_dir() + + assert state_path.parent.is_dir() + assert not state_path.exists() + + +class TestReadState: + def test_read_state__returns_none_for_missing_state_file(self, mocker: object, tmp_path: pathlib.Path) -> None: + _patch_session_globals(mocker, tmp_path) + + assert sessions.read_state() is None + + def test_read_state__returns_state_bytes(self, mocker: object, tmp_path: pathlib.Path) -> None: + _patch_session_globals(mocker, tmp_path) + sessions.write_state(b"state") + + assert sessions.read_state() == b"state" + + +class TestWriteState: + def test_write_state__writes_state_bytes(self, mocker: object, tmp_path: pathlib.Path) -> None: + _patch_session_globals(mocker, tmp_path) + sessions.dir() + + sessions.write_state(b"state") + + assert (tmp_path / ".session" / "donna" / STATE_FILE_NAME).read_bytes() == b"state" + + +class TestStateFingerprint: + def test_state_fingerprint__returns_none_for_missing_state_file( + self, mocker: object, tmp_path: pathlib.Path + ) -> None: + _patch_session_globals(mocker, tmp_path) + + assert sessions.state_fingerprint() is None + + def test_state_fingerprint__returns_fingerprint_for_state_file( + self, mocker: object, tmp_path: pathlib.Path + ) -> None: + _patch_session_globals(mocker, tmp_path) + sessions.write_state(b"state") + + assert sessions.state_fingerprint() == FileFingerprint.from_path( + tmp_path / ".session" / "donna" / STATE_FILE_NAME + ) diff --git a/donna/workspaces/tests/test_symbol_coverage.py b/donna/workspaces/tests/test_symbol_coverage.py new file mode 100644 index 00000000..fade7cf2 --- /dev/null +++ b/donna/workspaces/tests/test_symbol_coverage.py @@ -0,0 +1,67 @@ +import ast +import pathlib + + +def _pascal_case(name: str) -> str: + return "".join(part.capitalize() for part in name.strip("_").split("_")) + + +def _expected_test_class_name(symbol_name: str, *, symbol_is_class: bool) -> str: + if symbol_is_class: + return f"Test{symbol_name.lstrip('_')}" + + return f"Test{_pascal_case(symbol_name)}" + + +def _expected_test_class_for_node(node: ast.AST) -> str | None: + if isinstance(node, ast.ClassDef): + return _expected_test_class_name(node.name, symbol_is_class=True) + + if isinstance(node, ast.FunctionDef): + return _expected_test_class_name(node.name, symbol_is_class=False) + + return None + + +def _production_module_symbols(path: pathlib.Path) -> list[str]: + tree = ast.parse(path.read_text(encoding="utf-8")) + return [class_name for node in tree.body if (class_name := _expected_test_class_for_node(node)) is not None] + + +def _production_module_paths() -> list[pathlib.Path]: + workspace_dir = pathlib.Path(__file__).parents[1] + return [path for path in sorted(workspace_dir.glob("*.py")) if path.name != "__init__.py"] + + +def _production_symbols() -> dict[str, list[str]]: + symbols = {} + + for path in _production_module_paths(): + symbols[path.name] = _production_module_symbols(path) + + return symbols + + +def _test_class_names() -> set[str]: + tests_dir = pathlib.Path(__file__).parent + names = set() + + for path in tests_dir.glob("test_*.py"): + tree = ast.parse(path.read_text(encoding="utf-8")) + for node in tree.body: + if isinstance(node, ast.ClassDef): + names.add(node.name) + + return names + + +class TestWorkspaceSymbolCoverage: + def test_each_module_level_function_and_class_has_a_test_class(self) -> None: + test_classes = _test_class_names() + missing = { + module_name: [class_name for class_name in expected_classes if class_name not in test_classes] + for module_name, expected_classes in _production_symbols().items() + } + missing = {module_name: names for module_name, names in missing.items() if names} + + assert missing == {} diff --git a/donna/workspaces/tests/test_templates.py b/donna/workspaces/tests/test_templates.py new file mode 100644 index 00000000..1050c72e --- /dev/null +++ b/donna/workspaces/tests/test_templates.py @@ -0,0 +1,185 @@ +from typing import Any + +import pytest + +from donna.core.errors import EnvironmentErrorsProxy, ErrorsList +from donna.core.result import Err, Ok, Result +from donna.machine.templates import Directive, RenderMode +from donna.workspaces import errors as workspace_errors +from donna.workspaces import templates +from donna.workspaces.artifacts import ArtifactRenderContext +from donna.workspaces.templates import DirectivePathBuilder, render +from donna.workspaces.tests import make + + +class _Directive(Directive): + analyze_id: str = "sample" + + def render_view(self, context: dict[str, Any], *argv: Any) -> Result[Any, ErrorsList]: + return Ok(f"{context['render_mode']}:{','.join(str(arg) for arg in argv)}") + + +class _FailingDirective(Directive): + analyze_id: str = "failing" + + def render_view(self, context: dict[str, Any], *argv: Any) -> Result[Any, ErrorsList]: + return Err([workspace_errors.MarkdownArtifactWithoutSections(artifact_id=make.ARTIFACT_ID)]) + + +class _ExplodingDirective(Directive): + analyze_id: str = "exploding" + + def render_view(self, context: dict[str, Any], *argv: Any) -> Result[Any, ErrorsList]: + raise RuntimeError("boom") + + +sample_directive = _Directive() +failing_directive = _FailingDirective() +exploding_directive = _ExplodingDirective() +not_directive = object() + + +class TestIsImportableModule: + def test_returns_whether_module_can_be_imported(self) -> None: + assert templates._is_importable_module("donna.workspaces.tests.test_templates") + assert not templates._is_importable_module("donna.workspaces.tests.missing") + + +class TestDirectivePathBuilder: + def test_getattr__extends_directive_path(self) -> None: + builder = DirectivePathBuilder(("donna", "workspaces")).tests.test_templates.sample_directive + + result = builder({"render_mode": RenderMode.view, "artifact_id": make.ARTIFACT_ID}, "value") + + assert result == "view:value" + + def test_getitem__extends_directive_path(self) -> None: + builder = DirectivePathBuilder(("donna", "workspaces"))["tests"]["test_templates"]["sample_directive"] + + result = builder({"render_mode": RenderMode.view, "artifact_id": make.ARTIFACT_ID}, "value") + + assert result == "view:value" + + def test_call__applies_directive(self) -> None: + builder = DirectivePathBuilder(("donna", "workspaces", "tests", "test_templates", "sample_directive")) + + result = builder({"render_mode": RenderMode.view, "artifact_id": make.ARTIFACT_ID}, "value") + + assert result == "view:value" + + def test_call__reports_incomplete_path(self) -> None: + builder = DirectivePathBuilder(("donna",)) + + with pytest.raises(EnvironmentErrorsProxy) as exception_info: + builder({"artifact_id": make.ARTIFACT_ID}) + + error = exception_info.value.arguments["errors"][0] + assert isinstance(error, workspace_errors.DirectivePathIncomplete) + assert error.path == "donna" + + def test_call__reports_unimportable_module(self) -> None: + builder = DirectivePathBuilder(("donna", "workspaces", "tests", "missing", "sample_directive")) + + with pytest.raises(EnvironmentErrorsProxy) as exception_info: + builder({"artifact_id": make.ARTIFACT_ID}) + + error = exception_info.value.arguments["errors"][0] + assert isinstance(error, workspace_errors.DirectiveModuleNotImportable) + assert error.module_path == "donna.workspaces.tests.missing" + + def test_call__reports_unexpected_import_error(self, mocker: object) -> None: + builder = DirectivePathBuilder(("donna", "workspaces", "tests", "test_templates", "sample_directive")) + mocker.patch("importlib.import_module", side_effect=RuntimeError("boom")) + + with pytest.raises(EnvironmentErrorsProxy) as exception_info: + builder({"artifact_id": make.ARTIFACT_ID}) + + error = exception_info.value.arguments["errors"][0] + assert isinstance(error, workspace_errors.DirectiveUnexpectedError) + assert error.directive_path == "donna.workspaces.tests.test_templates.sample_directive" + + def test_call__reports_missing_directive(self) -> None: + builder = DirectivePathBuilder(("donna", "workspaces", "tests", "test_templates", "missing")) + + with pytest.raises(EnvironmentErrorsProxy) as exception_info: + builder({"artifact_id": make.ARTIFACT_ID}) + + error = exception_info.value.arguments["errors"][0] + assert isinstance(error, workspace_errors.DirectiveNotAvailable) + assert error.module_path == "donna.workspaces.tests.test_templates" + assert error.directive_name == "missing" + + def test_call__reports_non_directive_object(self) -> None: + builder = DirectivePathBuilder(("donna", "workspaces", "tests", "test_templates", "not_directive")) + + with pytest.raises(EnvironmentErrorsProxy) as exception_info: + builder({"artifact_id": make.ARTIFACT_ID}) + + error = exception_info.value.arguments["errors"][0] + assert isinstance(error, workspace_errors.DirectiveNotDirective) + + def test_call__passes_directive_result_errors(self) -> None: + builder = DirectivePathBuilder(("donna", "workspaces", "tests", "test_templates", "failing_directive")) + + with pytest.raises(EnvironmentErrorsProxy) as exception_info: + builder({"render_mode": RenderMode.view, "artifact_id": make.ARTIFACT_ID}) + + error = exception_info.value.arguments["errors"][0] + assert isinstance(error, workspace_errors.MarkdownArtifactWithoutSections) + assert error.artifact_id == make.ARTIFACT_ID + + def test_call__reports_unexpected_directive_error(self) -> None: + builder = DirectivePathBuilder(("donna", "workspaces", "tests", "test_templates", "exploding_directive")) + + with pytest.raises(EnvironmentErrorsProxy) as exception_info: + builder({"render_mode": RenderMode.view, "artifact_id": make.ARTIFACT_ID}) + + error = exception_info.value.arguments["errors"][0] + assert isinstance(error, workspace_errors.DirectiveUnexpectedError) + assert error.directive_path == "donna.workspaces.tests.test_templates.exploding_directive" + + +class TestDirectivePathUndefined: + def test_getattr__returns_directive_builder_for_importable_module(self) -> None: + value = templates.DirectivePathUndefined(name="donna").workspaces + + assert isinstance(value, DirectivePathBuilder) + + def test_getattr__returns_undefined_for_non_importable_module(self) -> None: + value = templates.DirectivePathUndefined(name="missing").directive + + assert not isinstance(value, DirectivePathBuilder) + + +class TestEnv: + def test_env__returns_cached_environment_with_directive_undefined(self) -> None: + environment = templates.env() + + assert environment is templates.env() + assert environment.undefined is templates.DirectivePathUndefined + + +class TestRender: + def test_render__applies_template_directives(self) -> None: + context = ArtifactRenderContext(primary_mode=RenderMode.view) + + result = render( + make.ARTIFACT_ID, + "{{ donna.workspaces.tests.test_templates.sample_directive('value') }}", + context, + ) + + assert result.is_ok() + assert result.unwrap() == "view:value" + + def test_render__returns_directive_errors(self) -> None: + context = ArtifactRenderContext(primary_mode=RenderMode.view) + + result = render( + make.ARTIFACT_ID, + "{{ donna.workspaces.tests.test_templates.failing_directive() }}", + context, + ) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], workspace_errors.MarkdownArtifactWithoutSections) diff --git a/donna/workspaces/tests/test_utils.py b/donna/workspaces/tests/test_utils.py new file mode 100644 index 00000000..e6492c2f --- /dev/null +++ b/donna/workspaces/tests/test_utils.py @@ -0,0 +1,40 @@ +import pathlib + +from donna.workspaces import errors as workspace_errors +from donna.workspaces import utils + + +class TestFirstProjectDirWithConfig: + def test_returns_nearest_parent_with_config(self, mocker: object, tmp_path: pathlib.Path) -> None: + project = tmp_path / "project" + nested = project / "nested" + nested.mkdir(parents=True) + (project / "donna.toml").write_text("version = 1", encoding="utf-8") + mocker.patch("pathlib.Path.cwd", return_value=nested) + + assert utils.first_project_dir_with_config("donna.toml") == project + + def test_returns_none_when_config_is_not_found(self, mocker: object, tmp_path: pathlib.Path) -> None: + mocker.patch("pathlib.Path.cwd", return_value=tmp_path) + + assert utils.first_project_dir_with_config("donna.toml") is None + + +class TestDiscoverProjectDir: + def test_returns_discovered_project_dir(self, mocker: object, tmp_path: pathlib.Path) -> None: + mocker.patch.object(utils, "first_project_dir_with_config", return_value=tmp_path) + + result = utils.discover_project_dir("donna.toml") + + assert result.is_ok() + assert result.unwrap() == tmp_path + + def test_reports_missing_config(self, mocker: object) -> None: + mocker.patch.object(utils, "first_project_dir_with_config", return_value=None) + + result = utils.discover_project_dir("donna.toml") + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, workspace_errors.WorkspaceConfigNotDiscovered) + assert error.config_name == "donna.toml" From 1782f51c724bdb99fbde5ca99684d82b85d7aa6f Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 13:20:18 +0200 Subject: [PATCH 47/98] context tests --- donna/context/tests/__init__.py | 1 + donna/context/tests/make.py | 31 ++++++ donna/context/tests/test_artifacts.py | 143 +++++++++++++++++++++++++ donna/context/tests/test_context.py | 75 +++++++++++++ donna/context/tests/test_journal.py | 88 +++++++++++++++ donna/context/tests/test_primitives.py | 67 ++++++++++++ donna/context/tests/test_state.py | 86 +++++++++++++++ specs/architecture/tests.md | 5 + tach.toml | 1 + 9 files changed, 497 insertions(+) create mode 100644 donna/context/tests/__init__.py create mode 100644 donna/context/tests/make.py create mode 100644 donna/context/tests/test_artifacts.py create mode 100644 donna/context/tests/test_context.py create mode 100644 donna/context/tests/test_journal.py create mode 100644 donna/context/tests/test_primitives.py create mode 100644 donna/context/tests/test_state.py diff --git a/donna/context/tests/__init__.py b/donna/context/tests/__init__.py new file mode 100644 index 00000000..76e4fa5c --- /dev/null +++ b/donna/context/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for donna.context.""" diff --git a/donna/context/tests/make.py b/donna/context/tests/make.py new file mode 100644 index 00000000..3e503228 --- /dev/null +++ b/donna/context/tests/make.py @@ -0,0 +1,31 @@ +import pathlib + +from donna.core.errors import ErrorsList +from donna.core.result import Ok, Result +from donna.machine.artifacts import Artifact +from donna.machine.templates import RenderMode +from donna.protocol.journal import JournalRecord +from donna.workspaces.artifacts import ArtifactRenderContext + + +class FakeRawArtifact: + def __init__(self, path: pathlib.Path, artifact: Artifact) -> None: + self.path = path + self.artifact = artifact + self.render_modes: list[RenderMode] = [] + + def render(self, artifact_id: object, render_context: ArtifactRenderContext) -> Result[Artifact, ErrorsList]: + self.render_modes.append(render_context.primary_mode) + return Ok(self.artifact) + + +class FakeOutputEmitter: + def __init__(self) -> None: + self.cells: list[object] = [] + self.journal_records: list[JournalRecord] = [] + + def emit_cell(self, cell: object) -> None: + self.cells.append(cell) + + def emit_journal(self, record: JournalRecord) -> None: + self.journal_records.append(record) diff --git a/donna/context/tests/test_artifacts.py b/donna/context/tests/test_artifacts.py new file mode 100644 index 00000000..5f321294 --- /dev/null +++ b/donna/context/tests/test_artifacts.py @@ -0,0 +1,143 @@ +import pathlib +from typing import Any + +from donna.context.artifacts import ArtifactsCache +from donna.context.tests import make +from donna.core.result import Ok +from donna.domain.artifact_ids import ArtifactId +from donna.machine.templates import RenderMode +from donna.machine.tests import make as machine_make +from donna.workspaces import artifacts as workspace_artifacts +from donna.workspaces import errors as workspace_errors +from donna.workspaces.files import FileFingerprint + + +def _write_artifact_file(tmp_path: pathlib.Path, name: str, content: str) -> pathlib.Path: + path = tmp_path / name + path.write_text(content, encoding="utf-8") + return path + + +class TestArtifactsCache: + def test_load__caches_view_rendered_artifact_by_render_mode(self, mocker: Any, tmp_path: pathlib.Path) -> None: + path = _write_artifact_file(tmp_path, "workflow.donna.md", "# Workflow") + raw_artifact = make.FakeRawArtifact(path, machine_make.artifact()) + mocker.patch("donna.workspaces.artifacts.fetch_raw_artifact", return_value=Ok(raw_artifact)) + mocker.patch( + "donna.workspaces.artifacts.artifact_fingerprint", return_value=Ok(FileFingerprint.from_path(path)) + ) + cache = ArtifactsCache() + + first_result = cache.load(machine_make.ARTIFACT_ID, workspace_artifacts.RENDER_CONTEXT_VIEW) + second_result = cache.load(machine_make.ARTIFACT_ID, workspace_artifacts.RENDER_CONTEXT_VIEW) + + assert first_result.is_ok() + assert second_result.is_ok() + assert first_result.unwrap() == second_result.unwrap() + assert raw_artifact.render_modes == [RenderMode.view] + + def test_load__renders_execute_mode_every_time(self, mocker: Any, tmp_path: pathlib.Path) -> None: + path = _write_artifact_file(tmp_path, "workflow.donna.md", "# Workflow") + raw_artifact = make.FakeRawArtifact(path, machine_make.artifact()) + mocker.patch("donna.workspaces.artifacts.fetch_raw_artifact", return_value=Ok(raw_artifact)) + mocker.patch( + "donna.workspaces.artifacts.artifact_fingerprint", return_value=Ok(FileFingerprint.from_path(path)) + ) + render_context = workspace_artifacts.ArtifactRenderContext(primary_mode=RenderMode.execute) + cache = ArtifactsCache() + + assert cache.load(machine_make.ARTIFACT_ID, render_context).is_ok() + assert cache.load(machine_make.ARTIFACT_ID, render_context).is_ok() + + assert raw_artifact.render_modes == [RenderMode.execute, RenderMode.execute] + + def test_load__refreshes_stale_raw_artifact(self, mocker: Any, tmp_path: pathlib.Path) -> None: + first_path = _write_artifact_file(tmp_path, "first.donna.md", "# First") + second_path = _write_artifact_file(tmp_path, "second.donna.md", "# Second with changed size") + first_raw_artifact = make.FakeRawArtifact(first_path, machine_make.artifact()) + second_raw_artifact = make.FakeRawArtifact(second_path, machine_make.artifact()) + mocker.patch( + "donna.workspaces.artifacts.fetch_raw_artifact", + side_effect=[Ok(first_raw_artifact), Ok(second_raw_artifact)], + ) + mocker.patch( + "donna.workspaces.artifacts.artifact_fingerprint", + return_value=Ok(FileFingerprint.from_path(second_path)), + ) + cache = ArtifactsCache() + + assert cache.load(machine_make.ARTIFACT_ID, workspace_artifacts.RENDER_CONTEXT_VIEW).is_ok() + result = cache.load(machine_make.ARTIFACT_ID, workspace_artifacts.RENDER_CONTEXT_VIEW) + + assert result.is_ok() + assert first_raw_artifact.render_modes == [RenderMode.view] + assert second_raw_artifact.render_modes == [RenderMode.view] + + def test_load__reports_missing_artifact_after_fetch(self, mocker: Any, tmp_path: pathlib.Path) -> None: + missing_path = tmp_path / "missing.donna.md" + raw_artifact = make.FakeRawArtifact(missing_path, machine_make.artifact()) + mocker.patch("donna.workspaces.artifacts.fetch_raw_artifact", return_value=Ok(raw_artifact)) + + result = ArtifactsCache().load(machine_make.ARTIFACT_ID, workspace_artifacts.RENDER_CONTEXT_VIEW) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, workspace_errors.ArtifactNotFound) + assert error.artifact_id == machine_make.ARTIFACT_ID + + def test_invalidate__removes_cached_rendered_artifact(self, mocker: Any, tmp_path: pathlib.Path) -> None: + path = _write_artifact_file(tmp_path, "workflow.donna.md", "# Workflow") + raw_artifact = make.FakeRawArtifact(path, machine_make.artifact()) + mocker.patch("donna.workspaces.artifacts.fetch_raw_artifact", return_value=Ok(raw_artifact)) + mocker.patch( + "donna.workspaces.artifacts.artifact_fingerprint", return_value=Ok(FileFingerprint.from_path(path)) + ) + cache = ArtifactsCache() + assert cache.load(machine_make.ARTIFACT_ID, workspace_artifacts.RENDER_CONTEXT_VIEW).is_ok() + + cache.invalidate(machine_make.ARTIFACT_ID) + assert cache.load(machine_make.ARTIFACT_ID, workspace_artifacts.RENDER_CONTEXT_VIEW).is_ok() + + assert raw_artifact.render_modes == [RenderMode.view, RenderMode.view] + + def test_list__returns_loaded_artifacts_in_workspace_order(self, mocker: Any, tmp_path: pathlib.Path) -> None: + first_id = machine_make.ARTIFACT_ID + second_id = ArtifactId("@/workflows/other.donna.md") + first_path = _write_artifact_file(tmp_path, "first.donna.md", "# First") + second_path = _write_artifact_file(tmp_path, "second.donna.md", "# Second") + first_artifact = machine_make.artifact() + second_artifact = machine_make.artifact().replace(id=second_id) + mocker.patch("donna.workspaces.artifacts.list_artifact_ids", return_value=[first_id, second_id]) + mocker.patch( + "donna.workspaces.artifacts.fetch_raw_artifact", + side_effect=[ + Ok(make.FakeRawArtifact(first_path, first_artifact)), + Ok(make.FakeRawArtifact(second_path, second_artifact)), + ], + ) + + result = ArtifactsCache().list(workspace_artifacts.RENDER_CONTEXT_VIEW) + + assert result.is_ok() + assert result.unwrap() == [first_artifact, second_artifact] + + def test_list__collects_artifact_loading_errors(self, mocker: Any, tmp_path: pathlib.Path) -> None: + missing_id = ArtifactId("@/workflows/missing.donna.md") + path = _write_artifact_file(tmp_path, "workflow.donna.md", "# Workflow") + mocker.patch( + "donna.workspaces.artifacts.list_artifact_ids", return_value=[machine_make.ARTIFACT_ID, missing_id] + ) + mocker.patch( + "donna.workspaces.artifacts.fetch_raw_artifact", + side_effect=[ + Ok(make.FakeRawArtifact(path, machine_make.artifact())), + Ok(make.FakeRawArtifact(tmp_path / "missing.donna.md", machine_make.artifact())), + ], + ) + + result = ArtifactsCache().list(workspace_artifacts.RENDER_CONTEXT_VIEW) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, workspace_errors.ArtifactNotFound) + assert error.artifact_id == missing_id diff --git a/donna/context/tests/test_context.py b/donna/context/tests/test_context.py new file mode 100644 index 00000000..6c5c0e82 --- /dev/null +++ b/donna/context/tests/test_context.py @@ -0,0 +1,75 @@ +import pytest + +from donna.context.artifacts import ArtifactsCache +from donna.context.context import Context, context, reset_context, set_context +from donna.context.journal import Journal +from donna.context.output import NoopEmitter +from donna.context.primitives import PrimitivesCache +from donna.context.state import StateCache +from donna.context.tests.make import FakeOutputEmitter + + +class TestContext: + def test_init__creates_invocation_local_caches_and_scopes(self) -> None: + runtime_context = Context() + + assert isinstance(runtime_context.artifacts, ArtifactsCache) + assert isinstance(runtime_context.state, StateCache) + assert isinstance(runtime_context.primitives, PrimitivesCache) + assert isinstance(runtime_context.journal, Journal) + assert isinstance(runtime_context.output, NoopEmitter) + assert runtime_context.current_work_unit_id.get() is None + assert runtime_context.current_operation_id.get() is None + + def test_init__uses_explicit_output_emitter(self) -> None: + output = FakeOutputEmitter() + + runtime_context = Context(output=output) + + assert runtime_context.output == output + + +class TestSetContext: + def test_sets_current_context(self) -> None: + runtime_context = Context() + token = set_context(runtime_context) + + try: + assert context() == runtime_context + finally: + reset_context(token) + + +class TestResetContext: + def test_restores_previous_context(self) -> None: + outer_context = Context() + inner_context = Context() + outer_token = set_context(outer_context) + + try: + inner_token = set_context(inner_context) + assert context() == inner_context + + reset_context(inner_token) + + assert context() == outer_context + finally: + reset_context(outer_token) + + +class TestContextFunction: + def test_context__raises_when_not_set(self) -> None: + with pytest.raises(RuntimeError): + context() + + def test_context__returns_current_context(self) -> None: + runtime_context = Context() + token = set_context(runtime_context) + + try: + assert context() == runtime_context + finally: + reset_context(token) + + with pytest.raises(RuntimeError): + context() diff --git a/donna/context/tests/test_journal.py b/donna/context/tests/test_journal.py new file mode 100644 index 00000000..5ede9298 --- /dev/null +++ b/donna/context/tests/test_journal.py @@ -0,0 +1,88 @@ +import datetime +from typing import Any, cast + +import pytest + +from donna.context.context import Context +from donna.context.journal import Journal +from donna.context.tests.make import FakeOutputEmitter +from donna.core.result import Ok +from donna.domain.artifact_ids import ArtifactSectionId +from donna.domain.internal_ids import WorkUnitId +from donna.machine.context import ValueScope +from donna.machine.tests import make as machine_make +from donna.protocol import errors as protocol_errors +from donna.protocol import modes as protocol_modes + + +class _FakeStateCache: + def __init__(self) -> None: + self.state = machine_make.mutable_state(tasks=[machine_make.task()]).freeze() + + def load(self) -> object: + return Ok(self.state) + + +class _FakeContext: + def __init__(self) -> None: + self.state = _FakeStateCache() + self.current_work_unit_id: ValueScope[WorkUnitId] = ValueScope() + self.current_operation_id: ValueScope[ArtifactSectionId] = ValueScope() + self.output = FakeOutputEmitter() + + +class TestJournal: + @pytest.mark.parametrize( + ("mode", "actor_id"), + [ + (protocol_modes.Mode.human, "human"), + (protocol_modes.Mode.llm, "agent"), + (protocol_modes.Mode.automation, "automation"), + ], + ) + def test_smart_actor_id__depends_on_selected_protocol( + self, mocker: Any, mode: protocol_modes.Mode, actor_id: str + ) -> None: + mocker.patch("donna.context.journal.protocol_mode", return_value=mode) + + assert Journal(cast(Context, _FakeContext())).smart_actor_id() == actor_id + + def test_smart_actor_id__raises_for_unsupported_mode(self, mocker: Any) -> None: + mocker.patch("donna.context.journal.protocol_mode", return_value="unsupported") + + with pytest.raises(protocol_errors.UnsupportedFormatterMode): + Journal(cast(Context, _FakeContext())).smart_actor_id() + + def test_add__builds_writes_and_emits_journal_record(self, mocker: Any) -> None: + now = datetime.datetime(2026, 5, 18, 8, 30, tzinfo=datetime.UTC) + fake_context = _FakeContext() + mocker.patch("donna.context.journal.protocol_mode", return_value=protocol_modes.Mode.llm) + mocker.patch("donna.context.journal.now", return_value=now) + write_record = mocker.patch("donna.context.journal.workspace_journal.write_record", return_value=Ok(None)) + + with fake_context.current_work_unit_id.scope(machine_make.WORK_UNIT_ID): + with fake_context.current_operation_id.scope(machine_make.PRIMARY_OPERATION_ID): + result = Journal(cast(Context, fake_context)).add("message") + + assert result.is_ok() + record = result.unwrap() + assert record.timestamp == now + assert record.actor_id == "agent" + assert record.message == "message" + assert record.current_task_id == machine_make.TASK_ID + assert record.current_work_unit_id == machine_make.WORK_UNIT_ID + assert record.current_operation_id == machine_make.PRIMARY_OPERATION_ID + write_record.assert_called_once_with(record) + assert fake_context.output.journal_records == [record] + + def test_add__uses_explicit_actor_id(self, mocker: Any) -> None: + fake_context = _FakeContext() + mocker.patch("donna.context.journal.now", return_value=datetime.datetime(2026, 5, 18, tzinfo=datetime.UTC)) + mocker.patch("donna.context.journal.workspace_journal.write_record", return_value=Ok(None)) + protocol_mode = mocker.patch("donna.context.journal.protocol_mode") + + result = Journal(cast(Context, fake_context)).add("message", actor_id="donna") + + assert result.is_ok() + assert result.unwrap().actor_id == "donna" + protocol_mode.assert_not_called() diff --git a/donna/context/tests/test_primitives.py b/donna/context/tests/test_primitives.py new file mode 100644 index 00000000..3157677b --- /dev/null +++ b/donna/context/tests/test_primitives.py @@ -0,0 +1,67 @@ +from typing import Any + +from donna.context.primitives import PrimitivesCache +from donna.domain.python_path import PythonPath +from donna.machine import errors as machine_errors +from donna.machine.tests import make as machine_make +from donna.machine.tests.test_primitives import sample_primitive + + +def _python_path(value: str) -> PythonPath: + normalized = PythonPath.normalize_raw_value(value) + assert normalized is not None + return PythonPath(normalized) + + +class TestPrimitivesCache: + def test_resolve__returns_primitive_from_python_path(self) -> None: + cache = PrimitivesCache() + + result = cache.resolve(machine_make.PRIMITIVE_PATH) + + assert result.is_ok() + assert result.unwrap() == sample_primitive + + def test_resolve__uses_cached_primitive_on_repeated_calls(self, mocker: Any) -> None: + cache = PrimitivesCache() + assert cache.resolve(machine_make.PRIMITIVE_PATH).is_ok() + import_module = mocker.patch("donna.context.primitives.importlib.import_module") + + result = cache.resolve(machine_make.PRIMITIVE_PATH) + + assert result.is_ok() + assert result.unwrap() == sample_primitive + import_module.assert_not_called() + + def test_resolve__rejects_path_without_attribute_name(self) -> None: + result = PrimitivesCache().resolve(_python_path("primitive")) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, machine_errors.PrimitiveInvalidImportPath) + assert error.import_path == "primitive" + + def test_resolve__reports_not_importable_module(self) -> None: + result = PrimitivesCache().resolve(_python_path("donna.machine.tests.missing.sample_primitive")) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, machine_errors.PrimitiveModuleNotImportable) + assert error.module_path == "donna.machine.tests.missing" + + def test_resolve__reports_missing_primitive_attribute(self) -> None: + result = PrimitivesCache().resolve(_python_path("donna.machine.tests.test_primitives.missing_primitive")) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, machine_errors.PrimitiveNotAvailable) + assert error.import_path == "donna.machine.tests.test_primitives.missing_primitive" + assert error.module_path == "donna.machine.tests.test_primitives" + + def test_resolve__reports_non_primitive_object(self) -> None: + result = PrimitivesCache().resolve(_python_path("donna.machine.tests.test_primitives.sample_non_primitive")) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, machine_errors.PrimitiveNotPrimitive) + assert error.import_path == "donna.machine.tests.test_primitives.sample_non_primitive" diff --git a/donna/context/tests/test_state.py b/donna/context/tests/test_state.py new file mode 100644 index 00000000..1067dcff --- /dev/null +++ b/donna/context/tests/test_state.py @@ -0,0 +1,86 @@ +import pathlib +from typing import Any + +from donna.context.state import StateCache +from donna.domain.constants import STATE_FILE_NAME +from donna.domain.paths import RelativeProjectPath +from donna.machine import errors as machine_errors +from donna.machine.tests import make as machine_make +from donna.workspaces import sessions as workspace_sessions +from donna.workspaces.config import Config + + +def _patch_session_globals(mocker: Any, tmp_path: pathlib.Path) -> None: + mocker.patch("donna.workspaces.sessions.project_dir", return_value=tmp_path) + mocker.patch( + "donna.workspaces.sessions.config", + return_value=Config(session_dir=RelativeProjectPath(pathlib.Path(".session/donna"))), + ) + + +class TestStateCache: + def test_load__reports_not_initialized_without_state_file(self, mocker: Any, tmp_path: pathlib.Path) -> None: + _patch_session_globals(mocker, tmp_path) + + result = StateCache().load() + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], machine_errors.SessionStateNotInitialized) + + def test_load__reads_consistent_state_from_workspace(self, mocker: Any, tmp_path: pathlib.Path) -> None: + _patch_session_globals(mocker, tmp_path) + state = machine_make.mutable_state(tasks=[machine_make.task()]).freeze() + workspace_sessions.write_state(state.to_json().encode("utf-8")) + + result = StateCache().load() + + assert result.is_ok() + assert result.unwrap() == state + + def test_load__returns_cached_state_while_fingerprint_matches(self, mocker: Any, tmp_path: pathlib.Path) -> None: + _patch_session_globals(mocker, tmp_path) + state = machine_make.mutable_state(tasks=[machine_make.task()]).freeze() + workspace_sessions.write_state(state.to_json().encode("utf-8")) + cache = StateCache() + assert cache.load().is_ok() + read_state = mocker.patch("donna.workspaces.sessions.read_state") + + result = cache.load() + + assert result.is_ok() + assert result.unwrap() == state + read_state.assert_not_called() + + def test_load__reports_cached_state_changed_externally(self, mocker: Any, tmp_path: pathlib.Path) -> None: + _patch_session_globals(mocker, tmp_path) + state = machine_make.mutable_state(tasks=[machine_make.task()]).freeze() + workspace_sessions.write_state(state.to_json().encode("utf-8")) + cache = StateCache() + assert cache.load().is_ok() + (tmp_path / ".session" / "donna" / STATE_FILE_NAME).write_bytes(b"external change") + + result = cache.load() + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], machine_errors.SessionStateChangedExternally) + + def test_save__writes_state_and_updates_cache(self, mocker: Any, tmp_path: pathlib.Path) -> None: + _patch_session_globals(mocker, tmp_path) + state = machine_make.mutable_state(tasks=[machine_make.task()]).freeze() + + result = StateCache().save(state) + + assert result.is_ok() + assert workspace_sessions.read_state() == state.to_json().encode("utf-8") + + def test_save__reports_cached_state_changed_externally(self, mocker: Any, tmp_path: pathlib.Path) -> None: + _patch_session_globals(mocker, tmp_path) + state = machine_make.mutable_state(tasks=[machine_make.task()]).freeze() + cache = StateCache() + assert cache.save(state).is_ok() + (tmp_path / ".session" / "donna" / STATE_FILE_NAME).write_bytes(b"external change") + + result = cache.save(state) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], machine_errors.SessionStateChangedExternally) diff --git a/specs/architecture/tests.md b/specs/architecture/tests.md index a6e1f245..94966f3f 100644 --- a/specs/architecture/tests.md +++ b/specs/architecture/tests.md @@ -112,6 +112,11 @@ Test data constructors reused by multiple test modules in the same package SHOUL `tests.make` MUST NOT contain assertions or behavior-verification helpers. +Tests MAY reuse constructors, fixtures, and helpers from another module's `tests` package when that avoids duplicating +non-owned setup data or supports cross-module integration coverage. + +Cross-module test helper reuse MUST remain test-only and MUST NOT make production modules depend on `tests` packages. + Test helper functions reused by multiple test modules in the same package SHOULD live in `tests/helpers`. `tests.helpers` SHOULD contain assertion helpers, setup helpers, cleanup helpers, and test workflow utilities. diff --git a/tach.toml b/tach.toml index c38c4ea6..d2338249 100644 --- a/tach.toml +++ b/tach.toml @@ -40,6 +40,7 @@ visibility = [] path = "donna.**.tests" depends_on = [ "donna.**", + "donna.**.tests", ] layer = "meta-tests" From ed31a9a5e62a8f4792b2f65490b296a3422cc312 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 13:39:43 +0200 Subject: [PATCH 48/98] primitives tests --- donna/primitives/artifacts/tests/__init__.py | 1 + .../artifacts/tests/test_workflow.py | 183 ++++++++++++++++++ donna/primitives/directives/tests/__init__.py | 1 + .../primitives/directives/tests/test_goto.py | 50 +++++ .../directives/tests/test_task_variable.py | 63 ++++++ donna/primitives/sections/tests/__init__.py | 1 + .../sections/tests/test_finish_workflow.py | 45 +++++ .../primitives/sections/tests/test_output.py | 83 ++++++++ .../sections/tests/test_request_action.py | 80 ++++++++ .../sections/tests/test_run_script.py | 172 ++++++++++++++++ donna/primitives/tests/__init__.py | 1 + donna/primitives/tests/make.py | 130 +++++++++++++ specs/architecture/tests.md | 11 +- 13 files changed, 820 insertions(+), 1 deletion(-) create mode 100644 donna/primitives/artifacts/tests/__init__.py create mode 100644 donna/primitives/artifacts/tests/test_workflow.py create mode 100644 donna/primitives/directives/tests/__init__.py create mode 100644 donna/primitives/directives/tests/test_goto.py create mode 100644 donna/primitives/directives/tests/test_task_variable.py create mode 100644 donna/primitives/sections/tests/__init__.py create mode 100644 donna/primitives/sections/tests/test_finish_workflow.py create mode 100644 donna/primitives/sections/tests/test_output.py create mode 100644 donna/primitives/sections/tests/test_request_action.py create mode 100644 donna/primitives/sections/tests/test_run_script.py create mode 100644 donna/primitives/tests/__init__.py create mode 100644 donna/primitives/tests/make.py diff --git a/donna/primitives/artifacts/tests/__init__.py b/donna/primitives/artifacts/tests/__init__.py new file mode 100644 index 00000000..ddbfacf2 --- /dev/null +++ b/donna/primitives/artifacts/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for donna.primitives.artifacts.""" diff --git a/donna/primitives/artifacts/tests/test_workflow.py b/donna/primitives/artifacts/tests/test_workflow.py new file mode 100644 index 00000000..8629f65d --- /dev/null +++ b/donna/primitives/artifacts/tests/test_workflow.py @@ -0,0 +1,183 @@ +from donna.domain.ids import SectionId +from donna.machine.artifacts import ArtifactSection +from donna.machine.changes import ChangeAddWorkUnit +from donna.machine.errors import ArtifactSectionNotFound +from donna.machine.operations import FsmMode, OperationMeta +from donna.primitives.artifacts.workflow import ( + FinalOperationHasTransitions, + NoOutgoingTransitions, + SectionIsNotAnOperation, + StartOperationMissing, + Workflow, + WorkflowMeta, + WorkflowSectionNotWorkflow, + WrongStartOperation, + find_workflow_sections, +) +from donna.primitives.tests import make + + +def workflow_section(meta: WorkflowMeta | None = None) -> ArtifactSection: + return make.artifact_section( + id=make.WORKFLOW_SECTION_ID, + kind=make.WORKFLOW_KIND, + title="Workflow", + primary=True, + meta=meta or WorkflowMeta(start_operation_id=make.START_SECTION_ID), + ) + + +def operation_section( + *, + id: SectionId, + transitions: set[SectionId] | None = None, + fsm_mode: FsmMode = FsmMode.normal, +) -> ArtifactSection: + return make.artifact_section( + id=id, + meta=OperationMeta( + fsm_mode=fsm_mode, + allowed_transtions=transitions or set(), + ), + ) + + +class TestFindWorkflowSections: + def test_follows_operation_transitions_once(self) -> None: + artifact = make.artifact( + [ + operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID}), + operation_section(id=make.NEXT_SECTION_ID, transitions={make.START_SECTION_ID}), + ] + ) + + sections = find_workflow_sections(make.START_SECTION_ID, artifact) + + assert sections == {make.START_SECTION_ID, make.NEXT_SECTION_ID} + + def test_stops_at_missing_or_non_operation_sections(self) -> None: + artifact = make.artifact( + [ + operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID, make.OTHER_SECTION_ID}), + make.artifact_section(id=make.NEXT_SECTION_ID), + ] + ) + + sections = find_workflow_sections(make.START_SECTION_ID, artifact) + + assert sections == {make.START_SECTION_ID, make.NEXT_SECTION_ID, make.OTHER_SECTION_ID} + + +class TestWorkflowMeta: + def test_cells_meta__serializes_explicit_start_operation(self) -> None: + meta = WorkflowMeta(start_operation_id=make.START_SECTION_ID) + + assert meta.cells_meta() == {"start_operation_id": "start"} + + def test_cells_meta__omits_missing_start_operation(self) -> None: + meta = WorkflowMeta() + + assert meta.cells_meta() == {} + + +class TestWorkflow: + def test_execute_section__adds_work_unit_for_resolved_start_operation(self) -> None: + artifact = make.artifact( + [ + workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID)), + operation_section(id=make.START_SECTION_ID, fsm_mode=FsmMode.final), + ] + ) + + result = Workflow().execute_section(make.task(), make.work_unit(), artifact, make.WORKFLOW_SECTION_ID) + + assert result.is_ok() + change = result.unwrap()[0] + assert isinstance(change, ChangeAddWorkUnit) + assert change.task_id == make.TASK_ID + assert change.operation_id == make.START_OPERATION_ID + + def test_validate_section__accepts_connected_workflow_ending_in_final_operation(self) -> None: + artifact = make.artifact( + [ + workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID)), + operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID}), + operation_section(id=make.NEXT_SECTION_ID, fsm_mode=FsmMode.final), + ] + ) + + result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) + + assert result.is_ok() + + def test_validate_section__uses_first_tail_section_as_default_start_operation(self) -> None: + artifact = make.artifact( + [ + workflow_section(WorkflowMeta()), + operation_section(id=make.START_SECTION_ID, fsm_mode=FsmMode.final), + ] + ) + + result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) + + assert result.is_ok() + + def test_validate_section__rejects_non_workflow_section_meta(self) -> None: + artifact = make.artifact([make.artifact_section(id=make.WORKFLOW_SECTION_ID, primary=True)]) + + result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], WorkflowSectionNotWorkflow) + + def test_validate_section__requires_start_operation_when_workflow_has_no_tail_sections(self) -> None: + artifact = make.artifact([workflow_section(WorkflowMeta())]) + + result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], StartOperationMissing) + + def test_validate_section__reports_wrong_explicit_start_operation(self) -> None: + artifact = make.artifact([workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID))]) + + result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) + + assert result.is_err() + errors = result.unwrap_err() + assert isinstance(errors[0], ArtifactSectionNotFound) + assert isinstance(errors[1], WrongStartOperation) + + def test_validate_section__reports_non_operation_workflow_section(self) -> None: + artifact = make.artifact( + [ + workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID)), + operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID}), + make.artifact_section(id=make.NEXT_SECTION_ID), + ] + ) + + result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], SectionIsNotAnOperation) + + def test_validate_section__reports_invalid_transition_rules(self) -> None: + artifact = make.artifact( + [ + workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID)), + operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID, make.DONE_SECTION_ID}), + operation_section( + id=make.NEXT_SECTION_ID, + transitions={make.DONE_SECTION_ID}, + fsm_mode=FsmMode.final, + ), + operation_section(id=make.DONE_SECTION_ID), + ] + ) + + result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) + + assert result.is_err() + errors = result.unwrap_err() + assert {type(error) for error in errors} == {FinalOperationHasTransitions, NoOutgoingTransitions} diff --git a/donna/primitives/directives/tests/__init__.py b/donna/primitives/directives/tests/__init__.py new file mode 100644 index 00000000..956f7279 --- /dev/null +++ b/donna/primitives/directives/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for donna.primitives.directives.""" diff --git a/donna/primitives/directives/tests/test_goto.py b/donna/primitives/directives/tests/test_goto.py new file mode 100644 index 00000000..a0fceea4 --- /dev/null +++ b/donna/primitives/directives/tests/test_goto.py @@ -0,0 +1,50 @@ +from pathlib import Path +from typing import Any + +from donna.domain.artifact_ids import ArtifactSectionId +from donna.primitives.directives import goto +from donna.primitives.directives.goto import GoTo, GoToInvalidArguments +from donna.primitives.tests import make +from donna.protocol.modes import Mode + + +class TestGoTo: + def test_prepare_arguments__requires_one_argument(self) -> None: + result = GoTo(analyze_id="goto")._prepare_arguments(make.template_context(artifact_id=make.ARTIFACT_ID)) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, GoToInvalidArguments) + assert error.provided_count == 0 + + def test_prepare_arguments__builds_artifact_section_id_in_current_artifact(self) -> None: + result = GoTo(analyze_id="goto")._prepare_arguments( + make.template_context(artifact_id=make.ARTIFACT_ID), "next" + ) + + assert result.is_ok() + assert result.unwrap() == (ArtifactSectionId("@/workflows/test.donna.md:next"),) + + def test_render_view__renders_complete_action_request_command(self, mocker: Any) -> None: + mocker.patch.object(goto.workspace_config, "protocol", return_value=Mode.llm) + mocker.patch.object(goto.workspace_config, "config_path", return_value=Path("/project/donna.toml")) + + result = GoTo(analyze_id="goto").render_view( + make.template_context(), + ArtifactSectionId("@/workflows/test.donna.md:next"), + ) + + assert result.is_ok() + assert ( + result.unwrap() == "donna -p llm --config '/project/donna.toml' " + "complete-action-request '@/workflows/test.donna.md:next'" + ) + + def test_render_analyze__renders_section_local_marker(self) -> None: + result = GoTo(analyze_id="goto").render_analyze( + make.template_context(), + ArtifactSectionId("@/workflows/test.donna.md:next"), + ) + + assert result.is_ok() + assert result.unwrap() == "$$donna goto next donna$$" diff --git a/donna/primitives/directives/tests/test_task_variable.py b/donna/primitives/directives/tests/test_task_variable.py new file mode 100644 index 00000000..36cc6e26 --- /dev/null +++ b/donna/primitives/directives/tests/test_task_variable.py @@ -0,0 +1,63 @@ +from donna.primitives.directives.task_variable import ( + TaskVariable, + TaskVariableInvalidArguments, + TaskVariableTaskContextMissing, +) +from donna.primitives.tests import make + + +class TestTaskVariable: + def test_prepare_arguments__requires_one_argument(self) -> None: + result = TaskVariable(analyze_id="task_variable")._prepare_arguments(make.template_context()) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, TaskVariableInvalidArguments) + assert error.provided_count == 0 + + def test_prepare_arguments__coerces_variable_name_to_string(self) -> None: + result = TaskVariable(analyze_id="task_variable")._prepare_arguments(make.template_context(), 42) + + assert result.is_ok() + assert result.unwrap() == ("42",) + + def test_render_view__describes_deferred_variable_substitution(self) -> None: + result = TaskVariable(analyze_id="task_variable").render_view(make.template_context(), "answer") + + assert result.is_ok() + assert "answer" in result.unwrap() + + def test_render_execute__returns_value_from_task_context_mapping(self) -> None: + result = TaskVariable(analyze_id="task_variable").render_execute( + make.template_context(task_context={"answer": 42}), + "answer", + ) + + assert result.is_ok() + assert result.unwrap() == 42 + + def test_render_execute__returns_value_from_current_task_context(self) -> None: + current_task = type("CurrentTask", (), {"context": {"answer": "yes"}})() + + result = TaskVariable(analyze_id="task_variable").render_execute( + make.template_context(current_task=current_task), + "answer", + ) + + assert result.is_ok() + assert result.unwrap() == "yes" + + def test_render_execute__reports_missing_task_context(self) -> None: + result = TaskVariable(analyze_id="task_variable").render_execute(make.template_context(), "answer") + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], TaskVariableTaskContextMissing) + + def test_render_execute__renders_warning_for_missing_variable(self) -> None: + result = TaskVariable(analyze_id="task_variable").render_execute( + make.template_context(task_context={}), + "answer", + ) + + assert result.is_ok() + assert "variable 'answer' does not found" in result.unwrap() diff --git a/donna/primitives/sections/tests/__init__.py b/donna/primitives/sections/tests/__init__.py new file mode 100644 index 00000000..e392d02f --- /dev/null +++ b/donna/primitives/sections/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for donna.primitives.sections.""" diff --git a/donna/primitives/sections/tests/test_finish_workflow.py b/donna/primitives/sections/tests/test_finish_workflow.py new file mode 100644 index 00000000..c6b6a5c7 --- /dev/null +++ b/donna/primitives/sections/tests/test_finish_workflow.py @@ -0,0 +1,45 @@ +from typing import Any + +from donna.machine.changes import ChangeFinishTask +from donna.machine.operations import FsmMode, OperationMeta +from donna.primitives.sections.finish_workflow import FinishWorkflow, FinishWorkflowConfig +from donna.primitives.tests import make + + +class TestFinishWorkflow: + def test_markdown_construct_meta__creates_final_operation_without_transitions(self) -> None: + result = FinishWorkflow().markdown_construct_meta( + artifact_id=make.ARTIFACT_ID, + source=make.section_source(), + section_config=FinishWorkflowConfig(id=make.DONE_SECTION_ID, kind=make.FINISH_WORKFLOW_KIND), + description="done", + ) + + assert result.is_ok() + meta = result.unwrap() + assert isinstance(meta, OperationMeta) + assert meta.fsm_mode == FsmMode.final + assert meta.allowed_transtions == set() + + def test_execute_section__emits_message_and_finishes_task(self, mocker: Any) -> None: + runtime_context = make.FakeRuntimeContext() + mocker.patch("donna.primitives.sections.finish_workflow.context", return_value=runtime_context) + artifact = make.artifact( + [ + make.artifact_section( + id=make.DONE_SECTION_ID, + description="Finished", + meta=OperationMeta(fsm_mode=FsmMode.final, allowed_transtions=set()), + ) + ] + ) + + result = FinishWorkflow().execute_section(make.task(), make.work_unit(), artifact, make.DONE_SECTION_ID) + + assert result.is_ok() + cell = runtime_context.output.cells[0] + assert cell.kind == "info" + assert cell.content == "Finished" + change = result.unwrap()[0] + assert isinstance(change, ChangeFinishTask) + assert change.task_id == make.TASK_ID diff --git a/donna/primitives/sections/tests/test_output.py b/donna/primitives/sections/tests/test_output.py new file mode 100644 index 00000000..32ddb267 --- /dev/null +++ b/donna/primitives/sections/tests/test_output.py @@ -0,0 +1,83 @@ +from typing import Any + +from donna.machine.changes import ChangeAddWorkUnit +from donna.machine.operations import FsmMode +from donna.primitives.sections.output import Output, OutputConfig, OutputMeta, OutputMissingNextOperation +from donna.primitives.tests import make + + +class TestOutput: + def test_markdown_construct_meta__records_next_operation_transition(self) -> None: + result = Output().markdown_construct_meta( + artifact_id=make.ARTIFACT_ID, + source=make.section_source(), + section_config=OutputConfig( + id=make.START_SECTION_ID, + kind=make.OUTPUT_KIND, + fsm_mode=FsmMode.start, + next_operation_id=make.NEXT_SECTION_ID, + ), + description="message", + ) + + assert result.is_ok() + meta = result.unwrap() + assert isinstance(meta, OutputMeta) + assert meta.fsm_mode == FsmMode.start + assert meta.next_operation_id == make.NEXT_SECTION_ID + assert meta.allowed_transtions == {make.NEXT_SECTION_ID} + + def test_markdown_construct_meta__allows_missing_next_operation_for_validation(self) -> None: + result = Output().markdown_construct_meta( + artifact_id=make.ARTIFACT_ID, + source=make.section_source(), + section_config=OutputConfig(id=make.START_SECTION_ID, kind=make.OUTPUT_KIND), + description="message", + ) + + assert result.is_ok() + meta = result.unwrap() + assert isinstance(meta, OutputMeta) + assert meta.next_operation_id is None + assert meta.allowed_transtions == set() + + def test_validate_section__requires_next_operation(self) -> None: + artifact = make.artifact( + [ + make.artifact_section( + id=make.START_SECTION_ID, + meta=OutputMeta(allowed_transtions=set(), next_operation_id=None), + ) + ] + ) + + result = Output().validate_section(artifact, make.START_SECTION_ID) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], OutputMissingNextOperation) + + def test_execute_section__emits_message_and_adds_next_work_unit(self, mocker: Any) -> None: + runtime_context = make.FakeRuntimeContext() + mocker.patch("donna.primitives.sections.output.context", return_value=runtime_context) + artifact = make.artifact( + [ + make.artifact_section( + id=make.START_SECTION_ID, + description="Agent message", + meta=OutputMeta( + allowed_transtions={make.NEXT_SECTION_ID}, + next_operation_id=make.NEXT_SECTION_ID, + ), + ) + ] + ) + + result = Output().execute_section(make.task(), make.work_unit(), artifact, make.START_SECTION_ID) + + assert result.is_ok() + cell = runtime_context.output.cells[0] + assert cell.kind == "info" + assert cell.content == "Agent message" + change = result.unwrap()[0] + assert isinstance(change, ChangeAddWorkUnit) + assert change.operation_id == make.ARTIFACT_ID + ":next" diff --git a/donna/primitives/sections/tests/test_request_action.py b/donna/primitives/sections/tests/test_request_action.py new file mode 100644 index 00000000..f94125d1 --- /dev/null +++ b/donna/primitives/sections/tests/test_request_action.py @@ -0,0 +1,80 @@ +import pytest +from pydantic import ValidationError + +from donna.domain.errors import InvalidIdentifier +from donna.machine.changes import ChangeAddActionRequest +from donna.machine.operations import FsmMode, OperationMeta +from donna.primitives.sections.request_action import RequestAction, RequestActionConfig, extract_transitions +from donna.primitives.tests import make + + +class TestExtractTransitions: + def test_extracts_unique_goto_directive_targets(self) -> None: + text = "\n".join( + [ + "$$donna goto next donna$$", + "regular text", + "$$donna goto done donna$$", + "$$donna goto next donna$$", + ] + ) + + transitions = extract_transitions(text) + + assert transitions == {make.NEXT_SECTION_ID, make.DONE_SECTION_ID} + + def test_rejects_goto_target_that_is_not_a_section_id(self) -> None: + with pytest.raises(InvalidIdentifier): + extract_transitions("$$donna goto folder/next donna$$") + + +class TestRequestActionConfig: + def test_validate_fsm_mode__rejects_final_mode(self) -> None: + with pytest.raises(ValidationError): + RequestActionConfig( + id=make.START_SECTION_ID, + kind=make.REQUEST_ACTION_KIND, + fsm_mode=FsmMode.final, + ) + + +class TestRequestAction: + def test_markdown_construct_meta__uses_analysis_markdown_for_allowed_transitions(self) -> None: + source = make.section_source_from_markdown( + "# Workflow\n\n## Ask\n\nChoose one.\n\n$$donna goto next donna$$\n", + section_index=1, + ) + + result = RequestAction().markdown_construct_meta( + artifact_id=make.ARTIFACT_ID, + source=source, + section_config=RequestActionConfig(id=make.START_SECTION_ID, kind=make.REQUEST_ACTION_KIND), + description="Choose one.", + ) + + assert result.is_ok() + meta = result.unwrap() + assert isinstance(meta, OperationMeta) + assert meta.allowed_transtions == {make.NEXT_SECTION_ID} + + def test_execute_section__adds_action_request_for_current_operation(self) -> None: + artifact = make.artifact( + [ + make.artifact_section( + id=make.START_SECTION_ID, + title="Ask agent", + description="Do the work", + meta=OperationMeta(fsm_mode=FsmMode.normal, allowed_transtions={make.NEXT_SECTION_ID}), + ) + ] + ) + + result = RequestAction().execute_section(make.task(), make.work_unit(), artifact, make.START_SECTION_ID) + + assert result.is_ok() + change = result.unwrap()[0] + assert isinstance(change, ChangeAddActionRequest) + assert change.action_request.id is None + assert change.action_request.title == "Ask agent" + assert change.action_request.request == "Do the work" + assert change.action_request.operation_id == make.START_OPERATION_ID diff --git a/donna/primitives/sections/tests/test_run_script.py b/donna/primitives/sections/tests/test_run_script.py new file mode 100644 index 00000000..53c574ef --- /dev/null +++ b/donna/primitives/sections/tests/test_run_script.py @@ -0,0 +1,172 @@ +from pathlib import Path +from typing import Any + +from donna.machine.changes import ChangeAddWorkUnit, ChangeSetTaskContext +from donna.primitives.sections import run_script +from donna.primitives.sections.run_script import ( + RunScript, + RunScriptConfig, + RunScriptGotoOnCodeIncludesZero, + RunScriptInvalidExitCode, + RunScriptMeta, + RunScriptMissingGotoOnFailure, + RunScriptMissingGotoOnSuccess, + RunScriptMissingScriptBlock, + _coerce_output, +) +from donna.primitives.tests import make + + +class TestRunScriptMeta: + def test_select_next_operation__uses_success_transition_for_zero_exit_code(self) -> None: + meta = RunScriptMeta( + allowed_transtions={make.NEXT_SECTION_ID, make.DONE_SECTION_ID}, + goto_on_success=make.NEXT_SECTION_ID, + goto_on_failure=make.DONE_SECTION_ID, + ) + + assert meta.select_next_operation(0) == make.NEXT_SECTION_ID + + def test_select_next_operation__uses_exit_code_specific_transition(self) -> None: + meta = RunScriptMeta( + allowed_transtions={make.NEXT_SECTION_ID, make.DONE_SECTION_ID}, + goto_on_success=make.NEXT_SECTION_ID, + goto_on_failure=make.DONE_SECTION_ID, + goto_on_code={"2": make.OTHER_SECTION_ID}, + ) + + assert meta.select_next_operation(2) == make.OTHER_SECTION_ID + + def test_select_next_operation__falls_back_to_failure_transition(self) -> None: + meta = RunScriptMeta( + allowed_transtions={make.NEXT_SECTION_ID, make.DONE_SECTION_ID}, + goto_on_success=make.NEXT_SECTION_ID, + goto_on_failure=make.DONE_SECTION_ID, + ) + + assert meta.select_next_operation(1) == make.DONE_SECTION_ID + + +class TestRunScript: + def test_markdown_construct_meta__builds_script_meta_and_allowed_transitions(self) -> None: + source = make.section_source( + configs=[ + make.code_source( + "bash", + "echo ok", + donna=True, + script=True, + ) + ] + ) + + result = RunScript().markdown_construct_meta( + artifact_id=make.ARTIFACT_ID, + source=source, + section_config=RunScriptConfig( + id=make.START_SECTION_ID, + kind=make.RUN_SCRIPT_KIND, + goto_on_success=make.NEXT_SECTION_ID, + goto_on_failure=make.DONE_SECTION_ID, + goto_on_code={"2": make.OTHER_SECTION_ID}, + save_stdout_to="stdout", + save_stderr_to="stderr", + timeout=5, + ), + description="run", + ) + + assert result.is_ok() + meta = result.unwrap() + assert isinstance(meta, RunScriptMeta) + assert meta.script == "echo ok" + assert meta.save_stdout_to == "stdout" + assert meta.save_stderr_to == "stderr" + assert meta.timeout == 5 + assert meta.allowed_transtions == {make.NEXT_SECTION_ID, make.DONE_SECTION_ID, make.OTHER_SECTION_ID} + + def test_markdown_construct_meta__requires_script_block(self) -> None: + result = RunScript().markdown_construct_meta( + artifact_id=make.ARTIFACT_ID, + source=make.section_source(), + section_config=RunScriptConfig(id=make.START_SECTION_ID, kind=make.RUN_SCRIPT_KIND), + description="run", + ) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], RunScriptMissingScriptBlock) + + def test_validate_section__collects_transition_config_errors(self) -> None: + artifact = make.artifact( + [ + make.artifact_section( + id=make.START_SECTION_ID, + meta=RunScriptMeta( + allowed_transtions={make.NEXT_SECTION_ID}, + goto_on_code={"not-an-int": make.NEXT_SECTION_ID, "0": make.DONE_SECTION_ID}, + ), + ) + ] + ) + + result = RunScript().validate_section(artifact, make.START_SECTION_ID) + + assert result.is_err() + assert {type(error) for error in result.unwrap_err()} == { + RunScriptMissingGotoOnSuccess, + RunScriptMissingGotoOnFailure, + RunScriptInvalidExitCode, + RunScriptGotoOnCodeIncludesZero, + } + + def test_execute_section__stores_outputs_and_adds_selected_next_work_unit(self, mocker: Any) -> None: + runtime_context = make.FakeRuntimeContext() + mocker.patch("donna.primitives.sections.run_script.context", return_value=runtime_context) + mocker.patch.object(run_script.workspace_config, "project_dir", return_value=Path("/project")) + run = mocker.patch.object(run_script, "_run_script", return_value=("stdout", "stderr", 2)) + artifact = make.artifact( + [ + make.artifact_section( + id=make.START_SECTION_ID, + title="Run checks", + meta=RunScriptMeta( + allowed_transtions={make.NEXT_SECTION_ID, make.DONE_SECTION_ID, make.OTHER_SECTION_ID}, + script="echo ok", + save_stdout_to="stdout_key", + save_stderr_to="stderr_key", + goto_on_success=make.NEXT_SECTION_ID, + goto_on_failure=make.DONE_SECTION_ID, + goto_on_code={"2": make.OTHER_SECTION_ID}, + ), + ) + ] + ) + + result = RunScript().execute_section(make.task(), make.work_unit(), artifact, make.START_SECTION_ID) + + assert result.is_ok() + run.assert_called_once_with(script="echo ok", timeout=60, project_dir=Path("/project")) + assert [message for _, message in runtime_context.journal.messages] == [ + "Run script `Run checks`", + "Script finished `Run checks`, exit code: 2, has stdout: True, has stderr: True`", + ] + changes = result.unwrap() + assert isinstance(changes[0], ChangeSetTaskContext) + assert changes[0].key == "stdout_key" + assert changes[0].value == "stdout" + assert isinstance(changes[1], ChangeSetTaskContext) + assert changes[1].key == "stderr_key" + assert changes[1].value == "stderr" + assert isinstance(changes[2], ChangeAddWorkUnit) + assert changes[2].operation_id == make.ARTIFACT_ID + ":other" + + +class TestCoerceOutput: + def test_returns_empty_string_for_missing_output(self) -> None: + assert _coerce_output(None) == "" + + def test_decodes_bytes_as_utf8_with_replacement(self) -> None: + assert _coerce_output(b"ok \xff") == "ok \ufffd" + + def test_returns_string_output_unchanged(self) -> None: + assert _coerce_output("ok") == "ok" diff --git a/donna/primitives/tests/__init__.py b/donna/primitives/tests/__init__.py new file mode 100644 index 00000000..e7d7faba --- /dev/null +++ b/donna/primitives/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for donna.primitives.""" diff --git a/donna/primitives/tests/make.py b/donna/primitives/tests/make.py new file mode 100644 index 00000000..24c623ea --- /dev/null +++ b/donna/primitives/tests/make.py @@ -0,0 +1,130 @@ +from typing import Any, cast + +from jinja2.runtime import Context + +from donna.core.entities import BaseEntity +from donna.core.errors import ErrorsList +from donna.core.result import Ok, Result +from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId +from donna.domain.id_paths import NormalizedRawIdPath +from donna.domain.ids import SectionId +from donna.domain.internal_ids import TaskId, WorkUnitId +from donna.domain.python_path import PythonPath +from donna.machine.artifacts import Artifact, ArtifactSection, ArtifactSectionMeta +from donna.machine.tasks import Task, WorkUnit +from donna.protocol.cells import Cell +from donna.protocol.journal import JournalRecord +from donna.workspaces import markdown +from donna.workspaces.markdown import CodeSource, SectionLevel, SectionSource + +ARTIFACT_ID = ArtifactId("@/workflows/test.donna.md") +WORKFLOW_SECTION_ID = SectionId("workflow") +START_SECTION_ID = SectionId("start") +NEXT_SECTION_ID = SectionId("next") +DONE_SECTION_ID = SectionId("done") +OTHER_SECTION_ID = SectionId("other") +WORKFLOW_OPERATION_ID = ArtifactSectionId("@/workflows/test.donna.md:workflow") +START_OPERATION_ID = ArtifactSectionId("@/workflows/test.donna.md:start") +TASK_ID = TaskId("T-1-b") +WORK_UNIT_ID = WorkUnitId("WU-2-c") + +WORKFLOW_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.artifacts.workflow.Workflow")) +TEXT_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.text.Text")) +REQUEST_ACTION_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.request_action.RequestAction")) +OUTPUT_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.output.Output")) +RUN_SCRIPT_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.run_script.RunScript")) +FINISH_WORKFLOW_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.finish_workflow.FinishWorkflow")) + + +def artifact_section( + *, + id: SectionId = START_SECTION_ID, + kind: PythonPath = TEXT_KIND, + title: str = "Step", + description: str = "Step description", + primary: bool = False, + meta: ArtifactSectionMeta | None = None, +) -> ArtifactSection: + return ArtifactSection( + id=id, + artifact_id=ARTIFACT_ID, + kind=kind, + title=title, + description=description, + primary=primary, + meta=meta or ArtifactSectionMeta(), + ) + + +def artifact(sections: list[ArtifactSection]) -> Artifact: + return Artifact(id=ARTIFACT_ID, sections=sections) + + +def task(*, id: TaskId = TASK_ID, workflow_id: ArtifactSectionId = WORKFLOW_OPERATION_ID) -> Task: + return Task.build(id=id, workflow_id=workflow_id) + + +def work_unit( + *, + id: WorkUnitId = WORK_UNIT_ID, + task_id: TaskId = TASK_ID, + operation_id: ArtifactSectionId = START_OPERATION_ID, + context: dict[str, Any] | None = None, +) -> WorkUnit: + return WorkUnit.build(id=id, task_id=task_id, operation_id=operation_id, context=context) + + +def code_source(format: str = "toml", content: str = "", **properties: str | bool) -> CodeSource: + return CodeSource(format=format, content=content, properties=properties) + + +def section_source( + *, + level: SectionLevel = SectionLevel.h2, + title: str | None = "Step", + configs: list[CodeSource] | None = None, +) -> SectionSource: + return SectionSource( + level=level, + title=title, + configs=configs or [], + original_tokens=[], + analysis_tokens=[], + ) + + +def section_source_from_markdown(text: str, section_index: int = 0) -> SectionSource: + source = markdown.parse(text, artifact_id=ARTIFACT_ID).unwrap()[section_index] + source.analysis_tokens.extend(source.original_tokens) + return source + + +def template_context(**values: Any) -> Context: + return cast(Context, values) + + +class FakeOutputEmitter: + def __init__(self) -> None: + self.cells: list[Cell] = [] + self.journal_records: list[JournalRecord] = [] + + def emit_cell(self, cell: Cell) -> None: + self.cells.append(cell) + + def emit_journal(self, record: JournalRecord) -> None: + self.journal_records.append(record) + + +class FakeJournal: + def __init__(self) -> None: + self.messages: list[tuple[str | None, str]] = [] + + def add(self, message: str, actor_id: str | None = None) -> Result[BaseEntity, ErrorsList]: + self.messages.append((actor_id, message)) + return Ok(BaseEntity()) + + +class FakeRuntimeContext: + def __init__(self) -> None: + self.output = FakeOutputEmitter() + self.journal = FakeJournal() diff --git a/specs/architecture/tests.md b/specs/architecture/tests.md index 94966f3f..d17a2b83 100644 --- a/specs/architecture/tests.md +++ b/specs/architecture/tests.md @@ -90,17 +90,26 @@ Tests SHOULD NOT use pytest `monkeypatch` for ordinary Python attribute replacem ## Test module layout -Each implementation module or submodule SHOULD have corresponding tests under a `tests` submodule owned by the same parent module. +Each implementation module or submodule SHOULD have corresponding tests under a `tests` submodule owned by the +nearest package that owns the behavior under test. The name of a test file MUST be built from the name of the tested module by adding the `test_` prefix. The structure of tests SHOULD mirror the implementation structure when that makes ownership clear. +When an implementation package contains nested component packages, tests for modules in those component packages +SHOULD live in a `tests` subpackage of the component package instead of being flattened into the top-level module's +test package. + +For example, tests for `./donna/primitives/artifacts/workflow.py` SHOULD live in +`./donna/primitives/artifacts/tests/test_workflow.py`, not in `./donna/primitives/tests/test_artifacts_workflow.py`. + Examples: - `./donna/core/utils.py` -> `./donna/core/tests/test_utils.py` - `./donna/domain/ids.py` -> `./donna/domain/tests/test_ids.py` - `./donna/workspaces/config.py` -> `./donna/workspaces/tests/test_config.py` +- `./donna/primitives/sections/output.py` -> `./donna/primitives/sections/tests/test_output.py` Cross-module integration tests MAY live under the module that owns the public boundary being exercised. From 1b0600d92d9e79fe2c8e6b425b862a0769a7a86b Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 13:48:43 +0200 Subject: [PATCH 49/98] protocol tests --- donna/protocol/errors.py | 2 +- donna/protocol/formatters/tests/__init__.py | 0 .../formatters/tests/test_automation.py | 36 +++++++ donna/protocol/formatters/tests/test_human.py | 35 +++++++ donna/protocol/formatters/tests/test_llm.py | 53 ++++++++++ donna/protocol/tests/__init__.py | 0 donna/protocol/tests/make.py | 33 +++++++ donna/protocol/tests/test_cell_shortcuts.py | 31 ++++++ donna/protocol/tests/test_cells.py | 56 +++++++++++ donna/protocol/tests/test_errors.py | 98 +++++++++++++++++++ donna/protocol/tests/test_journal.py | 49 ++++++++++ donna/protocol/tests/test_modes.py | 26 +++++ donna/protocol/tests/test_nodes.py | 57 +++++++++++ 13 files changed, 475 insertions(+), 1 deletion(-) create mode 100644 donna/protocol/formatters/tests/__init__.py create mode 100644 donna/protocol/formatters/tests/test_automation.py create mode 100644 donna/protocol/formatters/tests/test_human.py create mode 100644 donna/protocol/formatters/tests/test_llm.py create mode 100644 donna/protocol/tests/__init__.py create mode 100644 donna/protocol/tests/make.py create mode 100644 donna/protocol/tests/test_cell_shortcuts.py create mode 100644 donna/protocol/tests/test_cells.py create mode 100644 donna/protocol/tests/test_errors.py create mode 100644 donna/protocol/tests/test_journal.py create mode 100644 donna/protocol/tests/test_modes.py create mode 100644 donna/protocol/tests/test_nodes.py diff --git a/donna/protocol/errors.py b/donna/protocol/errors.py index 85398e53..2cfd6a4f 100644 --- a/donna/protocol/errors.py +++ b/donna/protocol/errors.py @@ -30,7 +30,7 @@ def meta(self) -> dict[str, MetaValue]: "error_code": self._error.code, } - for field_name, _field in self._error.model_fields.items(): + for field_name, _field in type(self._error).model_fields.items(): if field_name in ("code", "message", "cell_kind", "cell_media_type", "ways_to_fix"): continue diff --git a/donna/protocol/formatters/tests/__init__.py b/donna/protocol/formatters/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/donna/protocol/formatters/tests/test_automation.py b/donna/protocol/formatters/tests/test_automation.py new file mode 100644 index 00000000..728acf2d --- /dev/null +++ b/donna/protocol/formatters/tests/test_automation.py @@ -0,0 +1,36 @@ +import json + +from donna.protocol.formatters.automation import Formatter +from donna.protocol.tests.make import cell, journal_record + + +class TestFormatter: + def test_format_cell__serializes_cell_as_sorted_json_line(self) -> None: + formatted = Formatter().format_cell(cell()) + + assert json.loads(formatted) == { + "alpha": "first", + "content": "Sample content.", + "enabled": True, + "id": "EjRWeBI0VniSNFZ4EjRWeA", + "missing": None, + "zeta": 2, + } + assert formatted == ( + b'{"alpha":"first","content":"Sample content.","enabled":true,' + b'"id":"EjRWeBI0VniSNFZ4EjRWeA","missing":null,"zeta":2}\n' + ) + + def test_format_cell__serializes_missing_content_as_null(self) -> None: + formatted = Formatter().format_cell(cell(content=None)) + + assert json.loads(formatted)["content"] is None + + def test_format_journal__serializes_journal_record_as_json_line(self) -> None: + formatted = Formatter().format_journal(journal_record()) + + assert formatted == ( + b'{"actor_id":"agent","current_operation_id":"@/workflow.donna.md:operation",' + b'"current_task_id":"task-42-Q","current_work_unit_id":"work-unit-7-h",' + b'"message":"Completed step","timestamp":"2026-05-18T10:30:45Z"}\n' + ) diff --git a/donna/protocol/formatters/tests/test_human.py b/donna/protocol/formatters/tests/test_human.py new file mode 100644 index 00000000..f0b98d31 --- /dev/null +++ b/donna/protocol/formatters/tests/test_human.py @@ -0,0 +1,35 @@ +from donna.protocol.formatters.human import Formatter +from donna.protocol.tests.make import cell, journal_record + + +class TestFormatter: + def test_format_cell__renders_human_cell_with_sorted_metadata(self) -> None: + formatted = Formatter().format_cell(cell()).decode() + + assert formatted == ( + "----- DONNA CELL EjRWeBI0VniSNFZ4EjRWeA -----\n" + "kind = sample_status\n" + "media_type = text/markdown\n" + "alpha = first\n" + "enabled = True\n" + "missing = None\n" + "zeta = 2\n" + "\n" + "Sample content.\n" + "\n" + ) + + def test_format_cell__omits_media_type_and_content_when_absent(self) -> None: + formatted = Formatter().format_cell(cell(media_type=None, content=None, meta={})).decode() + + assert formatted == "----- DONNA CELL EjRWeBI0VniSNFZ4EjRWeA -----\nkind = sample_status\n\n" + + def test_format_journal__renders_time_short_task_actor_and_message(self) -> None: + formatted = Formatter().format_journal(journal_record()).decode() + + assert formatted == "10:30:45 [42] Completed step" + + def test_format_journal__uses_placeholders_for_missing_optional_fields(self) -> None: + formatted = Formatter().format_journal(journal_record(actor_id=None, current_task_id=None)).decode() + + assert formatted == "10:30:45 [-] <-> Completed step" diff --git a/donna/protocol/formatters/tests/test_llm.py b/donna/protocol/formatters/tests/test_llm.py new file mode 100644 index 00000000..31979814 --- /dev/null +++ b/donna/protocol/formatters/tests/test_llm.py @@ -0,0 +1,53 @@ +from donna.protocol.formatters.llm import Formatter +from donna.protocol.tests.make import cell, journal_record + + +class TestFormatter: + def test_format_cell__renders_llm_cell_with_sorted_metadata(self) -> None: + formatted = Formatter().format_cell(cell()).decode() + + assert formatted == ( + "--DONNA-CELL EjRWeBI0VniSNFZ4EjRWeA BEGIN--\n" + "kind=sample_status\n" + "media_type=text/markdown\n" + "alpha=first\n" + "enabled=True\n" + "missing=None\n" + "zeta=2\n" + "\n" + "Sample content.\n" + "--DONNA-CELL EjRWeBI0VniSNFZ4EjRWeA END--\n" + ) + + def test_format_cell__omits_media_type_and_content_when_absent(self) -> None: + formatted = Formatter().format_cell(cell(media_type=None, content=None, meta={})).decode() + + assert formatted == ( + "--DONNA-CELL EjRWeBI0VniSNFZ4EjRWeA BEGIN--\n" + "kind=sample_status\n" + "--DONNA-CELL EjRWeBI0VniSNFZ4EjRWeA END--\n" + ) + + def test_format_journal__renders_full_journal_context(self) -> None: + formatted = Formatter().format_journal(journal_record()).decode() + + assert formatted == ( + "2026-05-18T10:30:45+00:00 [task-42-Q] " + "[work-unit-7-h] [@/workflow.donna.md:operation] Completed step" + ) + + def test_format_journal__uses_placeholders_for_missing_optional_fields(self) -> None: + formatted = ( + Formatter() + .format_journal( + journal_record( + actor_id=None, + current_task_id=None, + current_work_unit_id=None, + current_operation_id=None, + ) + ) + .decode() + ) + + assert formatted == "2026-05-18T10:30:45+00:00 [-] <-> [-] [-] Completed step" diff --git a/donna/protocol/tests/__init__.py b/donna/protocol/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/donna/protocol/tests/make.py b/donna/protocol/tests/make.py new file mode 100644 index 00000000..583fcd0d --- /dev/null +++ b/donna/protocol/tests/make.py @@ -0,0 +1,33 @@ +import datetime +import uuid + +from donna.domain.artifact_ids import ArtifactId, artifact_section_id +from donna.domain.ids import SectionId +from donna.domain.internal_ids import TaskId, WorkUnitId +from donna.protocol.cells import Cell +from donna.protocol.journal import JournalRecord + + +def cell(**kwargs: object) -> Cell: + values = { + "id": uuid.UUID("12345678-1234-5678-9234-567812345678"), + "kind": "sample_status", + "media_type": "text/markdown", + "content": " Sample content. ", + "meta": {"zeta": 2, "alpha": "first", "enabled": True, "missing": None}, + } + values.update(kwargs) + return Cell.model_validate(values) + + +def journal_record(**kwargs: object) -> JournalRecord: + values = { + "timestamp": datetime.datetime(2026, 5, 18, 10, 30, 45, tzinfo=datetime.UTC), + "actor_id": "agent", + "message": "Completed step", + "current_task_id": TaskId.build("task", 42), + "current_work_unit_id": WorkUnitId.build("work-unit", 7), + "current_operation_id": artifact_section_id(ArtifactId("@/workflow.donna.md"), SectionId("operation")), + } + values.update(kwargs) + return JournalRecord.model_validate(values) diff --git a/donna/protocol/tests/test_cell_shortcuts.py b/donna/protocol/tests/test_cell_shortcuts.py new file mode 100644 index 00000000..b31db99c --- /dev/null +++ b/donna/protocol/tests/test_cell_shortcuts.py @@ -0,0 +1,31 @@ +from donna.protocol import cell_shortcuts + + +class TestOperationSucceeded: + def test_creates_markdown_operation_succeeded_cell(self) -> None: + cell = cell_shortcuts.operation_succeeded("Done.", operation="setup") + + assert cell.kind == "operation_succeeded" + assert cell.media_type == "text/markdown" + assert cell.content == "Done." + assert cell.meta == {"operation": "setup"} + + +class TestOperationFailed: + def test_creates_markdown_operation_failed_cell(self) -> None: + cell = cell_shortcuts.operation_failed("Failed.", operation="setup") + + assert cell.kind == "operation_failed" + assert cell.media_type == "text/markdown" + assert cell.content == "Failed." + assert cell.meta == {"operation": "setup"} + + +class TestInfo: + def test_creates_markdown_info_cell(self) -> None: + cell = cell_shortcuts.info("Ready.", scope="workspace") + + assert cell.kind == "info" + assert cell.media_type == "text/markdown" + assert cell.content == "Ready." + assert cell.meta == {"scope": "workspace"} diff --git a/donna/protocol/tests/test_cells.py b/donna/protocol/tests/test_cells.py new file mode 100644 index 00000000..0645fd89 --- /dev/null +++ b/donna/protocol/tests/test_cells.py @@ -0,0 +1,56 @@ +import uuid + +import pytest + +from donna.protocol.cells import Cell, to_meta_value +from donna.protocol.errors import ContentWithoutMediaType + + +class TestToMetaValue: + @pytest.mark.parametrize( + ("value", "expected"), + ( + ("value", "value"), + (1, 1), + (True, True), + (None, None), + (uuid.UUID("12345678-1234-5678-9234-567812345678"), "12345678-1234-5678-9234-567812345678"), + ), + ) + def test_converts_supported_metadata_values(self, value: object, expected: str | int | bool | None) -> None: + assert to_meta_value(value) == expected + + +class TestCell: + def test_build__requires_media_type_when_content_is_present(self) -> None: + with pytest.raises(ContentWithoutMediaType): + Cell.build(kind="status", media_type=None, content="content") + + def test_build__stores_metadata_as_cell_metadata(self) -> None: + cell = Cell.build(kind="status", media_type="text/plain", content="content", count=2, label="ready") + + assert cell.kind == "status" + assert cell.media_type == "text/plain" + assert cell.content == "content" + assert cell.meta == {"count": 2, "label": "ready"} + + def test_build_meta__creates_contentless_cell(self) -> None: + cell = Cell.build_meta(kind="status", count=0) + + assert cell.kind == "status" + assert cell.media_type is None + assert cell.content is None + assert cell.meta == {"count": 0} + + def test_build_markdown__uses_markdown_media_type(self) -> None: + cell = Cell.build_markdown(kind="status", content="# Done", count=1) + + assert cell.kind == "status" + assert cell.media_type == "text/markdown" + assert cell.content == "# Done" + assert cell.meta == {"count": 1} + + def test_short_id__returns_unpadded_urlsafe_base64_id(self) -> None: + cell = Cell.build_meta(kind="status").replace(id=uuid.UUID("12345678-1234-5678-9234-567812345678")) + + assert cell.short_id == "EjRWeBI0VniSNFZ4EjRWeA" diff --git a/donna/protocol/tests/test_errors.py b/donna/protocol/tests/test_errors.py new file mode 100644 index 00000000..7ce8746d --- /dev/null +++ b/donna/protocol/tests/test_errors.py @@ -0,0 +1,98 @@ +from decimal import Decimal + +from donna.core import errors as core_errors +from donna.protocol.errors import EnvironmentErrorNode, environment_error_node + + +class _SingleFixError(core_errors.EnvironmentError): + cell_kind: str = "sample_error" + code: str = "sample.single" + message: str = "Problem with {error.item}." + ways_to_fix: list[str] = ["Fix {error.item}."] + item: str + count: int + active: bool + optional: str | None = None + decimal_value: Decimal + + def content_intro(self) -> str: + return "Sample" + + +class _MultipleFixesError(core_errors.EnvironmentError): + cell_kind: str = "sample_error" + code: str = "sample.multiple" + message: str = "Problem with {error.item}." + ways_to_fix: list[str] = ["Fix {error.item}.", "Retry."] + item: str + + +class _MultilineError(core_errors.EnvironmentError): + cell_kind: str = "sample_error" + code: str = "sample.multiline" + message: str = "First line.\nSecond line." + + +class TestEnvironmentErrorNode: + def test_meta__includes_code_and_scalar_context_fields(self) -> None: + node = EnvironmentErrorNode( + _SingleFixError(item="artifact", count=3, active=True, decimal_value=Decimal("1.5")) + ) + + assert node.meta() == { + "error_code": "sample.single", + "item": "artifact", + "count": 3, + "active": True, + "decimal_value": "1.5", + } + + def test_content__renders_single_fix(self) -> None: + node = EnvironmentErrorNode( + _SingleFixError(item="artifact", count=3, active=True, decimal_value=Decimal("1.5")) + ) + + assert node.content() == "Sample: Problem with artifact.\nWay to fix: Fix artifact." + + def test_content__renders_multiple_fixes_as_list(self) -> None: + node = EnvironmentErrorNode(_MultipleFixesError(item="artifact")) + + assert node.content() == "Error: Problem with artifact.\n\nWays to fix:\n\n- Fix artifact.\n- Retry." + + def test_content__renders_multiline_message_as_block(self) -> None: + node = EnvironmentErrorNode(_MultilineError()) + + assert node.content() == "Error:\n\nFirst line.\nSecond line." + + def test_status__builds_error_cell(self) -> None: + node = EnvironmentErrorNode( + _SingleFixError(item="artifact", count=3, active=True, decimal_value=Decimal("1.5")) + ) + + cell = node.status() + + assert cell.kind == "sample_error" + assert cell.media_type == "text/markdown" + assert cell.content == "Sample: Problem with artifact.\nWay to fix: Fix artifact." + assert cell.meta == { + "error_code": "sample.single", + "item": "artifact", + "count": 3, + "active": True, + "decimal_value": "1.5", + } + + def test_journal_message__renders_single_line_message(self) -> None: + node = EnvironmentErrorNode(_MultilineError()) + + assert node.journal_message() == "First line. Second line." + + +class TestEnvironmentErrorNodeShortcut: + def test_returns_environment_error_node(self) -> None: + error = _MultipleFixesError(item="artifact") + + node = environment_error_node(error) + + assert isinstance(node, EnvironmentErrorNode) + assert node.status().meta["error_code"] == "sample.multiple" diff --git a/donna/protocol/tests/test_journal.py b/donna/protocol/tests/test_journal.py new file mode 100644 index 00000000..331626f5 --- /dev/null +++ b/donna/protocol/tests/test_journal.py @@ -0,0 +1,49 @@ +import json + +import pydantic +import pytest + +from donna.protocol.journal import JournalRecord, message_has_newlines, serialize_record +from donna.protocol.tests.make import journal_record + + +class TestMessageHasNewlines: + @pytest.mark.parametrize("message", ("line\nnext", "line\rnext")) + def test_detects_newline_characters(self, message: str) -> None: + assert message_has_newlines(message) + + def test_returns_false_for_single_line_message(self) -> None: + assert not message_has_newlines("single line") + + +class TestJournalRecord: + @pytest.mark.parametrize("message", ("line\nnext", "line\rnext")) + def test_validate_message_no_newlines__rejects_multiline_message(self, message: str) -> None: + with pytest.raises(pydantic.ValidationError): + JournalRecord.model_validate( + { + **journal_record().model_dump(), + "message": message, + } + ) + + +class TestSerializeRecord: + def test_serializes_record_as_sorted_compact_json(self) -> None: + record = journal_record() + + serialized = serialize_record(record) + + assert json.loads(serialized) == { + "actor_id": "agent", + "current_operation_id": "@/workflow.donna.md:operation", + "current_task_id": "task-42-Q", + "current_work_unit_id": "work-unit-7-h", + "message": "Completed step", + "timestamp": "2026-05-18T10:30:45Z", + } + assert serialized == ( + b'{"actor_id":"agent","current_operation_id":"@/workflow.donna.md:operation",' + b'"current_task_id":"task-42-Q","current_work_unit_id":"work-unit-7-h",' + b'"message":"Completed step","timestamp":"2026-05-18T10:30:45Z"}' + ) diff --git a/donna/protocol/tests/test_modes.py b/donna/protocol/tests/test_modes.py new file mode 100644 index 00000000..0fca6303 --- /dev/null +++ b/donna/protocol/tests/test_modes.py @@ -0,0 +1,26 @@ +import pytest + +from donna.protocol.errors import UnsupportedFormatterMode +from donna.protocol.formatters.automation import Formatter as AutomationFormatter +from donna.protocol.formatters.human import Formatter as HumanFormatter +from donna.protocol.formatters.llm import Formatter as LLMFormatter +from donna.protocol.modes import Mode, get_cell_formatter + + +class TestGetCellFormatter: + @pytest.mark.parametrize( + ("mode", "formatter_class"), + ( + (Mode.human, HumanFormatter), + (Mode.llm, LLMFormatter), + (Mode.automation, AutomationFormatter), + ), + ) + def test_returns_formatter_for_supported_mode(self, mode: Mode, formatter_class: type[object]) -> None: + assert isinstance(get_cell_formatter(mode), formatter_class) + + def test_unsupported_mode_raises_internal_error(self) -> None: + with pytest.raises(UnsupportedFormatterMode) as error_info: + get_cell_formatter("missing") # type: ignore[arg-type] + + assert error_info.value.arguments == {"mode": "missing"} diff --git a/donna/protocol/tests/test_nodes.py b/donna/protocol/tests/test_nodes.py new file mode 100644 index 00000000..c79ec101 --- /dev/null +++ b/donna/protocol/tests/test_nodes.py @@ -0,0 +1,57 @@ +from donna.protocol.cells import Cell +from donna.protocol.nodes import Node + + +class _SampleNode(Node): + __slots__ = ("_name", "_references") + + def __init__(self, name: str, references: list[Node] | None = None) -> None: + self._name = name + self._references = references or [] + + def status(self) -> Cell: + return Cell.build_markdown(kind="node_status", content=f"Status {self._name}") + + def info(self) -> Cell: + return Cell.build_markdown(kind="node_info", content=f"Info {self._name}") + + def references(self) -> list[Node]: + return self._references + + +class _StatusOnlyNode(Node): + __slots__ = ("_name",) + + def __init__(self, name: str) -> None: + self._name = name + + def status(self) -> Cell: + return Cell.build_markdown(kind="node_status", content=f"Status {self._name}") + + +class TestNode: + def test_info__defaults_to_status(self) -> None: + node = _StatusOnlyNode("root") + + info = node.info() + + assert info.kind == "node_status" + assert info.content == "Status root" + + def test_details__includes_info_for_node_and_references(self) -> None: + reference = _SampleNode("reference") + node = _SampleNode("root", references=[reference]) + + assert [cell.content for cell in node.details()] == ["Info root", "Info reference"] + + def test_index__includes_status_for_node_and_references(self) -> None: + reference = _SampleNode("reference") + node = _SampleNode("root", references=[reference]) + + assert [cell.content for cell in node.index()] == ["Status root", "Status reference"] + + def test_references__defaults_to_empty_list(self) -> None: + assert _StatusOnlyNode("root").references() == [] + + def test_components__defaults_to_empty_list(self) -> None: + assert _StatusOnlyNode("root").components() == [] From f9e3b66ae700f91da493624d8b52361ac47e2aa8 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 13:57:01 +0200 Subject: [PATCH 50/98] runtime tests --- donna/runtime/tests/__init__.py | 1 + donna/runtime/tests/make.py | 128 +++++++++++ donna/runtime/tests/test_sessions.py | 326 +++++++++++++++++++++++++++ 3 files changed, 455 insertions(+) create mode 100644 donna/runtime/tests/__init__.py create mode 100644 donna/runtime/tests/make.py create mode 100644 donna/runtime/tests/test_sessions.py diff --git a/donna/runtime/tests/__init__.py b/donna/runtime/tests/__init__.py new file mode 100644 index 00000000..7cdf4447 --- /dev/null +++ b/donna/runtime/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for donna.runtime.""" diff --git a/donna/runtime/tests/make.py b/donna/runtime/tests/make.py new file mode 100644 index 00000000..ff68ac68 --- /dev/null +++ b/donna/runtime/tests/make.py @@ -0,0 +1,128 @@ +from typing import Any, cast + +from donna.context.context import Context +from donna.context.context import reset_context as reset_runtime_context +from donna.context.context import set_context as set_runtime_context +from donna.core.errors import ErrorsList +from donna.core.result import Err, Ok, Result +from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId +from donna.domain.internal_ids import WorkUnitId +from donna.domain.python_path import PythonPath +from donna.machine.artifacts import Artifact +from donna.machine.context import ValueScope +from donna.machine.context import reset_context as reset_machine_context +from donna.machine.context import set_context as set_machine_context +from donna.machine.primitives import Primitive +from donna.machine.state import ConsistentState +from donna.machine.tasks import Task, WorkUnit +from donna.protocol.cells import Cell +from donna.workspaces.artifacts import ArtifactRenderContext + + +class FakeStateStore: + def __init__(self, state: ConsistentState | None = None, errors: ErrorsList | None = None) -> None: + self.state = state + self.errors = errors + self.loaded_count = 0 + self.saved: list[ConsistentState] = [] + + def load(self) -> Result[ConsistentState, ErrorsList]: + self.loaded_count += 1 + + if self.errors is not None: + return Err(self.errors) + + assert self.state is not None + return Ok(self.state) + + def save(self, state: ConsistentState) -> Result[None, ErrorsList]: + self.saved.append(state) + self.state = state + self.errors = None + return Ok(None) + + +class FakeArtifacts: + def __init__(self, artifact: Artifact) -> None: + self.artifact = artifact + self.loaded: list[tuple[ArtifactId, ArtifactRenderContext]] = [] + self.viewed: list[ArtifactId] = [] + self.executed: list[tuple[ArtifactId, Task, WorkUnit]] = [] + + def load(self, artifact_id: ArtifactId, render_context: ArtifactRenderContext) -> Result[Artifact, ErrorsList]: + self.loaded.append((artifact_id, render_context)) + return Ok(self.artifact) + + def load_for_view(self, artifact_id: ArtifactId) -> Result[Artifact, ErrorsList]: + self.viewed.append(artifact_id) + return Ok(self.artifact) + + def load_for_execution( + self, + artifact_id: ArtifactId, + task: Task, + work_unit: WorkUnit, + ) -> Result[Artifact, ErrorsList]: + self.executed.append((artifact_id, task, work_unit)) + return Ok(self.artifact) + + +class FakePrimitives: + def __init__(self, primitive: Primitive) -> None: + self.primitive = primitive + self.resolved: list[PythonPath] = [] + + def resolve(self, primitive_id: PythonPath) -> Result[Primitive, ErrorsList]: + self.resolved.append(primitive_id) + return Ok(self.primitive) + + +class FakeJournal: + def __init__(self) -> None: + self.records: list[dict[str, Any]] = [] + + def add(self, message: str, actor_id: str | None = None) -> Result[Any, ErrorsList]: + self.records.append({"message": message, "actor_id": actor_id}) + return Ok(None) + + +class FakeOutputEmitter: + def __init__(self) -> None: + self.cells: list[Cell] = [] + + def emit_cell(self, cell: Cell) -> None: + self.cells.append(cell) + + def emit_journal(self, record: object) -> None: + pass + + +class FakeRuntimeContext: + def __init__(self, *, state: ConsistentState | None, artifact: Artifact, primitive: Primitive) -> None: + self.state = FakeStateStore(state=state) + self.artifacts = FakeArtifacts(artifact) + self.primitives = FakePrimitives(primitive) + self.journal = FakeJournal() + self.output = FakeOutputEmitter() + self.current_work_unit_id: ValueScope[WorkUnitId] = ValueScope() + self.current_operation_id: ValueScope[ArtifactSectionId] = ValueScope() + + +class InstalledContext: + def __init__(self, context: FakeRuntimeContext) -> None: + self.context = context + self._runtime_token: Any = None + self._machine_token: Any = None + + def __enter__(self) -> FakeRuntimeContext: + self._runtime_token = set_runtime_context(cast(Context, self.context)) + self._machine_token = set_machine_context(self.context) + return self.context + + def __exit__(self, exc_type: object, exc: object, traceback: object) -> None: + reset_machine_context(self._machine_token) + reset_runtime_context(self._runtime_token) + + +def installed_context(context: FakeRuntimeContext) -> InstalledContext: + return InstalledContext(context) diff --git a/donna/runtime/tests/test_sessions.py b/donna/runtime/tests/test_sessions.py new file mode 100644 index 00000000..88b8239c --- /dev/null +++ b/donna/runtime/tests/test_sessions.py @@ -0,0 +1,326 @@ +from typing import Any + +from donna.core.errors import ErrorsList +from donna.core.result import Ok, Result +from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId +from donna.domain.id_paths import NormalizedRawIdPath +from donna.domain.ids import SectionId +from donna.domain.internal_ids import ActionRequestId, TaskId, WorkUnitId +from donna.domain.python_path import PythonPath +from donna.machine import errors as machine_errors +from donna.machine.action_requests import ActionRequest +from donna.machine.artifacts import Artifact, ArtifactSection +from donna.machine.changes import Change, ChangeAddActionRequest +from donna.machine.operations import OperationKind, OperationMeta +from donna.machine.state import ConsistentState, MutableState +from donna.machine.tasks import Task, WorkUnit +from donna.runtime import sessions +from donna.runtime.tests import make + +ARTIFACT_ID = ArtifactId("@/workflows/runtime.donna.md") +START_SECTION_ID = SectionId("start") +NEXT_SECTION_ID = SectionId("next") +OTHER_SECTION_ID = SectionId("other") +START_OPERATION_ID = ArtifactSectionId("@/workflows/runtime.donna.md:start") +NEXT_OPERATION_ID = ArtifactSectionId("@/workflows/runtime.donna.md:next") +OTHER_OPERATION_ID = ArtifactSectionId("@/workflows/runtime.donna.md:other") +OPERATION_KIND = PythonPath(NormalizedRawIdPath("donna.runtime.tests.test_sessions.operation")) +TASK_ID = TaskId("T-1-b") +WORK_UNIT_ID = WorkUnitId("WU-2-c") +ACTION_REQUEST_ID = ActionRequestId("AR-3-d") + + +class _NoopOperation(OperationKind): + def execute_section( + self, + task: Task, + unit: WorkUnit, + artifact: Artifact, + section_id: SectionId, + ) -> Result[list[Change], ErrorsList]: + return Ok([]) + + +class _RequestActionOperation(OperationKind): + def execute_section( + self, + task: Task, + unit: WorkUnit, + artifact: Artifact, + section_id: SectionId, + ) -> Result[list[Change], ErrorsList]: + request = ActionRequest.build( + title="Choose next", + request="Pick the next operation", + operation_id=unit.operation_id, + ) + return Ok([ChangeAddActionRequest(action_request=request)]) + + +def _operation_section( + *, + id: SectionId, + primary: bool = False, + allowed_transitions: set[SectionId] | None = None, +) -> ArtifactSection: + return ArtifactSection( + id=id, + artifact_id=ARTIFACT_ID, + kind=OPERATION_KIND, + title=f"Operation {id}", + description=f"Description for {id}", + primary=primary, + meta=OperationMeta(allowed_transtions=allowed_transitions or set()), + ) + + +def _artifact() -> Artifact: + return Artifact( + id=ARTIFACT_ID, + sections=[ + _operation_section(id=START_SECTION_ID, primary=True, allowed_transitions={NEXT_SECTION_ID}), + _operation_section(id=NEXT_SECTION_ID), + _operation_section(id=OTHER_SECTION_ID), + ], + ) + + +def _context( + *, + state: ConsistentState | None = None, + primitive: OperationKind | None = None, +) -> make.FakeRuntimeContext: + return make.FakeRuntimeContext( + state=state or MutableState.build().freeze(), + artifact=_artifact(), + primitive=primitive or _NoopOperation(), + ) + + +def _task() -> Task: + return Task.build(id=TASK_ID, workflow_id=START_OPERATION_ID) + + +def _work_unit() -> WorkUnit: + return WorkUnit.build(id=WORK_UNIT_ID, task_id=TASK_ID, operation_id=START_OPERATION_ID) + + +def _action_request(operation_id: ArtifactSectionId = START_OPERATION_ID) -> ActionRequest: + return ActionRequest( + id=ACTION_REQUEST_ID, + title="Choose next", + request="Pick next", + operation_id=operation_id, + ) + + +class TestLoadState: + def test_returns_loaded_state(self) -> None: + state = MutableState.build().freeze() + runtime_context = _context(state=state) + + with make.installed_context(runtime_context): + result = sessions.load_state() + + assert result.is_ok() + assert result.unwrap() == state + assert runtime_context.state.saved == [] + + def test_creates_empty_state_when_session_state_is_missing(self) -> None: + runtime_context = _context() + runtime_context.state.state = None + runtime_context.state.errors = [machine_errors.SessionStateNotInitialized()] + + with make.installed_context(runtime_context): + result = sessions.load_state() + + assert result.is_ok() + loaded_state = result.unwrap() + assert loaded_state == MutableState.build().freeze() + assert runtime_context.state.saved == [loaded_state] + + def test_returns_non_initialization_errors_without_saving(self) -> None: + error = machine_errors.SessionStateChangedExternally() + runtime_context = _context() + runtime_context.state.errors = [error] + + with make.installed_context(runtime_context): + result = sessions.load_state() + + assert result.is_err() + assert result.unwrap_err() == [error] + assert runtime_context.state.saved == [] + + +class TestNewSession: + def test_creates_fresh_state_and_reports_success(self) -> None: + existing_state = MutableState.build() + existing_state.mark_started() + runtime_context = _context(state=existing_state.freeze()) + + with make.installed_context(runtime_context): + result = sessions.new_session() + + assert result.is_ok() + assert runtime_context.state.saved == [MutableState.build().freeze()] + assert runtime_context.journal.records == [{"message": "Created new session state.", "actor_id": None}] + assert result.unwrap()[0].kind == "operation_succeeded" + + +class TestClear: + def test_resets_workspace_session_dir_and_reports_success(self, mocker: Any) -> None: + reset_dir = mocker.patch("donna.runtime.sessions.workspace_sessions.reset_dir") + + result = sessions.clear() + + assert result.is_ok() + reset_dir.assert_called_once_with() + assert result.unwrap()[0].kind == "operation_succeeded" + + +class TestContinue: + def test_runs_queued_work_until_action_request_and_returns_details(self) -> None: + state = MutableState( + tasks=[_task()], + work_units=[_work_unit()], + action_requests=[], + started=True, + last_id=2, + ) + runtime_context = _context(state=state.freeze(), primitive=_RequestActionOperation()) + + with make.installed_context(runtime_context): + result = sessions.continue_() + + assert result.is_ok() + final_state = runtime_context.state.state + assert final_state is not None + assert final_state.work_units == [] + assert len(final_state.action_requests) == 1 + assert runtime_context.state.saved[-1] == final_state + assert [cell.kind for cell in result.unwrap()] == ["session_state_status", "action_request"] + + +class TestStatus: + def test_returns_current_state_info_cell(self) -> None: + runtime_context = _context() + + with make.installed_context(runtime_context): + result = sessions.status() + + assert result.is_ok() + assert [cell.kind for cell in result.unwrap()] == ["session_state_status"] + + +class TestDetails: + def test_returns_current_state_detail_cells(self) -> None: + state = MutableState( + tasks=[_task()], + work_units=[], + action_requests=[_action_request()], + started=True, + last_id=3, + ) + runtime_context = _context(state=state.freeze()) + + with make.installed_context(runtime_context): + result = sessions.details() + + assert result.is_ok() + assert [cell.kind for cell in result.unwrap()] == ["session_state_status", "action_request"] + + +class TestStartWorkflow: + def test_starts_primary_workflow_operation_runs_it_and_returns_details(self) -> None: + runtime_context = _context(primitive=_RequestActionOperation()) + + with make.installed_context(runtime_context): + result = sessions.start_workflow(ARTIFACT_ID) + + assert result.is_ok() + final_state = runtime_context.state.state + assert final_state is not None + assert final_state.started + assert len(final_state.tasks) == 1 + assert final_state.work_units == [] + assert len(final_state.action_requests) == 1 + assert runtime_context.artifacts.loaded[0][0] == ARTIFACT_ID + assert runtime_context.artifacts.viewed == [ARTIFACT_ID] + assert [cell.kind for cell in result.unwrap()] == ["session_state_status", "action_request"] + + +class TestValidateOperationTransition: + def test_accepts_allowed_transition(self) -> None: + state = MutableState( + tasks=[_task()], + work_units=[], + action_requests=[_action_request()], + started=True, + last_id=3, + ) + runtime_context = _context() + + with make.installed_context(runtime_context): + result = sessions._validate_operation_transition(state, ACTION_REQUEST_ID, NEXT_OPERATION_ID) + + assert result.is_ok() + + def test_rejects_disallowed_transition(self) -> None: + state = MutableState( + tasks=[_task()], + work_units=[], + action_requests=[_action_request()], + started=True, + last_id=3, + ) + runtime_context = _context() + + with make.installed_context(runtime_context): + result = sessions._validate_operation_transition(state, ACTION_REQUEST_ID, OTHER_OPERATION_ID) + + assert result.is_err() + error = result.unwrap_err()[0] + assert isinstance(error, machine_errors.InvalidOperationTransition) + assert error.operation_id == START_OPERATION_ID + assert error.next_operation_id == OTHER_OPERATION_ID + + +class TestCompleteActionRequest: + def test_completes_request_runs_next_operation_and_returns_details(self) -> None: + state = MutableState( + tasks=[_task()], + work_units=[], + action_requests=[_action_request()], + started=True, + last_id=3, + ) + runtime_context = _context(state=state.freeze()) + + with make.installed_context(runtime_context): + result = sessions.complete_action_request(ACTION_REQUEST_ID, NEXT_OPERATION_ID) + + assert result.is_ok() + final_state = runtime_context.state.state + assert final_state is not None + assert final_state.action_requests == [] + assert final_state.work_units == [] + assert len(runtime_context.state.saved) == 2 + assert runtime_context.artifacts.executed[0][0] == ARTIFACT_ID + assert [cell.kind for cell in result.unwrap()] == ["session_state_status"] + + def test_returns_error_for_disallowed_transition_without_saving(self) -> None: + state = MutableState( + tasks=[_task()], + work_units=[], + action_requests=[_action_request()], + started=True, + last_id=3, + ) + runtime_context = _context(state=state.freeze()) + + with make.installed_context(runtime_context): + result = sessions.complete_action_request(ACTION_REQUEST_ID, OTHER_OPERATION_ID) + + assert result.is_err() + assert isinstance(result.unwrap_err()[0], machine_errors.InvalidOperationTransition) + assert runtime_context.state.saved == [] From 231042f14051c32783923b462daabf022107cdf1 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 14:00:29 +0200 Subject: [PATCH 51/98] lib tests --- donna/lib/tests/__init__.py | 1 + donna/lib/tests/test_init.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 donna/lib/tests/__init__.py create mode 100644 donna/lib/tests/test_init.py diff --git a/donna/lib/tests/__init__.py b/donna/lib/tests/__init__.py new file mode 100644 index 00000000..148e68a7 --- /dev/null +++ b/donna/lib/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for donna.lib.""" diff --git a/donna/lib/tests/test_init.py b/donna/lib/tests/test_init.py new file mode 100644 index 00000000..8c50a3a1 --- /dev/null +++ b/donna/lib/tests/test_init.py @@ -0,0 +1,20 @@ +import donna.lib as lib +from donna.primitives.artifacts import Workflow +from donna.primitives.directives import GoTo, TaskVariable +from donna.primitives.sections import FinishWorkflow, Output, RequestAction, RunScript, Text + + +class TestPrimitiveInitialization: + def test_section_and_artifact_primitives_are_initialized(self) -> None: + assert isinstance(lib.workflow, Workflow) + assert isinstance(lib.text, Text) + assert isinstance(lib.request_action, RequestAction) + assert isinstance(lib.finish, FinishWorkflow) + assert isinstance(lib.output, Output) + assert isinstance(lib.run_script, RunScript) + + def test_directive_primitives_are_initialized_with_analyze_ids(self) -> None: + assert isinstance(lib.goto, GoTo) + assert lib.goto.analyze_id == "goto" + assert isinstance(lib.task_variable, TaskVariable) + assert lib.task_variable.analyze_id == "task_variable" From 0e3af20bf40ddb9cb4528874ecc910d537e951d2 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 14:50:25 +0200 Subject: [PATCH 52/98] fixed types --- donna/cli/tests/__init__.py | 0 donna/cli/tests/conftest.py | 12 +++ donna/cli/tests/helpers.py | 76 ++++++++++++++++ donna/cli/tests/test_artifacts.py | 90 +++++++++++++++++++ donna/cli/tests/test_sessions.py | 28 ++++++ donna/cli/tests/test_skills.py | 27 ++++++ donna/cli/tests/test_version.py | 14 +++ donna/cli/tests/test_workspaces.py | 28 ++++++ donna/context/tests/test_artifacts.py | 21 +++-- donna/context/tests/test_journal.py | 11 +-- donna/context/tests/test_primitives.py | 4 +- donna/context/tests/test_state.py | 25 ++++-- donna/core/entities.py | 4 +- donna/core/errors.py | 4 +- donna/core/result.py | 12 +-- donna/domain/artifact_ids.py | 8 +- donna/domain/id_paths.py | 27 +++--- donna/domain/ids.py | 12 +-- donna/domain/internal_ids.py | 10 +-- donna/domain/paths.py | 2 +- donna/domain/tests/test_artifact_ids.py | 8 +- donna/domain/tests/test_id_paths.py | 3 +- donna/domain/tests/test_ids.py | 10 +-- donna/domain/tests/test_internal_ids.py | 6 +- donna/machine/artifacts.py | 6 +- donna/machine/context.py | 4 +- donna/machine/operations.py | 6 +- donna/machine/primitives.py | 9 +- donna/machine/tasks.py | 11 +-- donna/machine/templates.py | 41 +++++---- donna/machine/templates_context.py | 4 + donna/machine/tests/__init__.py | 1 - donna/machine/tests/helpers.py | 17 ++-- donna/machine/tests/make.py | 9 +- donna/machine/tests/test_action_requests.py | 1 + donna/machine/tests/test_artifacts.py | 7 +- donna/machine/tests/test_operations.py | 4 +- donna/machine/tests/test_primitives.py | 13 ++- donna/machine/tests/test_state.py | 5 ++ donna/machine/tests/test_templates.py | 13 ++- donna/primitives/artifacts/workflow.py | 4 +- donna/primitives/directives/goto.py | 18 ++-- donna/primitives/directives/task_variable.py | 22 ++--- .../primitives/directives/tests/test_goto.py | 5 +- .../directives/tests/test_task_variable.py | 8 +- .../sections/tests/test_finish_workflow.py | 4 +- .../primitives/sections/tests/test_output.py | 4 +- .../sections/tests/test_run_script.py | 5 +- donna/primitives/tests/make.py | 11 ++- donna/protocol/cells.py | 4 +- donna/protocol/formatters/automation.py | 4 +- donna/runtime/tests/make.py | 15 ++-- donna/runtime/tests/test_sessions.py | 4 +- donna/skills/tests/__init__.py | 1 - donna/workspaces/markdown.py | 9 +- donna/workspaces/markdown_parser.py | 8 +- donna/workspaces/templates.py | 10 ++- donna/workspaces/tests/make.py | 5 +- donna/workspaces/tests/test_artifacts.py | 66 +++++++++----- donna/workspaces/tests/test_config.py | 18 ++-- donna/workspaces/tests/test_errors.py | 19 ++-- donna/workspaces/tests/test_initialization.py | 14 +-- donna/workspaces/tests/test_journal.py | 10 ++- .../workspaces/tests/test_markdown_parser.py | 22 ++--- donna/workspaces/tests/test_sessions.py | 34 ++++--- donna/workspaces/tests/test_templates.py | 40 ++++++--- donna/workspaces/tests/test_utils.py | 10 ++- pyproject.toml | 1 + 68 files changed, 679 insertions(+), 289 deletions(-) create mode 100644 donna/cli/tests/__init__.py create mode 100644 donna/cli/tests/conftest.py create mode 100644 donna/cli/tests/helpers.py create mode 100644 donna/cli/tests/test_artifacts.py create mode 100644 donna/cli/tests/test_sessions.py create mode 100644 donna/cli/tests/test_skills.py create mode 100644 donna/cli/tests/test_version.py create mode 100644 donna/cli/tests/test_workspaces.py create mode 100644 donna/machine/templates_context.py diff --git a/donna/cli/tests/__init__.py b/donna/cli/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/donna/cli/tests/conftest.py b/donna/cli/tests/conftest.py new file mode 100644 index 00000000..3d4213be --- /dev/null +++ b/donna/cli/tests/conftest.py @@ -0,0 +1,12 @@ +import pytest +from pytest_mock import MockerFixture + +from donna.workspaces import config as workspace_config + + +@pytest.fixture(autouse=True) +def isolated_workspace_globals(mocker: MockerFixture) -> None: + mocker.patch.object(workspace_config.project_dir, "_value", None) + mocker.patch.object(workspace_config.config_path, "_value", None) + mocker.patch.object(workspace_config.config, "_value", None) + mocker.patch.object(workspace_config.protocol, "_value", None) diff --git a/donna/cli/tests/helpers.py b/donna/cli/tests/helpers.py new file mode 100644 index 00000000..482b43bb --- /dev/null +++ b/donna/cli/tests/helpers.py @@ -0,0 +1,76 @@ +import importlib +import json +import pathlib + +from click.testing import Result as CliResult +from typer.testing import CliRunner + +from donna.cli.application import app + +COMMAND_MODULES = ( + "donna.cli.commands.artifacts", + "donna.cli.commands.sessions", + "donna.cli.commands.skills", + "donna.cli.commands.version", + "donna.cli.commands.workspaces", +) + + +def load_cli_commands() -> None: + for module_name in COMMAND_MODULES: + importlib.import_module(module_name) + + +def make_runner() -> CliRunner: + load_cli_commands() + return CliRunner() + + +def invoke(args: list[str]) -> CliResult: + return make_runner().invoke(app, args) + + +def json_lines(output: str) -> list[dict[str, object]]: + return [json.loads(line) for line in output.splitlines() if line.startswith("{")] + + +def write_config(project_dir: pathlib.Path, *, workflow_dirs: list[str] | None = None) -> pathlib.Path: + workflow_dirs = workflow_dirs or ["workflows"] + config_path = project_dir / "donna.toml" + workflow_dirs_text = ", ".join(f'"{path}"' for path in workflow_dirs) + config_path.write_text(f"version = 1\nworkflow_dirs = [{workflow_dirs_text}]\n", encoding="utf-8") + return config_path + + +def write_workflow( + project_dir: pathlib.Path, + *, + path: str = "workflows/test.donna.md", + title: str = "Test Workflow", + description: str = "Workflow description.", +) -> pathlib.Path: + artifact_path = project_dir / path + artifact_path.parent.mkdir(parents=True, exist_ok=True) + artifact_path.write_text( + f"""# {title} + +```toml donna +id = "workflow" +kind = "donna.lib.workflow" +start_operation_id = "finish" +``` + +{description} + +## Finish + +```toml donna +id = "finish" +kind = "donna.lib.finish" +``` + +Done. +""", + encoding="utf-8", + ) + return artifact_path diff --git a/donna/cli/tests/test_artifacts.py b/donna/cli/tests/test_artifacts.py new file mode 100644 index 00000000..8118e00f --- /dev/null +++ b/donna/cli/tests/test_artifacts.py @@ -0,0 +1,90 @@ +import pathlib + +from donna.cli.tests import helpers + + +class TestList: + def test_lists_discovered_artifacts_with_selected_protocol(self, tmp_path: pathlib.Path) -> None: + config_path = helpers.write_config(tmp_path) + helpers.write_workflow(tmp_path) + + result = helpers.invoke(["--config", str(config_path), "-p", "llm", "list"]) + + assert result.exit_code == 0 + assert "kind=artifact_status" in result.output + assert "artifact_id=@/workflows/test.donna.md" in result.output + assert "artifact_title=Test Workflow" in result.output + + +class TestRender: + def test_renders_raw_markdown_without_cell_wrapping(self, tmp_path: pathlib.Path) -> None: + config_path = helpers.write_config(tmp_path) + helpers.write_workflow(tmp_path) + + result = helpers.invoke( + ["--config", str(config_path), "-p", "human", "render", "--mode", "view", "@/workflows/test.donna.md"] + ) + + assert result.exit_code == 0 + assert "# Test Workflow" in result.output + assert "----- DONNA CELL" not in result.output + assert "--DONNA-CELL" not in result.output + + +class TestValidate: + def test_all_option_validates_every_discovered_artifact(self, tmp_path: pathlib.Path) -> None: + config_path = helpers.write_config(tmp_path) + helpers.write_workflow(tmp_path) + + result = helpers.invoke(["--config", str(config_path), "-p", "automation", "validate", "--all"]) + + assert result.exit_code == 0 + records = helpers.json_lines(result.output) + assert records[0]["content"] == "All artifacts are valid" + + def test_explicit_artifact_argument_is_normalized(self, tmp_path: pathlib.Path) -> None: + config_path = helpers.write_config(tmp_path) + helpers.write_workflow(tmp_path) + + result = helpers.invoke( + [ + "--config", + str(config_path), + "-p", + "llm", + "validate", + "@/workflows/test.donna.md", + ] + ) + + assert result.exit_code == 0 + assert "kind=operation_succeeded" in result.output + + def test_rejects_all_option_combined_with_artifact_argument(self, tmp_path: pathlib.Path) -> None: + config_path = helpers.write_config(tmp_path) + helpers.write_workflow(tmp_path) + + result = helpers.invoke(["--config", str(config_path), "validate", "--all", "@/workflows/test.donna.md"]) + + assert result.exit_code == 2 + assert "Pass artifact ids or --all, not both." in result.output + + def test_rejects_missing_selection(self, tmp_path: pathlib.Path) -> None: + config_path = helpers.write_config(tmp_path) + + result = helpers.invoke(["--config", str(config_path), "validate"]) + + assert result.exit_code == 2 + assert "Pass artifact ids or --all." in result.output + + +class TestParseArtifactIdArgument: + def test_rejects_unsupported_artifact_extension(self, tmp_path: pathlib.Path) -> None: + config_path = helpers.write_config(tmp_path) + (tmp_path / "workflows").mkdir() + (tmp_path / "workflows" / "test.md").write_text("# Test\n", encoding="utf-8") + + result = helpers.invoke(["--config", str(config_path), "render", "--mode", "view", "@/workflows/test.md"]) + + assert result.exit_code != 0 + assert "Unsupported artifact extension" in result.output diff --git a/donna/cli/tests/test_sessions.py b/donna/cli/tests/test_sessions.py new file mode 100644 index 00000000..d5acc572 --- /dev/null +++ b/donna/cli/tests/test_sessions.py @@ -0,0 +1,28 @@ +import pathlib + +from donna.cli.tests import helpers + + +class TestNewSession: + def test_creates_fresh_session_state(self, tmp_path: pathlib.Path) -> None: + config_path = helpers.write_config(tmp_path) + + result = helpers.invoke(["--config", str(config_path), "-p", "llm", "new-session"]) + + assert result.exit_code == 0 + assert "kind=operation_succeeded" in result.output + assert (tmp_path / ".session" / "donna" / "state.json").is_file() + + +class TestStatus: + def test_reports_new_session_status(self, tmp_path: pathlib.Path) -> None: + config_path = helpers.write_config(tmp_path) + + result = helpers.invoke(["--config", str(config_path), "-p", "llm", "status"]) + + assert result.exit_code == 0 + assert "kind=session_state_status" in result.output + assert "tasks=0" in result.output + assert "queued_work_units=0" in result.output + assert "pending_action_requests=0" in result.output + assert "This is a new session" in result.output diff --git a/donna/cli/tests/test_skills.py b/donna/cli/tests/test_skills.py new file mode 100644 index 00000000..1f7dacd8 --- /dev/null +++ b/donna/cli/tests/test_skills.py @@ -0,0 +1,27 @@ +from donna.cli.tests import helpers + + +class TestSkill: + def test_default_document_outputs_usage_skill_without_workspace_config(self) -> None: + result = helpers.invoke(["-p", "llm", "skill"]) + + assert result.exit_code == 0 + assert "kind=skill" in result.output + assert "document=usage" in result.output + assert "# `donna` Usage" in result.output + + def test_document_argument_selects_skill_document(self) -> None: + result = helpers.invoke(["-p", "automation", "skill", "configuration"]) + + assert result.exit_code == 0 + records = helpers.json_lines(result.output) + assert records[0]["document"] == "configuration" + content = records[0]["content"] + assert isinstance(content, str) + assert content.startswith("# `donna` Configuration") + + def test_unknown_document_fails_as_invalid_cli_argument(self) -> None: + result = helpers.invoke(["skill", "missing"]) + + assert result.exit_code != 0 + assert "Invalid value" in result.output diff --git a/donna/cli/tests/test_version.py b/donna/cli/tests/test_version.py new file mode 100644 index 00000000..530a5042 --- /dev/null +++ b/donna/cli/tests/test_version.py @@ -0,0 +1,14 @@ +from pytest_mock import MockerFixture + +from donna.cli.tests import helpers + + +class TestVersion: + def test_prints_package_version_line(self, mocker: MockerFixture) -> None: + helpers.load_cli_commands() + mocker.patch("donna.cli.commands.version.importlib.metadata.version", return_value="9.8.7") + + result = helpers.invoke(["version"]) + + assert result.exit_code == 0 + assert result.output == "9.8.7\n" diff --git a/donna/cli/tests/test_workspaces.py b/donna/cli/tests/test_workspaces.py new file mode 100644 index 00000000..1b5e7806 --- /dev/null +++ b/donna/cli/tests/test_workspaces.py @@ -0,0 +1,28 @@ +import pathlib + +import pytest + +from donna.cli.tests import helpers + + +class TestInit: + def test_creates_config_in_current_directory_by_default( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path + ) -> None: + monkeypatch.chdir(tmp_path) + + result = helpers.invoke(["-p", "automation", "init"]) + + assert result.exit_code == 0 + assert (tmp_path / "donna.toml").is_file() + records = helpers.json_lines(result.output) + assert records[0]["content"] == "Donna project initialized successfully" + + def test_config_option_selects_target_file(self, tmp_path: pathlib.Path) -> None: + config_path = tmp_path / "custom.toml" + + result = helpers.invoke(["--config", str(config_path), "-p", "llm", "init"]) + + assert result.exit_code == 0 + assert config_path.is_file() + assert "kind=operation_succeeded" in result.output diff --git a/donna/context/tests/test_artifacts.py b/donna/context/tests/test_artifacts.py index 5f321294..5b0204d5 100644 --- a/donna/context/tests/test_artifacts.py +++ b/donna/context/tests/test_artifacts.py @@ -1,5 +1,6 @@ import pathlib -from typing import Any + +from pytest_mock import MockerFixture from donna.context.artifacts import ArtifactsCache from donna.context.tests import make @@ -19,7 +20,9 @@ def _write_artifact_file(tmp_path: pathlib.Path, name: str, content: str) -> pat class TestArtifactsCache: - def test_load__caches_view_rendered_artifact_by_render_mode(self, mocker: Any, tmp_path: pathlib.Path) -> None: + def test_load__caches_view_rendered_artifact_by_render_mode( + self, mocker: MockerFixture, tmp_path: pathlib.Path + ) -> None: path = _write_artifact_file(tmp_path, "workflow.donna.md", "# Workflow") raw_artifact = make.FakeRawArtifact(path, machine_make.artifact()) mocker.patch("donna.workspaces.artifacts.fetch_raw_artifact", return_value=Ok(raw_artifact)) @@ -36,7 +39,7 @@ def test_load__caches_view_rendered_artifact_by_render_mode(self, mocker: Any, t assert first_result.unwrap() == second_result.unwrap() assert raw_artifact.render_modes == [RenderMode.view] - def test_load__renders_execute_mode_every_time(self, mocker: Any, tmp_path: pathlib.Path) -> None: + def test_load__renders_execute_mode_every_time(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: path = _write_artifact_file(tmp_path, "workflow.donna.md", "# Workflow") raw_artifact = make.FakeRawArtifact(path, machine_make.artifact()) mocker.patch("donna.workspaces.artifacts.fetch_raw_artifact", return_value=Ok(raw_artifact)) @@ -51,7 +54,7 @@ def test_load__renders_execute_mode_every_time(self, mocker: Any, tmp_path: path assert raw_artifact.render_modes == [RenderMode.execute, RenderMode.execute] - def test_load__refreshes_stale_raw_artifact(self, mocker: Any, tmp_path: pathlib.Path) -> None: + def test_load__refreshes_stale_raw_artifact(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: first_path = _write_artifact_file(tmp_path, "first.donna.md", "# First") second_path = _write_artifact_file(tmp_path, "second.donna.md", "# Second with changed size") first_raw_artifact = make.FakeRawArtifact(first_path, machine_make.artifact()) @@ -73,7 +76,7 @@ def test_load__refreshes_stale_raw_artifact(self, mocker: Any, tmp_path: pathlib assert first_raw_artifact.render_modes == [RenderMode.view] assert second_raw_artifact.render_modes == [RenderMode.view] - def test_load__reports_missing_artifact_after_fetch(self, mocker: Any, tmp_path: pathlib.Path) -> None: + def test_load__reports_missing_artifact_after_fetch(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: missing_path = tmp_path / "missing.donna.md" raw_artifact = make.FakeRawArtifact(missing_path, machine_make.artifact()) mocker.patch("donna.workspaces.artifacts.fetch_raw_artifact", return_value=Ok(raw_artifact)) @@ -85,7 +88,7 @@ def test_load__reports_missing_artifact_after_fetch(self, mocker: Any, tmp_path: assert isinstance(error, workspace_errors.ArtifactNotFound) assert error.artifact_id == machine_make.ARTIFACT_ID - def test_invalidate__removes_cached_rendered_artifact(self, mocker: Any, tmp_path: pathlib.Path) -> None: + def test_invalidate__removes_cached_rendered_artifact(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: path = _write_artifact_file(tmp_path, "workflow.donna.md", "# Workflow") raw_artifact = make.FakeRawArtifact(path, machine_make.artifact()) mocker.patch("donna.workspaces.artifacts.fetch_raw_artifact", return_value=Ok(raw_artifact)) @@ -100,7 +103,9 @@ def test_invalidate__removes_cached_rendered_artifact(self, mocker: Any, tmp_pat assert raw_artifact.render_modes == [RenderMode.view, RenderMode.view] - def test_list__returns_loaded_artifacts_in_workspace_order(self, mocker: Any, tmp_path: pathlib.Path) -> None: + def test_list__returns_loaded_artifacts_in_workspace_order( + self, mocker: MockerFixture, tmp_path: pathlib.Path + ) -> None: first_id = machine_make.ARTIFACT_ID second_id = ArtifactId("@/workflows/other.donna.md") first_path = _write_artifact_file(tmp_path, "first.donna.md", "# First") @@ -121,7 +126,7 @@ def test_list__returns_loaded_artifacts_in_workspace_order(self, mocker: Any, tm assert result.is_ok() assert result.unwrap() == [first_artifact, second_artifact] - def test_list__collects_artifact_loading_errors(self, mocker: Any, tmp_path: pathlib.Path) -> None: + def test_list__collects_artifact_loading_errors(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: missing_id = ArtifactId("@/workflows/missing.donna.md") path = _write_artifact_file(tmp_path, "workflow.donna.md", "# Workflow") mocker.patch( diff --git a/donna/context/tests/test_journal.py b/donna/context/tests/test_journal.py index 5ede9298..527febed 100644 --- a/donna/context/tests/test_journal.py +++ b/donna/context/tests/test_journal.py @@ -1,7 +1,8 @@ import datetime -from typing import Any, cast +from typing import cast import pytest +from pytest_mock import MockerFixture from donna.context.context import Context from donna.context.journal import Journal @@ -41,19 +42,19 @@ class TestJournal: ], ) def test_smart_actor_id__depends_on_selected_protocol( - self, mocker: Any, mode: protocol_modes.Mode, actor_id: str + self, mocker: MockerFixture, mode: protocol_modes.Mode, actor_id: str ) -> None: mocker.patch("donna.context.journal.protocol_mode", return_value=mode) assert Journal(cast(Context, _FakeContext())).smart_actor_id() == actor_id - def test_smart_actor_id__raises_for_unsupported_mode(self, mocker: Any) -> None: + def test_smart_actor_id__raises_for_unsupported_mode(self, mocker: MockerFixture) -> None: mocker.patch("donna.context.journal.protocol_mode", return_value="unsupported") with pytest.raises(protocol_errors.UnsupportedFormatterMode): Journal(cast(Context, _FakeContext())).smart_actor_id() - def test_add__builds_writes_and_emits_journal_record(self, mocker: Any) -> None: + def test_add__builds_writes_and_emits_journal_record(self, mocker: MockerFixture) -> None: now = datetime.datetime(2026, 5, 18, 8, 30, tzinfo=datetime.UTC) fake_context = _FakeContext() mocker.patch("donna.context.journal.protocol_mode", return_value=protocol_modes.Mode.llm) @@ -75,7 +76,7 @@ def test_add__builds_writes_and_emits_journal_record(self, mocker: Any) -> None: write_record.assert_called_once_with(record) assert fake_context.output.journal_records == [record] - def test_add__uses_explicit_actor_id(self, mocker: Any) -> None: + def test_add__uses_explicit_actor_id(self, mocker: MockerFixture) -> None: fake_context = _FakeContext() mocker.patch("donna.context.journal.now", return_value=datetime.datetime(2026, 5, 18, tzinfo=datetime.UTC)) mocker.patch("donna.context.journal.workspace_journal.write_record", return_value=Ok(None)) diff --git a/donna/context/tests/test_primitives.py b/donna/context/tests/test_primitives.py index 3157677b..e4296d98 100644 --- a/donna/context/tests/test_primitives.py +++ b/donna/context/tests/test_primitives.py @@ -1,4 +1,4 @@ -from typing import Any +from pytest_mock import MockerFixture from donna.context.primitives import PrimitivesCache from donna.domain.python_path import PythonPath @@ -22,7 +22,7 @@ def test_resolve__returns_primitive_from_python_path(self) -> None: assert result.is_ok() assert result.unwrap() == sample_primitive - def test_resolve__uses_cached_primitive_on_repeated_calls(self, mocker: Any) -> None: + def test_resolve__uses_cached_primitive_on_repeated_calls(self, mocker: MockerFixture) -> None: cache = PrimitivesCache() assert cache.resolve(machine_make.PRIMITIVE_PATH).is_ok() import_module = mocker.patch("donna.context.primitives.importlib.import_module") diff --git a/donna/context/tests/test_state.py b/donna/context/tests/test_state.py index 1067dcff..d5e9d946 100644 --- a/donna/context/tests/test_state.py +++ b/donna/context/tests/test_state.py @@ -1,5 +1,6 @@ import pathlib -from typing import Any + +from pytest_mock import MockerFixture from donna.context.state import StateCache from donna.domain.constants import STATE_FILE_NAME @@ -10,7 +11,7 @@ from donna.workspaces.config import Config -def _patch_session_globals(mocker: Any, tmp_path: pathlib.Path) -> None: +def _patch_session_globals(mocker: MockerFixture, tmp_path: pathlib.Path) -> None: mocker.patch("donna.workspaces.sessions.project_dir", return_value=tmp_path) mocker.patch( "donna.workspaces.sessions.config", @@ -19,7 +20,9 @@ def _patch_session_globals(mocker: Any, tmp_path: pathlib.Path) -> None: class TestStateCache: - def test_load__reports_not_initialized_without_state_file(self, mocker: Any, tmp_path: pathlib.Path) -> None: + def test_load__reports_not_initialized_without_state_file( + self, mocker: MockerFixture, tmp_path: pathlib.Path + ) -> None: _patch_session_globals(mocker, tmp_path) result = StateCache().load() @@ -27,7 +30,7 @@ def test_load__reports_not_initialized_without_state_file(self, mocker: Any, tmp assert result.is_err() assert isinstance(result.unwrap_err()[0], machine_errors.SessionStateNotInitialized) - def test_load__reads_consistent_state_from_workspace(self, mocker: Any, tmp_path: pathlib.Path) -> None: + def test_load__reads_consistent_state_from_workspace(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: _patch_session_globals(mocker, tmp_path) state = machine_make.mutable_state(tasks=[machine_make.task()]).freeze() workspace_sessions.write_state(state.to_json().encode("utf-8")) @@ -37,7 +40,9 @@ def test_load__reads_consistent_state_from_workspace(self, mocker: Any, tmp_path assert result.is_ok() assert result.unwrap() == state - def test_load__returns_cached_state_while_fingerprint_matches(self, mocker: Any, tmp_path: pathlib.Path) -> None: + def test_load__returns_cached_state_while_fingerprint_matches( + self, mocker: MockerFixture, tmp_path: pathlib.Path + ) -> None: _patch_session_globals(mocker, tmp_path) state = machine_make.mutable_state(tasks=[machine_make.task()]).freeze() workspace_sessions.write_state(state.to_json().encode("utf-8")) @@ -51,7 +56,9 @@ def test_load__returns_cached_state_while_fingerprint_matches(self, mocker: Any, assert result.unwrap() == state read_state.assert_not_called() - def test_load__reports_cached_state_changed_externally(self, mocker: Any, tmp_path: pathlib.Path) -> None: + def test_load__reports_cached_state_changed_externally( + self, mocker: MockerFixture, tmp_path: pathlib.Path + ) -> None: _patch_session_globals(mocker, tmp_path) state = machine_make.mutable_state(tasks=[machine_make.task()]).freeze() workspace_sessions.write_state(state.to_json().encode("utf-8")) @@ -64,7 +71,7 @@ def test_load__reports_cached_state_changed_externally(self, mocker: Any, tmp_pa assert result.is_err() assert isinstance(result.unwrap_err()[0], machine_errors.SessionStateChangedExternally) - def test_save__writes_state_and_updates_cache(self, mocker: Any, tmp_path: pathlib.Path) -> None: + def test_save__writes_state_and_updates_cache(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: _patch_session_globals(mocker, tmp_path) state = machine_make.mutable_state(tasks=[machine_make.task()]).freeze() @@ -73,7 +80,9 @@ def test_save__writes_state_and_updates_cache(self, mocker: Any, tmp_path: pathl assert result.is_ok() assert workspace_sessions.read_state() == state.to_json().encode("utf-8") - def test_save__reports_cached_state_changed_externally(self, mocker: Any, tmp_path: pathlib.Path) -> None: + def test_save__reports_cached_state_changed_externally( + self, mocker: MockerFixture, tmp_path: pathlib.Path + ) -> None: _patch_session_globals(mocker, tmp_path) state = machine_make.mutable_state(tasks=[machine_make.task()]).freeze() cache = StateCache() diff --git a/donna/core/entities.py b/donna/core/entities.py index 570d8710..efc263b0 100644 --- a/donna/core/entities.py +++ b/donna/core/entities.py @@ -1,4 +1,4 @@ -from typing import Any, TypeVar +from typing import TypeVar import pydantic @@ -15,7 +15,7 @@ class BaseEntity(pydantic.BaseModel): from_attributes=False, ) - def replace(self: BASE_ENTITY, **kwargs: Any) -> BASE_ENTITY: + def replace(self: BASE_ENTITY, **kwargs: object) -> BASE_ENTITY: return self.model_copy(update=kwargs, deep=True) def to_json(self) -> str: diff --git a/donna/core/errors.py b/donna/core/errors.py index 3c9aa078..7d08b4cf 100644 --- a/donna/core/errors.py +++ b/donna/core/errors.py @@ -1,5 +1,3 @@ -from typing import Any - import pydantic from donna.core.entities import BaseEntity @@ -8,7 +6,7 @@ class InternalError(Exception): message = "An internal error occurred" - def __init__(self, **kwargs: Any) -> None: + def __init__(self, **kwargs: object) -> None: self.arguments = kwargs def error_message(self) -> str: diff --git a/donna/core/result.py b/donna/core/result.py index 02825d74..f2950354 100644 --- a/donna/core/result.py +++ b/donna/core/result.py @@ -5,10 +5,12 @@ from donna.core.errors import InternalError -T = TypeVar("T") +T = TypeVar("T", covariant=True) U = TypeVar("U") -E = TypeVar("E") +E = TypeVar("E", covariant=True) F = TypeVar("F") +TValue = TypeVar("TValue") +EValue = TypeVar("EValue") P = ParamSpec("P") @@ -27,7 +29,7 @@ class UnwrapErrError(ResultError): class Result(Generic[T, E]): __slots__ = ("_is_ok", "_value") - def __init__(self, is_ok: bool, value: T | E) -> None: + def __init__(self, is_ok: bool, value: object) -> None: self._is_ok = is_ok self._value = value @@ -75,11 +77,11 @@ def map_err(self, func: Callable[[E], F]) -> "Result[T, F]": return Result(True, cast(T, self._value)) -def Ok(value: T) -> Result[T, E]: +def Ok(value: TValue) -> Result[TValue, EValue]: return Result(True, value) -def Err(error: E) -> Result[T, E]: +def Err(error: EValue) -> Result[TValue, EValue]: return Result(False, error) diff --git a/donna/domain/artifact_ids.py b/donna/domain/artifact_ids.py index eab7867e..e977a682 100644 --- a/donna/domain/artifact_ids.py +++ b/donna/domain/artifact_ids.py @@ -25,7 +25,7 @@ def __init__(self, *, full_id: ArtifactSectionId, artifact_id: ArtifactId, secti self.section_id = section_id -def _raw_artifact_path(value: str) -> str | None: +def _raw_artifact_path(value: object) -> str | None: if not isinstance(value, str) or not value.startswith(ARTIFACT_ID_PREFIX): return None @@ -36,7 +36,7 @@ def _raw_artifact_path(value: str) -> str | None: return raw -def validate_artifact_id(value: str) -> bool: +def validate_artifact_id(value: object) -> bool: raw = _raw_artifact_path(value) if raw is None: return False @@ -51,7 +51,7 @@ def validate_artifact_id(value: str) -> bool: return bool(pathlib.PurePosixPath(parts[-1]).suffix) -def validate_artifact_section_id(value: str) -> bool: +def validate_artifact_section_id(value: object) -> bool: parts = split_artifact_section_id(value) return parts is not None @@ -74,7 +74,7 @@ def artifact_section_id(artifact_id: ArtifactId, local_id: SectionId | str) -> A return ArtifactSectionId(section_id) -def split_artifact_section_id(value: str | ArtifactSectionId) -> ArtifactSectionParts | None: +def split_artifact_section_id(value: object) -> ArtifactSectionParts | None: if not isinstance(value, str) or not value: return None diff --git a/donna/domain/id_paths.py b/donna/domain/id_paths.py index c3d03056..7e2b1e55 100644 --- a/donna/domain/id_paths.py +++ b/donna/domain/id_paths.py @@ -1,5 +1,6 @@ +from collections.abc import Callable from functools import total_ordering -from typing import Any, Self, Sequence, TypeVar +from typing import Self, Sequence, TypeVar from pydantic_core import PydanticCustomError, core_schema @@ -8,13 +9,13 @@ from donna.domain import errors as domain_errors -def _stringify_value(value: Any) -> str: +def _stringify_value(value: object) -> str: if isinstance(value, str): return value return repr(value) -def _pydantic_type_error(type_name: str, value: Any) -> PydanticCustomError: +def _pydantic_type_error(type_name: str, value: object) -> PydanticCustomError: return PydanticCustomError( "type_error", "{type_name} must be a str, got {actual_type}", @@ -22,7 +23,7 @@ def _pydantic_type_error(type_name: str, value: Any) -> PydanticCustomError: ) -def _pydantic_value_error(type_name: str, value: Any) -> PydanticCustomError: +def _pydantic_value_error(type_name: str, value: object) -> PydanticCustomError: return PydanticCustomError( "value_error", "Invalid {type_name}: {value}", @@ -33,7 +34,7 @@ def _pydantic_value_error(type_name: str, value: Any) -> PydanticCustomError: TParsed = TypeVar("TParsed") -def _invalid_format(id_type: str, value: Any) -> Result[TParsed, ErrorsList]: +def _invalid_format(id_type: str, value: object) -> Result[TParsed, ErrorsList]: return Err([domain_errors.InvalidIdFormat(id_type=id_type, value=_stringify_value(value))]) @@ -67,7 +68,7 @@ def _validate_parts(cls, parts: Sequence[str]) -> bool: return all(part.isidentifier() for part in parts) @classmethod - def validate(cls, value: str) -> bool: + def validate(cls, value: object) -> bool: if not isinstance(value, str) or not value: return False @@ -89,7 +90,7 @@ def raw_value(self) -> str: return self.delimiter.join(self.parts) @classmethod - def normalize_raw_value(cls, value: str) -> NormalizedRawIdPath | None: + def normalize_raw_value(cls, value: object) -> NormalizedRawIdPath | None: if not isinstance(value, str) or not value: return None @@ -126,15 +127,15 @@ def __lt__(self, other: object) -> bool: def __copy__(self) -> Self: return self - def __deepcopy__(self, memo: dict[int, Any]) -> Self: + def __deepcopy__(self, memo: dict[int, object]) -> Self: memo[id(self)] = self return self - def __setattr__(self, name: str, value: Any) -> None: + def __setattr__(self, name: str, value: object) -> None: raise AttributeError(f"{type(self).__name__} is immutable") @classmethod - def parse(cls, text: str) -> Result[Self, ErrorsList]: + def parse(cls, text: object) -> Result[Self, ErrorsList]: normalized = cls.normalize_raw_value(text) if normalized is None: return _invalid_format(cls.__name__, text) @@ -142,7 +143,7 @@ def parse(cls, text: str) -> Result[Self, ErrorsList]: return Ok(cls(normalized)) @classmethod - def _build_pydantic_schema(cls, validate_func: Any) -> core_schema.CoreSchema: + def _build_pydantic_schema(cls, validate_func: Callable[[object], "IdPath"]) -> core_schema.CoreSchema: str_then_validate = core_schema.no_info_after_validator_function( validate_func, core_schema.str_schema(), @@ -157,9 +158,9 @@ def _build_pydantic_schema(cls, validate_func: Any) -> core_schema.CoreSchema: ) @classmethod - def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> core_schema.CoreSchema: + def __get_pydantic_core_schema__(cls, source_type: object, handler: object) -> core_schema.CoreSchema: - def validate(v: Any) -> "IdPath": + def validate(v: object) -> "IdPath": if isinstance(v, cls): return v diff --git a/donna/domain/ids.py b/donna/domain/ids.py index 0858c53e..7666a5eb 100644 --- a/donna/domain/ids.py +++ b/donna/domain/ids.py @@ -1,4 +1,4 @@ -from typing import Any, TypeVar +from typing import TypeVar from pydantic_core import core_schema @@ -32,7 +32,7 @@ def __new__(cls, value: str) -> "Identifier": return super().__new__(cls, value) @classmethod - def validate(cls, value: str) -> bool: + def validate(cls, value: object) -> bool: if not isinstance(value, str): return False return value.isidentifier() @@ -48,9 +48,11 @@ def parse(cls: type[TIdentifier], text: str) -> Result[TIdentifier, ErrorsList]: return Ok(cls(text)) @classmethod - def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> core_schema.CoreSchema: # noqa: CCR001 + def __get_pydantic_core_schema__( + cls, source_type: object, handler: object + ) -> core_schema.CoreSchema: # noqa: CCR001 - def validate(v: Any) -> "Identifier": + def validate(v: object) -> "Identifier": if isinstance(v, cls): return v @@ -73,7 +75,7 @@ class SectionId(Identifier): __slots__ = () @classmethod - def validate(cls, value: str) -> bool: + def validate(cls, value: object) -> bool: if not isinstance(value, str): return False diff --git a/donna/domain/internal_ids.py b/donna/domain/internal_ids.py index 3c8d7fe0..763e8150 100644 --- a/donna/domain/internal_ids.py +++ b/donna/domain/internal_ids.py @@ -1,5 +1,3 @@ -from typing import Any - from pydantic_core import core_schema from donna.domain import errors as domain_errors @@ -37,7 +35,7 @@ def build(cls, prefix: str, value: int) -> "InternalId": return cls(f"{prefix}-{value}-{_id_crc(value)}") @classmethod - def validate(cls, id: str) -> bool: + def validate(cls, id: object) -> bool: if not isinstance(id, str): return False @@ -58,9 +56,11 @@ def short(self) -> str: return self.split("-")[1] @classmethod - def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> core_schema.CoreSchema: # noqa: CCR001 + def __get_pydantic_core_schema__( + cls, source_type: object, handler: object + ) -> core_schema.CoreSchema: # noqa: CCR001 - def validate(v: Any) -> "InternalId": + def validate(v: object) -> "InternalId": if isinstance(v, cls): return v diff --git a/donna/domain/paths.py b/donna/domain/paths.py index 0f3fbc5e..eb873af1 100644 --- a/donna/domain/paths.py +++ b/donna/domain/paths.py @@ -9,4 +9,4 @@ RelativeProjectPath = NewType("RelativeProjectPath", Path) ResolvedProjectPath = NewType("ResolvedProjectPath", Path) UntrustedPath = NewType("UntrustedPath", Path) -PathInput = UntrustedPath | ProjectRootPath | ProjectConfigPath +PathInput = Path | UntrustedPath | ProjectRootPath | ProjectConfigPath diff --git a/donna/domain/tests/test_artifact_ids.py b/donna/domain/tests/test_artifact_ids.py index 791c0dc8..ad339e96 100644 --- a/donna/domain/tests/test_artifact_ids.py +++ b/donna/domain/tests/test_artifact_ids.py @@ -1,5 +1,3 @@ -from typing import Any, cast - import pytest from donna.domain import errors @@ -42,7 +40,7 @@ def test_valid_canonical_artifact_id(self, value: str) -> None: ], ) def test_invalid_canonical_artifact_id(self, value: object) -> None: - assert not validate_artifact_id(cast(Any, value)) + assert not validate_artifact_id(value) class TestValidateArtifactSectionId: @@ -60,7 +58,7 @@ def test_valid_section_id(self) -> None: ], ) def test_invalid_section_id(self, value: object) -> None: - assert not validate_artifact_section_id(cast(Any, value)) + assert not validate_artifact_section_id(value) class TestArtifactPathParts: @@ -113,4 +111,4 @@ def test_valid_section_id(self) -> None: ], ) def test_invalid_section_id(self, value: object) -> None: - assert split_artifact_section_id(cast(Any, value)) is None + assert split_artifact_section_id(value) is None diff --git a/donna/domain/tests/test_id_paths.py b/donna/domain/tests/test_id_paths.py index 4409f32c..b95fefb1 100644 --- a/donna/domain/tests/test_id_paths.py +++ b/donna/domain/tests/test_id_paths.py @@ -1,5 +1,4 @@ import copy -from typing import Any, cast import pydantic import pytest @@ -102,4 +101,4 @@ def test_pydantic_validation__rejects_invalid_python_value(self) -> None: _PathEntity.model_validate({"path": "donna..ids"}) with pytest.raises(pydantic.ValidationError): - _PathEntity.model_validate({"path": cast(Any, 123)}) + _PathEntity.model_validate({"path": 123}) diff --git a/donna/domain/tests/test_ids.py b/donna/domain/tests/test_ids.py index 118c3bbb..0247186d 100644 --- a/donna/domain/tests/test_ids.py +++ b/donna/domain/tests/test_ids.py @@ -1,5 +1,3 @@ -from typing import Any, cast - import pydantic import pytest @@ -22,7 +20,7 @@ def test_validate__accepts_python_identifier(self) -> None: @pytest.mark.parametrize("value", ["", "1invalid", "not-valid", None]) def test_validate__rejects_non_identifier(self, value: object) -> None: - assert not Identifier.validate(cast(Any, value)) + assert not Identifier.validate(value) def test_init__raises_internal_error_for_invalid_value(self) -> None: with pytest.raises(errors.InvalidIdentifier): @@ -55,7 +53,7 @@ def test_pydantic_validation__rejects_invalid_identifier_value(self) -> None: _IdentifierEntity.model_validate({"identifier": "not-valid"}) with pytest.raises(pydantic.ValidationError): - _IdentifierEntity.model_validate({"identifier": cast(Any, 123)}) + _IdentifierEntity.model_validate({"identifier": 123}) class TestSectionId: @@ -65,7 +63,7 @@ def test_validate__accepts_artifact_slug_part(self, value: str) -> None: @pytest.mark.parametrize("value", ["", "---", "...", "section/id", "section id", None]) def test_validate__rejects_invalid_artifact_slug_part(self, value: object) -> None: - assert not SectionId.validate(cast(Any, value)) + assert not SectionId.validate(value) def test_parse__returns_section_id(self) -> None: result = SectionId.parse("section-1") @@ -94,4 +92,4 @@ def test_pydantic_validation__rejects_invalid_section_id_value(self) -> None: _SectionEntity.model_validate({"section_id": "---"}) with pytest.raises(pydantic.ValidationError): - _SectionEntity.model_validate({"section_id": cast(Any, 123)}) + _SectionEntity.model_validate({"section_id": 123}) diff --git a/donna/domain/tests/test_internal_ids.py b/donna/domain/tests/test_internal_ids.py index 33fbbf1a..ac8f3608 100644 --- a/donna/domain/tests/test_internal_ids.py +++ b/donna/domain/tests/test_internal_ids.py @@ -1,5 +1,3 @@ -from typing import Any, cast - import pydantic import pytest @@ -31,7 +29,7 @@ def test_build__creates_crc_protected_identifier(self) -> None: ], ) def test_validate__rejects_invalid_identifier(self, value: object) -> None: - assert not InternalId.validate(cast(Any, value)) + assert not InternalId.validate(value) def test_init__raises_internal_error_for_invalid_value(self) -> None: with pytest.raises(errors.InvalidInternalId): @@ -48,7 +46,7 @@ def test_pydantic_validation__rejects_invalid_internal_id_value(self) -> None: _InternalIdEntity.model_validate({"internal_id": "WU-0-b"}) with pytest.raises(pydantic.ValidationError): - _InternalIdEntity.model_validate({"internal_id": cast(Any, 123)}) + _InternalIdEntity.model_validate({"internal_id": 123}) class TestWorkUnitId: diff --git a/donna/machine/artifacts.py b/donna/machine/artifacts.py index e58ed815..432a8538 100644 --- a/donna/machine/artifacts.py +++ b/donna/machine/artifacts.py @@ -1,4 +1,4 @@ -from typing import Any +from collections.abc import Mapping from donna.core.entities import BaseEntity from donna.core.errors import ErrorsList @@ -8,7 +8,7 @@ from donna.domain.python_path import PythonPath from donna.machine.context import context from donna.machine.errors import ArtifactPrimarySectionMissing, ArtifactSectionNotFound, MultiplePrimarySectionsError -from donna.protocol.cells import Cell +from donna.protocol.cells import Cell, MetaValue from donna.protocol.errors import environment_error_node from donna.protocol.nodes import Node @@ -19,7 +19,7 @@ class ArtifactSectionConfig(BaseEntity): class ArtifactSectionMeta(BaseEntity): - def cells_meta(self) -> dict[str, Any]: + def cells_meta(self) -> Mapping[str, MetaValue]: return {} diff --git a/donna/machine/context.py b/donna/machine/context.py index 77c16765..ca379e47 100644 --- a/donna/machine/context.py +++ b/donna/machine/context.py @@ -3,7 +3,7 @@ import contextvars from collections.abc import Iterator from contextlib import contextmanager -from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar +from typing import TYPE_CHECKING, Generic, Protocol, TypeVar from donna.core.errors import ErrorsList from donna.core.result import Result @@ -58,7 +58,7 @@ def resolve(self, primitive_id: PythonPath) -> Result["Primitive", ErrorsList]: class MachineJournal(Protocol): - def add(self, message: str, actor_id: str | None = None) -> Result[Any, ErrorsList]: + def add(self, message: str, actor_id: str | None = None) -> Result[object, ErrorsList]: pass diff --git a/donna/machine/operations.py b/donna/machine/operations.py index feb15b8f..560f6de8 100644 --- a/donna/machine/operations.py +++ b/donna/machine/operations.py @@ -1,9 +1,11 @@ import enum -from typing import TYPE_CHECKING, Any +from collections.abc import Mapping +from typing import TYPE_CHECKING from donna.domain.ids import SectionId from donna.machine.artifacts import ArtifactSectionConfig, ArtifactSectionMeta from donna.machine.primitives import Primitive +from donna.protocol.cells import MetaValue if TYPE_CHECKING: pass @@ -27,5 +29,5 @@ class OperationMeta(ArtifactSectionMeta): fsm_mode: FsmMode = FsmMode.normal allowed_transtions: set[SectionId] - def cells_meta(self) -> dict[str, Any]: + def cells_meta(self) -> Mapping[str, MetaValue]: return {"fsm_mode": self.fsm_mode.value, "allowed_transtions": [str(t) for t in self.allowed_transtions]} diff --git a/donna/machine/primitives.py b/donna/machine/primitives.py index 46a9065a..7c0e22ec 100644 --- a/donna/machine/primitives.py +++ b/donna/machine/primitives.py @@ -1,7 +1,5 @@ import importlib -from typing import TYPE_CHECKING, Any, ClassVar - -from jinja2.runtime import Context +from typing import TYPE_CHECKING, ClassVar from donna.core.entities import BaseEntity from donna.core.errors import ErrorsList @@ -10,6 +8,7 @@ from donna.domain.python_path import PythonPath from donna.machine import errors as machine_errors from donna.machine.artifacts import ArtifactSectionConfig +from donna.machine.templates_context import DirectiveContext if TYPE_CHECKING: from donna.machine.artifacts import Artifact @@ -32,7 +31,9 @@ def execute_section( primitive_name=self.__class__.__name__, method_name="execute_section()" ) - def apply_directive(self, context: Context, *argv: Any, **kwargs: Any) -> Result[Any, ErrorsList]: + def apply_directive( + self, context: DirectiveContext, *argv: object, **kwargs: object + ) -> Result[object, ErrorsList]: raise machine_errors.PrimitiveMethodUnsupported( primitive_name=self.__class__.__name__, method_name="apply_directive()" ) diff --git a/donna/machine/tasks.py b/donna/machine/tasks.py index 351a814a..e6ebbd83 100644 --- a/donna/machine/tasks.py +++ b/donna/machine/tasks.py @@ -1,5 +1,6 @@ import copy -from typing import TYPE_CHECKING, Any +from collections.abc import Mapping +from typing import TYPE_CHECKING from donna.core.entities import BaseEntity from donna.core.errors import ErrorsList @@ -15,7 +16,7 @@ class Task(BaseEntity): id: TaskId workflow_id: ArtifactSectionId - context: dict[str, Any] + context: dict[str, object] @classmethod def build(cls, id: TaskId, workflow_id: ArtifactSectionId) -> "Task": @@ -30,7 +31,7 @@ class WorkUnit(BaseEntity): id: WorkUnitId task_id: TaskId operation_id: ArtifactSectionId - context: dict[str, Any] + context: dict[str, object] @classmethod def build( @@ -38,7 +39,7 @@ def build( id: WorkUnitId, task_id: TaskId, operation_id: ArtifactSectionId, - context: dict[str, Any] | None = None, + context: Mapping[str, object] | None = None, ) -> "WorkUnit": if context is None: @@ -48,7 +49,7 @@ def build( task_id=task_id, id=id, operation_id=operation_id, - context=copy.deepcopy(context), + context=copy.deepcopy(dict(context)), ) return unit diff --git a/donna/machine/templates.py b/donna/machine/templates.py index 3ec385fb..526aea54 100644 --- a/donna/machine/templates.py +++ b/donna/machine/templates.py @@ -1,18 +1,17 @@ import enum from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, TypeAlias - -from jinja2.runtime import Context +from typing import TYPE_CHECKING, TypeAlias from donna.core.errors import ErrorsList from donna.core.result import Ok, Result from donna.machine import errors as machine_errors from donna.machine.primitives import Primitive +from donna.machine.templates_context import DirectiveContext if TYPE_CHECKING: pass -PreparedDirectiveArguments: TypeAlias = tuple[Any, ...] +PreparedDirectiveArguments: TypeAlias = tuple[object, ...] PreparedDirectiveResult: TypeAlias = Result[PreparedDirectiveArguments, ErrorsList] @@ -38,7 +37,7 @@ class RenderMode(enum.StrEnum): class DirectiveUnsupportedRenderMode(machine_errors.InternalError): message: str = "Render mode {render_mode} not implemented in directive {directive_name}." - render_mode: Any + render_mode: object directive_name: str @@ -47,10 +46,10 @@ class Directive(Primitive, ABC): def apply_directive( # noqa: E704 self, - context: Context, - *argv: Any, - **kwargs: Any, - ) -> Result[Any, ErrorsList]: + context: DirectiveContext, + *argv: object, + **kwargs: object, + ) -> Result[object, ErrorsList]: render_mode = context["render_mode"] arguments_result = self._prepare_arguments(context, *argv, **kwargs) if arguments_result.is_err(): @@ -70,31 +69,31 @@ def apply_directive( # noqa: E704 def _prepare_arguments( self, - context: Context, - *argv: Any, - **kwargs: Any, + context: DirectiveContext, + *argv: object, + **kwargs: object, ) -> PreparedDirectiveResult: return Ok(argv) @abstractmethod def render_view( # noqa: E704 self, - context: Context, - *argv: Any, - ) -> Result[Any, ErrorsList]: ... + context: DirectiveContext, + *argv: object, + ) -> Result[object, ErrorsList]: ... def render_execute( self, - context: Context, - *argv: Any, - ) -> Result[Any, ErrorsList]: + context: DirectiveContext, + *argv: object, + ) -> Result[object, ErrorsList]: return self.render_view(context, *argv) def render_analyze( self, - context: Context, - *argv: Any, - ) -> Result[str, ErrorsList]: + context: DirectiveContext, + *argv: object, + ) -> Result[object, ErrorsList]: parts = [str(arg) for arg in argv] arguments = " ".join(parts) diff --git a/donna/machine/templates_context.py b/donna/machine/templates_context.py new file mode 100644 index 00000000..a40df803 --- /dev/null +++ b/donna/machine/templates_context.py @@ -0,0 +1,4 @@ +from collections.abc import Mapping +from typing import TypeAlias + +DirectiveContext: TypeAlias = Mapping[str, object] diff --git a/donna/machine/tests/__init__.py b/donna/machine/tests/__init__.py index 8b137891..e69de29b 100644 --- a/donna/machine/tests/__init__.py +++ b/donna/machine/tests/__init__.py @@ -1 +0,0 @@ - diff --git a/donna/machine/tests/helpers.py b/donna/machine/tests/helpers.py index c2f8e20d..2dd03c46 100644 --- a/donna/machine/tests/helpers.py +++ b/donna/machine/tests/helpers.py @@ -1,8 +1,7 @@ -from typing import Any - from donna.core.errors import ErrorsList from donna.core.result import Err, Ok, Result -from donna.domain.artifact_ids import ArtifactId +from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId +from donna.domain.internal_ids import WorkUnitId from donna.domain.python_path import PythonPath from donna.machine.artifacts import Artifact from donna.machine.context import ValueScope @@ -53,9 +52,9 @@ def resolve(self, primitive_id: PythonPath) -> Result[Primitive, ErrorsList]: class FakeJournal: def __init__(self) -> None: - self.records: list[dict[str, Any]] = [] + self.records: list[dict[str, object]] = [] - def add(self, message: str, actor_id: str | None = None) -> Result[Any, ErrorsList]: + def add(self, message: str, actor_id: str | None = None) -> Result[object, ErrorsList]: self.records.append({"message": message, "actor_id": actor_id}) return Ok(None) @@ -72,8 +71,8 @@ def __init__( self._artifacts = FakeArtifacts(artifact=artifact, error=artifact_error) self._primitives = FakePrimitives(primitive=primitive, error=primitive_error) self._journal = FakeJournal() - self._current_work_unit_id: ValueScope[Any] = ValueScope() - self._current_operation_id: ValueScope[Any] = ValueScope() + self._current_work_unit_id: ValueScope[WorkUnitId] = ValueScope() + self._current_operation_id: ValueScope[ArtifactSectionId] = ValueScope() @property def artifacts(self) -> FakeArtifacts: @@ -88,9 +87,9 @@ def journal(self) -> FakeJournal: return self._journal @property - def current_work_unit_id(self) -> ValueScope[Any]: + def current_work_unit_id(self) -> ValueScope[WorkUnitId]: return self._current_work_unit_id @property - def current_operation_id(self) -> ValueScope[Any]: + def current_operation_id(self) -> ValueScope[ArtifactSectionId]: return self._current_operation_id diff --git a/donna/machine/tests/make.py b/donna/machine/tests/make.py index 9a112637..e5e18219 100644 --- a/donna/machine/tests/make.py +++ b/donna/machine/tests/make.py @@ -1,6 +1,5 @@ -from typing import Any - from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId +from donna.domain.id_paths import NormalizedRawIdPath from donna.domain.ids import SectionId from donna.domain.internal_ids import ActionRequestId, TaskId, WorkUnitId from donna.domain.python_path import PythonPath @@ -12,7 +11,7 @@ ARTIFACT_ID = ArtifactId("@/workflows/test.donna.md") PRIMARY_SECTION_ID = SectionId("workflow") SECONDARY_SECTION_ID = SectionId("next") -PRIMITIVE_PATH = PythonPath("donna.machine.tests.test_primitives.sample_primitive") +PRIMITIVE_PATH = PythonPath(NormalizedRawIdPath("donna.machine.tests.test_primitives.sample_primitive")) PRIMARY_OPERATION_ID = ArtifactSectionId("@/workflows/test.donna.md:workflow") SECONDARY_OPERATION_ID = ArtifactSectionId("@/workflows/test.donna.md:next") TASK_ID = TaskId("T-1-b") @@ -20,7 +19,7 @@ ACTION_REQUEST_ID = ActionRequestId("AR-3-d") -def artifact_section( +def artifact_section( # noqa: CFQ002 *, id: SectionId = PRIMARY_SECTION_ID, artifact_id: ArtifactId = ARTIFACT_ID, @@ -57,7 +56,7 @@ def work_unit( id: WorkUnitId = WORK_UNIT_ID, task_id: TaskId = TASK_ID, operation_id: ArtifactSectionId = PRIMARY_OPERATION_ID, - context: dict[str, Any] | None = None, + context: dict[str, object] | None = None, ) -> WorkUnit: return WorkUnit.build(id=id, task_id=task_id, operation_id=operation_id, context=context) diff --git a/donna/machine/tests/test_action_requests.py b/donna/machine/tests/test_action_requests.py index 5a80b004..70e8aecf 100644 --- a/donna/machine/tests/test_action_requests.py +++ b/donna/machine/tests/test_action_requests.py @@ -31,5 +31,6 @@ def test_status__returns_action_request_status_cell(self) -> None: assert cell.kind == "action_request" assert cell.media_type == "text/markdown" + assert cell.content is not None assert "Do the thing" in cell.content assert cell.meta == {"action_request_id": str(make.ACTION_REQUEST_ID)} diff --git a/donna/machine/tests/test_artifacts.py b/donna/machine/tests/test_artifacts.py index 583253ad..9444bd7e 100644 --- a/donna/machine/tests/test_artifacts.py +++ b/donna/machine/tests/test_artifacts.py @@ -1,11 +1,8 @@ -from typing import Any - from donna.core.errors import ErrorsList from donna.core.result import Err, Result from donna.domain.ids import SectionId -from donna.domain.python_path import PythonPath from donna.machine import errors as machine_errors -from donna.machine.artifacts import Artifact, ArtifactNode, ArtifactSection, ArtifactSectionMeta, ArtifactSectionNode +from donna.machine.artifacts import Artifact, ArtifactNode, ArtifactSectionMeta, ArtifactSectionNode from donna.machine.context import reset_context, set_context from donna.machine.primitives import Primitive from donna.machine.tests import make @@ -13,7 +10,7 @@ class _Meta(ArtifactSectionMeta): - def cells_meta(self) -> dict[str, Any]: + def cells_meta(self) -> dict[str, str]: return {"custom": "value"} diff --git a/donna/machine/tests/test_operations.py b/donna/machine/tests/test_operations.py index 351e80ab..cb4cbdab 100644 --- a/donna/machine/tests/test_operations.py +++ b/donna/machine/tests/test_operations.py @@ -9,4 +9,6 @@ def test_cells_meta__serializes_fsm_mode_and_allowed_transitions(self) -> None: cell_meta = meta.cells_meta() assert cell_meta["fsm_mode"] == "final" - assert set(cell_meta["allowed_transtions"]) == {"next", "done"} + transitions = cell_meta["allowed_transtions"] + assert isinstance(transitions, list) + assert set(transitions) == {"next", "done"} diff --git a/donna/machine/tests/test_primitives.py b/donna/machine/tests/test_primitives.py index 3e0da76d..025a8d11 100644 --- a/donna/machine/tests/test_primitives.py +++ b/donna/machine/tests/test_primitives.py @@ -1,5 +1,6 @@ import pytest +from donna.domain.id_paths import NormalizedRawIdPath from donna.domain.python_path import PythonPath from donna.machine import errors as machine_errors from donna.machine.primitives import Primitive, resolve_primitive @@ -38,7 +39,7 @@ def test_success_from_python_path(self) -> None: assert result.unwrap() == sample_primitive def test_invalid_import_path_without_attribute_name(self) -> None: - result = resolve_primitive(PythonPath("primitive")) + result = resolve_primitive(PythonPath(NormalizedRawIdPath("primitive"))) assert result.is_err() error = result.unwrap_err()[0] @@ -46,7 +47,7 @@ def test_invalid_import_path_without_attribute_name(self) -> None: assert error.import_path == "primitive" def test_module_not_importable(self) -> None: - result = resolve_primitive(PythonPath("donna.machine.tests.missing.sample_primitive")) + result = resolve_primitive(PythonPath(NormalizedRawIdPath("donna.machine.tests.missing.sample_primitive"))) assert result.is_err() error = result.unwrap_err()[0] @@ -54,7 +55,9 @@ def test_module_not_importable(self) -> None: assert error.module_path == "donna.machine.tests.missing" def test_primitive_not_available(self) -> None: - result = resolve_primitive(PythonPath("donna.machine.tests.test_primitives.missing_primitive")) + result = resolve_primitive( + PythonPath(NormalizedRawIdPath("donna.machine.tests.test_primitives.missing_primitive")) + ) assert result.is_err() error = result.unwrap_err()[0] @@ -63,7 +66,9 @@ def test_primitive_not_available(self) -> None: assert error.module_path == "donna.machine.tests.test_primitives" def test_object_is_not_primitive(self) -> None: - result = resolve_primitive(PythonPath("donna.machine.tests.test_primitives.sample_non_primitive")) + result = resolve_primitive( + PythonPath(NormalizedRawIdPath("donna.machine.tests.test_primitives.sample_non_primitive")) + ) assert result.is_err() error = result.unwrap_err()[0] diff --git a/donna/machine/tests/test_state.py b/donna/machine/tests/test_state.py index 1e872fcc..b5911f0f 100644 --- a/donna/machine/tests/test_state.py +++ b/donna/machine/tests/test_state.py @@ -197,30 +197,35 @@ def test_status__reports_new_session(self) -> None: cell = MutableState.build().node().status() assert cell.kind == "session_state_status" + assert cell.content is not None assert "new session" in cell.content assert cell.meta == {"tasks": 0, "queued_work_units": 0, "pending_action_requests": 0} def test_status__reports_idle_session(self) -> None: cell = make.mutable_state(tasks=[], work_units=[], action_requests=[], started=True).node().status() + assert cell.content is not None assert "IDLE" in cell.content assert cell.meta["tasks"] == 0 def test_status__reports_pending_work_units(self) -> None: cell = make.mutable_state(tasks=[make.task()], work_units=[make.work_unit()]).node().status() + assert cell.content is not None assert "PENDING WORK UNITS" in cell.content assert cell.meta["queued_work_units"] == 1 def test_status__reports_pending_action_requests(self) -> None: cell = make.mutable_state(tasks=[make.task()], action_requests=[make.action_request()]).node().status() + assert cell.content is not None assert "AWAITING YOUR ACTION" in cell.content assert cell.meta["pending_action_requests"] == 1 def test_status__reports_unfinished_tasks(self) -> None: cell = make.mutable_state(tasks=[make.task()]).node().status() + assert cell.content is not None assert "unfinished TASKS" in cell.content assert cell.meta["tasks"] == 1 diff --git a/donna/machine/tests/test_templates.py b/donna/machine/tests/test_templates.py index 624d11be..b7d8a866 100644 --- a/donna/machine/tests/test_templates.py +++ b/donna/machine/tests/test_templates.py @@ -1,31 +1,30 @@ -from typing import Any - import pytest from donna.core.errors import ErrorsList from donna.core.result import Err, Ok, Result from donna.machine import errors as machine_errors from donna.machine.templates import Directive, DirectiveUnsupportedRenderMode, RenderMode +from donna.machine.templates_context import DirectiveContext class _Directive(Directive): analyze_id: str = "sample" - def render_view(self, context: dict[str, Any], *argv: Any) -> Result[Any, ErrorsList]: + def render_view(self, context: DirectiveContext, *argv: object) -> Result[object, ErrorsList]: return Ok({"mode": context["render_mode"], "argv": argv}) class _PreparingDirective(_Directive): def _prepare_arguments( - self, context: dict[str, Any], *argv: Any, **kwargs: Any - ) -> Result[tuple[Any, ...], ErrorsList]: + self, context: DirectiveContext, *argv: object, **kwargs: object + ) -> Result[tuple[object, ...], ErrorsList]: return Ok(("prepared", *argv, kwargs["extra"])) class _FailingDirective(_Directive): def _prepare_arguments( - self, context: dict[str, Any], *argv: Any, **kwargs: Any - ) -> Result[tuple[Any, ...], ErrorsList]: + self, context: DirectiveContext, *argv: object, **kwargs: object + ) -> Result[tuple[object, ...], ErrorsList]: return Err([machine_errors.PrimitiveInvalidImportPath(import_path="bad")]) diff --git a/donna/primitives/artifacts/workflow.py b/donna/primitives/artifacts/workflow.py index c061527a..0bb9cdba 100644 --- a/donna/primitives/artifacts/workflow.py +++ b/donna/primitives/artifacts/workflow.py @@ -1,3 +1,4 @@ +from collections.abc import Mapping from typing import TYPE_CHECKING, ClassVar, cast from donna.core import errors as core_errors @@ -9,6 +10,7 @@ from donna.machine.errors import ArtifactValidationError from donna.machine.operations import FsmMode, OperationMeta from donna.machine.primitives import Primitive +from donna.protocol.cells import MetaValue from donna.workspaces import markdown from donna.workspaces.markdown_parser import MarkdownSectionMixin @@ -107,7 +109,7 @@ class WorkflowConfig(ArtifactSectionConfig): class WorkflowMeta(ArtifactSectionMeta): start_operation_id: SectionId | None = None - def cells_meta(self) -> dict[str, object]: + def cells_meta(self) -> Mapping[str, MetaValue]: if self.start_operation_id is None: return {} return {"start_operation_id": str(self.start_operation_id)} diff --git a/donna/primitives/directives/goto.py b/donna/primitives/directives/goto.py index c52d13e3..1ebd3104 100644 --- a/donna/primitives/directives/goto.py +++ b/donna/primitives/directives/goto.py @@ -1,12 +1,11 @@ -from typing import Any, cast - -from jinja2.runtime import Context +from typing import cast from donna.core import errors as core_errors from donna.core.errors import ErrorsList from donna.core.result import Err, Ok, Result from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId, artifact_section_id, split_artifact_section_id from donna.machine.templates import Directive, PreparedDirectiveResult +from donna.machine.templates_context import DirectiveContext from donna.workspaces import config as workspace_config @@ -24,19 +23,21 @@ class GoToInvalidArguments(EnvironmentError): class GoTo(Directive): def _prepare_arguments( self, - context: Context, - *argv: Any, + context: DirectiveContext, + *argv: object, + **kwargs: object, ) -> PreparedDirectiveResult: if argv is None or len(argv) != 1: return Err([GoToInvalidArguments(provided_count=0 if argv is None else len(argv))]) artifact_id = cast(ArtifactId, context["artifact_id"]) - next_operation_id = artifact_section_id(artifact_id, argv[0]) + next_operation_id = artifact_section_id(artifact_id, str(argv[0])) return Ok((next_operation_id,)) - def render_view(self, context: Context, next_operation_id: ArtifactSectionId) -> Result[Any, ErrorsList]: + def render_view(self, context: DirectiveContext, *argv: object) -> Result[object, ErrorsList]: + next_operation_id = cast(ArtifactSectionId, argv[0]) protocol = workspace_config.protocol().value config_path = workspace_config.config_path() return Ok( @@ -44,7 +45,8 @@ def render_view(self, context: Context, next_operation_id: ArtifactSectionId) -> f"complete-action-request '{next_operation_id}'" ) - def render_analyze(self, context: Context, next_operation_id: ArtifactSectionId) -> Result[Any, ErrorsList]: + def render_analyze(self, context: DirectiveContext, *argv: object) -> Result[object, ErrorsList]: + next_operation_id = cast(ArtifactSectionId, argv[0]) parts = split_artifact_section_id(next_operation_id) assert parts is not None return Ok(f"$$donna {self.analyze_id} {parts.section_id} donna$$") diff --git a/donna/primitives/directives/task_variable.py b/donna/primitives/directives/task_variable.py index 8c0d86b4..42bd6caf 100644 --- a/donna/primitives/directives/task_variable.py +++ b/donna/primitives/directives/task_variable.py @@ -1,11 +1,10 @@ -from typing import Any, cast - -from jinja2.runtime import Context +from typing import cast from donna.core import errors as core_errors from donna.core.errors import ErrorsList from donna.core.result import Err, Ok, Result from donna.machine.templates import Directive, PreparedDirectiveResult +from donna.machine.templates_context import DirectiveContext class EnvironmentError(core_errors.EnvironmentError): @@ -28,8 +27,9 @@ class TaskVariableTaskContextMissing(EnvironmentError): class TaskVariable(Directive): def _prepare_arguments( self, - context: Context, - *argv: Any, + context: DirectiveContext, + *argv: object, + **kwargs: object, ) -> PreparedDirectiveResult: if argv is None or len(argv) != 1: return Err([TaskVariableInvalidArguments(provided_count=0 if argv is None else len(argv))]) @@ -38,13 +38,15 @@ def _prepare_arguments( return Ok((variable_name,)) - def render_view(self, context: Context, variable_name: str) -> Result[Any, ErrorsList]: + def render_view(self, context: DirectiveContext, *argv: object) -> Result[object, ErrorsList]: + variable_name = str(argv[0]) return Ok( "$$donna at the time of execution of this section here will placed a value " f"of the task variable '{variable_name}' donna$$" ) - def render_execute(self, context: Context, variable_name: str) -> Result[Any, ErrorsList]: + def render_execute(self, context: DirectiveContext, *argv: object) -> Result[object, ErrorsList]: + variable_name = str(argv[0]) task_context = self._resolve_task_context(context) if task_context is None: return Err([TaskVariableTaskContextMissing()]) @@ -61,13 +63,13 @@ def render_execute(self, context: Context, variable_name: str) -> Result[Any, Er return Ok(task_context[variable_name]) - def _resolve_task_context(self, context: Context) -> dict[str, Any] | None: + def _resolve_task_context(self, context: DirectiveContext) -> dict[str, object] | None: task = context.get("current_task") if task is not None and hasattr(task, "context"): - return cast(dict[str, Any], task.context) + return cast(dict[str, object], task.context) task_context = context.get("task_context") if isinstance(task_context, dict): - return cast(dict[str, Any], task_context) + return cast(dict[str, object], task_context) return None diff --git a/donna/primitives/directives/tests/test_goto.py b/donna/primitives/directives/tests/test_goto.py index a0fceea4..13fca633 100644 --- a/donna/primitives/directives/tests/test_goto.py +++ b/donna/primitives/directives/tests/test_goto.py @@ -1,5 +1,6 @@ from pathlib import Path -from typing import Any + +from pytest_mock import MockerFixture from donna.domain.artifact_ids import ArtifactSectionId from donna.primitives.directives import goto @@ -25,7 +26,7 @@ def test_prepare_arguments__builds_artifact_section_id_in_current_artifact(self) assert result.is_ok() assert result.unwrap() == (ArtifactSectionId("@/workflows/test.donna.md:next"),) - def test_render_view__renders_complete_action_request_command(self, mocker: Any) -> None: + def test_render_view__renders_complete_action_request_command(self, mocker: MockerFixture) -> None: mocker.patch.object(goto.workspace_config, "protocol", return_value=Mode.llm) mocker.patch.object(goto.workspace_config, "config_path", return_value=Path("/project/donna.toml")) diff --git a/donna/primitives/directives/tests/test_task_variable.py b/donna/primitives/directives/tests/test_task_variable.py index 36cc6e26..0231b729 100644 --- a/donna/primitives/directives/tests/test_task_variable.py +++ b/donna/primitives/directives/tests/test_task_variable.py @@ -25,7 +25,9 @@ def test_render_view__describes_deferred_variable_substitution(self) -> None: result = TaskVariable(analyze_id="task_variable").render_view(make.template_context(), "answer") assert result.is_ok() - assert "answer" in result.unwrap() + content = result.unwrap() + assert isinstance(content, str) + assert "answer" in content def test_render_execute__returns_value_from_task_context_mapping(self) -> None: result = TaskVariable(analyze_id="task_variable").render_execute( @@ -60,4 +62,6 @@ def test_render_execute__renders_warning_for_missing_variable(self) -> None: ) assert result.is_ok() - assert "variable 'answer' does not found" in result.unwrap() + content = result.unwrap() + assert isinstance(content, str) + assert "variable 'answer' does not found" in content diff --git a/donna/primitives/sections/tests/test_finish_workflow.py b/donna/primitives/sections/tests/test_finish_workflow.py index c6b6a5c7..d55f7f54 100644 --- a/donna/primitives/sections/tests/test_finish_workflow.py +++ b/donna/primitives/sections/tests/test_finish_workflow.py @@ -1,4 +1,4 @@ -from typing import Any +from pytest_mock import MockerFixture from donna.machine.changes import ChangeFinishTask from donna.machine.operations import FsmMode, OperationMeta @@ -21,7 +21,7 @@ def test_markdown_construct_meta__creates_final_operation_without_transitions(se assert meta.fsm_mode == FsmMode.final assert meta.allowed_transtions == set() - def test_execute_section__emits_message_and_finishes_task(self, mocker: Any) -> None: + def test_execute_section__emits_message_and_finishes_task(self, mocker: MockerFixture) -> None: runtime_context = make.FakeRuntimeContext() mocker.patch("donna.primitives.sections.finish_workflow.context", return_value=runtime_context) artifact = make.artifact( diff --git a/donna/primitives/sections/tests/test_output.py b/donna/primitives/sections/tests/test_output.py index 32ddb267..1842399d 100644 --- a/donna/primitives/sections/tests/test_output.py +++ b/donna/primitives/sections/tests/test_output.py @@ -1,4 +1,4 @@ -from typing import Any +from pytest_mock import MockerFixture from donna.machine.changes import ChangeAddWorkUnit from donna.machine.operations import FsmMode @@ -56,7 +56,7 @@ def test_validate_section__requires_next_operation(self) -> None: assert result.is_err() assert isinstance(result.unwrap_err()[0], OutputMissingNextOperation) - def test_execute_section__emits_message_and_adds_next_work_unit(self, mocker: Any) -> None: + def test_execute_section__emits_message_and_adds_next_work_unit(self, mocker: MockerFixture) -> None: runtime_context = make.FakeRuntimeContext() mocker.patch("donna.primitives.sections.output.context", return_value=runtime_context) artifact = make.artifact( diff --git a/donna/primitives/sections/tests/test_run_script.py b/donna/primitives/sections/tests/test_run_script.py index 53c574ef..240e2063 100644 --- a/donna/primitives/sections/tests/test_run_script.py +++ b/donna/primitives/sections/tests/test_run_script.py @@ -1,5 +1,6 @@ from pathlib import Path -from typing import Any + +from pytest_mock import MockerFixture from donna.machine.changes import ChangeAddWorkUnit, ChangeSetTaskContext from donna.primitives.sections import run_script @@ -119,7 +120,7 @@ def test_validate_section__collects_transition_config_errors(self) -> None: RunScriptGotoOnCodeIncludesZero, } - def test_execute_section__stores_outputs_and_adds_selected_next_work_unit(self, mocker: Any) -> None: + def test_execute_section__stores_outputs_and_adds_selected_next_work_unit(self, mocker: MockerFixture) -> None: runtime_context = make.FakeRuntimeContext() mocker.patch("donna.primitives.sections.run_script.context", return_value=runtime_context) mocker.patch.object(run_script.workspace_config, "project_dir", return_value=Path("/project")) diff --git a/donna/primitives/tests/make.py b/donna/primitives/tests/make.py index 24c623ea..3686dccf 100644 --- a/donna/primitives/tests/make.py +++ b/donna/primitives/tests/make.py @@ -1,6 +1,4 @@ -from typing import Any, cast - -from jinja2.runtime import Context +from typing import cast from donna.core.entities import BaseEntity from donna.core.errors import ErrorsList @@ -12,6 +10,7 @@ from donna.domain.python_path import PythonPath from donna.machine.artifacts import Artifact, ArtifactSection, ArtifactSectionMeta from donna.machine.tasks import Task, WorkUnit +from donna.machine.templates_context import DirectiveContext from donna.protocol.cells import Cell from donna.protocol.journal import JournalRecord from donna.workspaces import markdown @@ -69,7 +68,7 @@ def work_unit( id: WorkUnitId = WORK_UNIT_ID, task_id: TaskId = TASK_ID, operation_id: ArtifactSectionId = START_OPERATION_ID, - context: dict[str, Any] | None = None, + context: dict[str, object] | None = None, ) -> WorkUnit: return WorkUnit.build(id=id, task_id=task_id, operation_id=operation_id, context=context) @@ -99,8 +98,8 @@ def section_source_from_markdown(text: str, section_index: int = 0) -> SectionSo return source -def template_context(**values: Any) -> Context: - return cast(Context, values) +def template_context(**values: object) -> DirectiveContext: + return cast(DirectiveContext, values) class FakeOutputEmitter: diff --git a/donna/protocol/cells.py b/donna/protocol/cells.py index 29bc1321..91c9599e 100644 --- a/donna/protocol/cells.py +++ b/donna/protocol/cells.py @@ -5,12 +5,14 @@ from donna.core.entities import BaseEntity -MetaValue = str | int | bool | None +MetaValue = str | int | bool | None | list[str] def to_meta_value(value: object) -> MetaValue: if isinstance(value, (str, int, bool)) or value is None: return value + if isinstance(value, list) and all(isinstance(item, str) for item in value): + return value return str(value) diff --git a/donna/protocol/formatters/automation.py b/donna/protocol/formatters/automation.py index df3a4b8d..a78c48eb 100644 --- a/donna/protocol/formatters/automation.py +++ b/donna/protocol/formatters/automation.py @@ -1,6 +1,6 @@ import json -from donna.protocol.cells import Cell +from donna.protocol.cells import Cell, MetaValue from donna.protocol.formatters.base import Formatter as BaseFormatter from donna.protocol.journal import JournalRecord, serialize_record @@ -13,7 +13,7 @@ def _json_line(self, data: object) -> bytes: ) def format_cell(self, cell: Cell) -> bytes: - data: dict[str, str | int | bool | None] = {"id": cell.short_id} + data: dict[str, MetaValue] = {"id": cell.short_id} for meta_key, meta_value in sorted(cell.meta.items()): data[meta_key] = meta_value diff --git a/donna/runtime/tests/make.py b/donna/runtime/tests/make.py index ff68ac68..945bb26a 100644 --- a/donna/runtime/tests/make.py +++ b/donna/runtime/tests/make.py @@ -1,4 +1,5 @@ -from typing import Any, cast +import contextvars +from typing import cast from donna.context.context import Context from donna.context.context import reset_context as reset_runtime_context @@ -9,7 +10,7 @@ from donna.domain.internal_ids import WorkUnitId from donna.domain.python_path import PythonPath from donna.machine.artifacts import Artifact -from donna.machine.context import ValueScope +from donna.machine.context import MachineContext, ValueScope from donna.machine.context import reset_context as reset_machine_context from donna.machine.context import set_context as set_machine_context from donna.machine.primitives import Primitive @@ -79,9 +80,9 @@ def resolve(self, primitive_id: PythonPath) -> Result[Primitive, ErrorsList]: class FakeJournal: def __init__(self) -> None: - self.records: list[dict[str, Any]] = [] + self.records: list[dict[str, object]] = [] - def add(self, message: str, actor_id: str | None = None) -> Result[Any, ErrorsList]: + def add(self, message: str, actor_id: str | None = None) -> Result[object, ErrorsList]: self.records.append({"message": message, "actor_id": actor_id}) return Ok(None) @@ -111,8 +112,8 @@ def __init__(self, *, state: ConsistentState | None, artifact: Artifact, primiti class InstalledContext: def __init__(self, context: FakeRuntimeContext) -> None: self.context = context - self._runtime_token: Any = None - self._machine_token: Any = None + self._runtime_token: contextvars.Token[Context | None] | None = None + self._machine_token: contextvars.Token[MachineContext | None] | None = None def __enter__(self) -> FakeRuntimeContext: self._runtime_token = set_runtime_context(cast(Context, self.context)) @@ -120,6 +121,8 @@ def __enter__(self) -> FakeRuntimeContext: return self.context def __exit__(self, exc_type: object, exc: object, traceback: object) -> None: + assert self._machine_token is not None + assert self._runtime_token is not None reset_machine_context(self._machine_token) reset_runtime_context(self._runtime_token) diff --git a/donna/runtime/tests/test_sessions.py b/donna/runtime/tests/test_sessions.py index 88b8239c..da177d88 100644 --- a/donna/runtime/tests/test_sessions.py +++ b/donna/runtime/tests/test_sessions.py @@ -1,4 +1,4 @@ -from typing import Any +from pytest_mock import MockerFixture from donna.core.errors import ErrorsList from donna.core.result import Ok, Result @@ -168,7 +168,7 @@ def test_creates_fresh_state_and_reports_success(self) -> None: class TestClear: - def test_resets_workspace_session_dir_and_reports_success(self, mocker: Any) -> None: + def test_resets_workspace_session_dir_and_reports_success(self, mocker: MockerFixture) -> None: reset_dir = mocker.patch("donna.runtime.sessions.workspace_sessions.reset_dir") result = sessions.clear() diff --git a/donna/skills/tests/__init__.py b/donna/skills/tests/__init__.py index 8b137891..e69de29b 100644 --- a/donna/skills/tests/__init__.py +++ b/donna/skills/tests/__init__.py @@ -1 +0,0 @@ - diff --git a/donna/workspaces/markdown.py b/donna/workspaces/markdown.py index 1a2de02b..f66b709b 100644 --- a/donna/workspaces/markdown.py +++ b/donna/workspaces/markdown.py @@ -1,5 +1,5 @@ import enum -from typing import Any +from typing import cast from markdown_it import MarkdownIt from markdown_it.token import Token @@ -23,7 +23,7 @@ class CodeSource(BaseEntity): properties: dict[str, str | bool] content: str - def structured_data(self) -> Result[Any, ErrorsList]: + def structured_data(self) -> Result[object, ErrorsList]: if "script" in self.properties: return Ok({}) @@ -75,7 +75,7 @@ def as_original_markdown(self, with_title: bool) -> str: def as_analysis_markdown(self, with_title: bool) -> str: return self._as_markdown(self.analysis_tokens, with_title) - def config(self) -> Result[dict[str, Any], ErrorsList]: + def config(self) -> Result[dict[str, object], ErrorsList]: config_blocks = [config for config in self.configs if "config" in config.properties] if len(config_blocks) > 1: return Err( @@ -89,7 +89,8 @@ def config(self) -> Result[dict[str, Any], ErrorsList]: if not config_blocks: return Ok({}) - return config_blocks[0].structured_data() + data = config_blocks[0].structured_data().unwrap() + return Ok(cast(dict[str, object], data)) def script(self) -> Result[str | None, ErrorsList]: script_blocks = [config.content for config in self.configs if "script" in config.properties] diff --git a/donna/workspaces/markdown_parser.py b/donna/workspaces/markdown_parser.py index 4f8d2bd0..f64737e7 100644 --- a/donna/workspaces/markdown_parser.py +++ b/donna/workspaces/markdown_parser.py @@ -1,5 +1,5 @@ import uuid -from typing import Any, ClassVar, Protocol, cast +from typing import ClassVar, Protocol, cast from donna.core.errors import ErrorsList from donna.core.result import Err, Ok, Result, unwrap_to_error @@ -20,7 +20,7 @@ def markdown_construct_section( self, artifact_id: ArtifactId, source: markdown.SectionSource, - config: dict[str, Any], + config: dict[str, object], primary: bool = False, ) -> Result[ArtifactSection, ErrorsList]: pass @@ -62,7 +62,7 @@ def markdown_construct_section( # noqa: CCR001 self, artifact_id: ArtifactId, source: markdown.SectionSource, - config: dict[str, Any], + config: dict[str, object], primary: bool = False, ) -> Result[ArtifactSection, ErrorsList]: section_config = self.config_class.model_validate(config) @@ -240,7 +240,7 @@ def _resolve_primitive( return resolve_primitive(primitive_id) -def _parse_primitive_id(value: PythonPath | str) -> Result[PythonPath, ErrorsList]: +def _parse_primitive_id(value: object) -> Result[PythonPath, ErrorsList]: if isinstance(value, PythonPath): return Ok(value) diff --git a/donna/workspaces/templates.py b/donna/workspaces/templates.py index a774e9b8..79e57ff5 100644 --- a/donna/workspaces/templates.py +++ b/donna/workspaces/templates.py @@ -2,7 +2,7 @@ import importlib import importlib.util -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import jinja2 @@ -11,6 +11,7 @@ from donna.core.result import Err, Ok, Result from donna.domain.artifact_ids import ArtifactId from donna.machine.templates import Directive +from donna.machine.templates_context import DirectiveContext from donna.workspaces import errors as world_errors if TYPE_CHECKING: @@ -35,8 +36,9 @@ def __getitem__(self, name: str) -> "DirectivePathBuilder": return DirectivePathBuilder(self._parts + (name,)) @jinja2.pass_context - def __call__(self, context: jinja2.runtime.Context, *argv: object, **kwargs: object) -> object: # noqa: CCR001 - artifact_id = context.get("artifact_id") + def __call__(self, context: DirectiveContext, *argv: object, **kwargs: object) -> object: # noqa: CCR001 + raw_artifact_id = context.get("artifact_id") + artifact_id = cast(ArtifactId | None, raw_artifact_id if isinstance(raw_artifact_id, str) else None) directive_path = ".".join(self._parts) if len(self._parts) < 2: raise EnvironmentErrorsProxy( @@ -154,4 +156,4 @@ def render(artifact_id: ArtifactId, template: str, render_context: "ArtifactRend template_obj = env().from_string(template) return Ok(template_obj.render(**context)) except EnvironmentErrorsProxy as exc: - return Err(exc.arguments["errors"]) + return Err(cast(ErrorsList, exc.arguments["errors"])) diff --git a/donna/workspaces/tests/make.py b/donna/workspaces/tests/make.py index 6265ec7f..37f0f804 100644 --- a/donna/workspaces/tests/make.py +++ b/donna/workspaces/tests/make.py @@ -2,8 +2,9 @@ import pathlib from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId -from donna.domain.internal_ids import TaskId, WorkUnitId +from donna.domain.id_paths import NormalizedRawIdPath from donna.domain.ids import SectionId +from donna.domain.internal_ids import TaskId, WorkUnitId from donna.domain.paths import ProjectConfigPath, ProjectRootPath from donna.domain.python_path import PythonPath from donna.machine.artifacts import ArtifactSectionConfig @@ -13,7 +14,7 @@ ARTIFACT_ID = ArtifactId("@/workflows/test.donna.md") OPERATION_ID = ArtifactSectionId("@/workflows/test.donna.md:primary") -TEXT_KIND = PythonPath("donna.primitives.sections.text.Text") +TEXT_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.text.Text")) def workspace(root: pathlib.Path, config: Config | None = None) -> Workspace: diff --git a/donna/workspaces/tests/test_artifacts.py b/donna/workspaces/tests/test_artifacts.py index 6aa82a64..82e8d7c8 100644 --- a/donna/workspaces/tests/test_artifacts.py +++ b/donna/workspaces/tests/test_artifacts.py @@ -1,5 +1,7 @@ import pathlib +from pytest_mock import MockerFixture + from donna.core.result import Err, Ok from donna.domain.artifact_ids import ArtifactId from donna.domain.paths import RelativeProjectPath, ResolvedProjectPath @@ -61,8 +63,11 @@ def test_rejects_workflow_dir_itself_and_other_dirs(self) -> None: class TestArtifactIsVisibleInWorkspace: - def test_uses_configured_workflow_dirs(self, mocker: object) -> None: - mocker.patch("donna.workspaces.config.config", return_value=Config(workflow_dirs=["workflows"])) + def test_uses_configured_workflow_dirs(self, mocker: MockerFixture) -> None: + mocker.patch( + "donna.workspaces.config.config", + return_value=Config(workflow_dirs=[RelativeProjectPath(pathlib.Path("workflows"))]), + ) assert artifacts._artifact_is_visible_in_workspace(make.ARTIFACT_ID) @@ -105,7 +110,9 @@ def test_walks_directory_recursively_in_name_order(self, tmp_path: pathlib.Path) class TestWalkFilesystem: - def test_walk_filesystem__lists_artifacts_in_workflow_dirs(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_walk_filesystem__lists_artifacts_in_workflow_dirs( + self, mocker: MockerFixture, tmp_path: pathlib.Path + ) -> None: workflows = tmp_path / "workflows" nested = workflows / "nested" nested.mkdir(parents=True) @@ -120,7 +127,9 @@ def test_walk_filesystem__lists_artifacts_in_workflow_dirs(self, mocker: object, ArtifactId("@/workflows/nested/a.donna.md"), ] - def test_walk_filesystem__preserves_workflow_dir_order(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_walk_filesystem__preserves_workflow_dir_order( + self, mocker: MockerFixture, tmp_path: pathlib.Path + ) -> None: first = tmp_path / "first" second = tmp_path / "second" first.mkdir() @@ -141,15 +150,17 @@ def test_walk_filesystem__preserves_workflow_dir_order(self, mocker: object, tmp ArtifactId("@/first/b.donna.md"), ] - def test_walk_filesystem__ignores_missing_workflow_dirs(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_walk_filesystem__ignores_missing_workflow_dirs( + self, mocker: MockerFixture, tmp_path: pathlib.Path + ) -> None: mocker.patch("donna.workspaces.config.project_dir", return_value=tmp_path) assert list(artifacts.walk_filesystem([RelativeProjectPath(pathlib.Path("missing"))])) == [] class TestListArtifactIds: - def test_list_artifact_ids__deduplicates_discovered_artifacts(self, mocker: object) -> None: - config = Config(workflow_dirs=["workflows"]) + def test_list_artifact_ids__deduplicates_discovered_artifacts(self, mocker: MockerFixture) -> None: + config = Config(workflow_dirs=[RelativeProjectPath(pathlib.Path("workflows"))]) mocker.patch("donna.workspaces.config.config", return_value=config) mocker.patch.object( artifacts, @@ -162,7 +173,7 @@ def test_list_artifact_ids__deduplicates_discovered_artifacts(self, mocker: obje class TestResolveArtifactPath: def test_resolve_artifact_path__returns_existing_visible_file( - self, mocker: object, tmp_path: pathlib.Path + self, mocker: MockerFixture, tmp_path: pathlib.Path ) -> None: path = tmp_path / "workflows" / "test.donna.md" path.parent.mkdir() @@ -175,7 +186,7 @@ def test_resolve_artifact_path__returns_existing_visible_file( assert result.unwrap() == path def test_resolve_artifact_path__returns_none_for_missing_file( - self, mocker: object, tmp_path: pathlib.Path + self, mocker: MockerFixture, tmp_path: pathlib.Path ) -> None: mocker.patch("donna.workspaces.config.project_dir", return_value=tmp_path) @@ -193,7 +204,7 @@ def test_get_bytes__returns_file_bytes(self, tmp_path: pathlib.Path) -> None: assert raw_artifact.get_bytes() == b"content" - def test_render__renders_markdown_from_file_bytes(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_render__renders_markdown_from_file_bytes(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: path = tmp_path / "workflow.donna.md" path.write_bytes(b"# Workflow") expected_artifact = Artifact(id=make.ARTIFACT_ID, sections=[]) @@ -214,7 +225,7 @@ def test_render__renders_markdown_from_file_bytes(self, mocker: object, tmp_path artifacts.RENDER_CONTEXT_VIEW, ) - def test_render__returns_markdown_render_errors(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_render__returns_markdown_render_errors(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: path = tmp_path / "workflow.donna.md" path.write_bytes(b"# Workflow") error = workspace_errors.MarkdownArtifactWithoutSections(artifact_id=make.ARTIFACT_ID) @@ -228,12 +239,17 @@ def test_render__returns_markdown_render_errors(self, mocker: object, tmp_path: class TestFetchRawArtifact: - def test_fetch_raw_artifact__returns_filesystem_artifact(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_fetch_raw_artifact__returns_filesystem_artifact( + self, mocker: MockerFixture, tmp_path: pathlib.Path + ) -> None: path = tmp_path / "workflows" / "test.donna.md" path.parent.mkdir() path.write_text("content", encoding="utf-8") mocker.patch("donna.workspaces.config.project_dir", return_value=tmp_path) - mocker.patch("donna.workspaces.config.config", return_value=Config(workflow_dirs=["workflows"])) + mocker.patch( + "donna.workspaces.config.config", + return_value=Config(workflow_dirs=[RelativeProjectPath(pathlib.Path("workflows"))]), + ) result = artifacts.fetch_raw_artifact(make.ARTIFACT_ID) @@ -241,9 +257,12 @@ def test_fetch_raw_artifact__returns_filesystem_artifact(self, mocker: object, t assert result.unwrap().get_bytes() == b"content" def test_fetch_raw_artifact__rejects_artifacts_outside_workflow_dirs( - self, mocker: object, tmp_path: pathlib.Path + self, mocker: MockerFixture, tmp_path: pathlib.Path ) -> None: - mocker.patch("donna.workspaces.config.config", return_value=Config(workflow_dirs=["other"])) + mocker.patch( + "donna.workspaces.config.config", + return_value=Config(workflow_dirs=[RelativeProjectPath(pathlib.Path("other"))]), + ) result = artifacts.fetch_raw_artifact(make.ARTIFACT_ID) @@ -251,14 +270,17 @@ def test_fetch_raw_artifact__rejects_artifacts_outside_workflow_dirs( assert isinstance(result.unwrap_err()[0], workspace_errors.ArtifactNotFound) def test_fetch_raw_artifact__rejects_unsupported_artifact_extension( - self, mocker: object, tmp_path: pathlib.Path + self, mocker: MockerFixture, tmp_path: pathlib.Path ) -> None: artifact_id = ArtifactId("@/workflows/test.md") path = tmp_path / "workflows" / "test.md" path.parent.mkdir() path.write_text("content", encoding="utf-8") mocker.patch("donna.workspaces.config.project_dir", return_value=tmp_path) - mocker.patch("donna.workspaces.config.config", return_value=Config(workflow_dirs=["workflows"])) + mocker.patch( + "donna.workspaces.config.config", + return_value=Config(workflow_dirs=[RelativeProjectPath(pathlib.Path("workflows"))]), + ) result = artifacts.fetch_raw_artifact(artifact_id) @@ -269,7 +291,7 @@ def test_fetch_raw_artifact__rejects_unsupported_artifact_extension( class TestFetchArtifactBytes: - def test_fetch_artifact_bytes__returns_raw_bytes(self, mocker: object) -> None: + def test_fetch_artifact_bytes__returns_raw_bytes(self, mocker: MockerFixture) -> None: raw_artifact = mocker.Mock() raw_artifact.get_bytes.return_value = b"content" mocker.patch.object(artifacts, "fetch_raw_artifact", return_value=Ok(raw_artifact)) @@ -281,7 +303,7 @@ def test_fetch_artifact_bytes__returns_raw_bytes(self, mocker: object) -> None: class TestRenderMarkdownArtifact: - def test_render_markdown_artifact__uses_workspace_defaults(self, mocker: object) -> None: + def test_render_markdown_artifact__uses_workspace_defaults(self, mocker: MockerFixture) -> None: expected_artifact = Artifact(id=make.ARTIFACT_ID, sections=[]) mocker.patch("donna.workspaces.config.config", return_value=Config()) construct = mocker.patch( @@ -304,7 +326,9 @@ def test_render_markdown_artifact__uses_workspace_defaults(self, mocker: object) class TestArtifactFingerprint: - def test_artifact_fingerprint__returns_file_fingerprint(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_artifact_fingerprint__returns_file_fingerprint( + self, mocker: MockerFixture, tmp_path: pathlib.Path + ) -> None: path = tmp_path / "workflow.donna.md" path.write_text("data", encoding="utf-8") mocker.patch.object(artifacts, "resolve_artifact_path", return_value=Ok(path)) @@ -314,7 +338,7 @@ def test_artifact_fingerprint__returns_file_fingerprint(self, mocker: object, tm assert result.is_ok() assert result.unwrap() == FileFingerprint.from_path(path) - def test_artifact_fingerprint__returns_none_for_missing_artifact(self, mocker: object) -> None: + def test_artifact_fingerprint__returns_none_for_missing_artifact(self, mocker: MockerFixture) -> None: mocker.patch.object(artifacts, "resolve_artifact_path", return_value=Ok(None)) result = artifacts.artifact_fingerprint(make.ARTIFACT_ID) diff --git a/donna/workspaces/tests/test_config.py b/donna/workspaces/tests/test_config.py index 8730f25a..1e17dc9f 100644 --- a/donna/workspaces/tests/test_config.py +++ b/donna/workspaces/tests/test_config.py @@ -2,6 +2,7 @@ import pydantic import pytest +from pytest_mock import MockerFixture from donna.domain.id_paths import NormalizedRawIdPath from donna.domain.paths import ProjectConfigPath, ProjectRootPath, RelativeProjectPath @@ -37,7 +38,7 @@ def test_validate_cmd__accepts_none_and_supported_placeholders(self) -> None: @pytest.mark.parametrize("cmd", [[], ["tool", "{missing}"], ["tool", 1]]) def test_validate_cmd__rejects_invalid_command(self, cmd: list[object]) -> None: with pytest.raises(pydantic.ValidationError): - JournalConfig(cmd=cmd) + JournalConfig.model_validate({"cmd": cmd}) class TestDefaultsConfig: @@ -98,7 +99,7 @@ def test_defaults__match_configuration_spec(self) -> None: assert config.journal == JournalConfig() def test_validate_workflow_dirs__deduplicates_preserving_order(self) -> None: - config = Config(workflow_dirs=["workflows", "./workflows", ".session/donna"]) + config = Config.model_validate({"workflow_dirs": ["workflows", "./workflows", ".session/donna"]}) assert config.workflow_dirs == [pathlib.Path("workflows"), pathlib.Path(".session/donna")] @@ -131,7 +132,12 @@ def test_validation__rejects_unknown_fields(self, data: dict[str, object]) -> No Config.model_validate(data) def test_model_dump__serializes_workflow_dirs_as_project_relative_strings(self) -> None: - config = Config(workflow_dirs=["workflows", ".session/donna"]) + config = Config( + workflow_dirs=[ + RelativeProjectPath(pathlib.Path("workflows")), + RelativeProjectPath(pathlib.Path(".session/donna")), + ] + ) assert config.model_dump(mode="json")["workflow_dirs"] == ["./workflows", "./.session/donna"] @@ -166,7 +172,7 @@ def test_builds_validated_workspace_entity(self, tmp_path: pathlib.Path) -> None class TestInstallWorkspace: - def test_install_workspace__sets_unset_globals(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_install_workspace__sets_unset_globals(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: project_dir = GlobalConfig[ProjectRootPath]() config_path = GlobalConfig[ProjectConfigPath]() config = GlobalConfig[Config]() @@ -182,14 +188,14 @@ def test_install_workspace__sets_unset_globals(self, mocker: object, tmp_path: p assert config.get() == workspace.config def test_install_workspace__does_not_replace_existing_globals( - self, mocker: object, tmp_path: pathlib.Path + self, mocker: MockerFixture, tmp_path: pathlib.Path ) -> None: project_dir = GlobalConfig[ProjectRootPath]() config_path = GlobalConfig[ProjectConfigPath]() config = GlobalConfig[Config]() project_dir.set(ProjectRootPath(tmp_path / "existing")) config_path.set(ProjectConfigPath(tmp_path / "existing.toml")) - config.set(Config(session_dir="custom")) + config.set(Config(session_dir=RelativeProjectPath(pathlib.Path("custom")))) mocker.patch.object(workspace_config, "project_dir", project_dir) mocker.patch.object(workspace_config, "config_path", config_path) mocker.patch.object(workspace_config, "config", config) diff --git a/donna/workspaces/tests/test_errors.py b/donna/workspaces/tests/test_errors.py index a85407dc..2f802745 100644 --- a/donna/workspaces/tests/test_errors.py +++ b/donna/workspaces/tests/test_errors.py @@ -1,12 +1,15 @@ import pathlib +from donna.domain.paths import ProjectConfigPath from donna.workspaces import errors as workspace_errors from donna.workspaces.tests import make class TestWorkspaceConfigError: def test_content_intro__includes_config_path(self, tmp_path: pathlib.Path) -> None: - error = workspace_errors.ConfigParseFailed(config_path=tmp_path / "donna.toml", details="bad") + error = workspace_errors.ConfigParseFailed( + config_path=ProjectConfigPath(tmp_path / "donna.toml"), details="bad" + ) assert error.content_intro() == f"Error in Donna config file '{tmp_path / 'donna.toml'}'" @@ -41,12 +44,16 @@ def test_error_message__formats_without_failure(self) -> None: class TestConfigParseFailed(_EnvironmentErrorCase): def error(self) -> workspace_errors.WorkspaceError: - return workspace_errors.ConfigParseFailed(config_path=pathlib.Path("donna.toml"), details="bad") + return workspace_errors.ConfigParseFailed( + config_path=ProjectConfigPath(pathlib.Path("donna.toml")), details="bad" + ) class TestConfigValidationFailed(_EnvironmentErrorCase): def error(self) -> workspace_errors.WorkspaceError: - return workspace_errors.ConfigValidationFailed(config_path=pathlib.Path("donna.toml"), details="bad") + return workspace_errors.ConfigValidationFailed( + config_path=ProjectConfigPath(pathlib.Path("donna.toml")), details="bad" + ) class TestWorkspaceConfigNotDiscovered(_EnvironmentErrorCase): @@ -56,17 +63,17 @@ def error(self) -> workspace_errors.WorkspaceError: class TestWorkspaceAlreadyInitialized(_EnvironmentErrorCase): def error(self) -> workspace_errors.WorkspaceError: - return workspace_errors.WorkspaceAlreadyInitialized(config_path=pathlib.Path("donna.toml")) + return workspace_errors.WorkspaceAlreadyInitialized(config_path=ProjectConfigPath(pathlib.Path("donna.toml"))) class TestWorkspaceConfigNotFound(_EnvironmentErrorCase): def error(self) -> workspace_errors.WorkspaceError: - return workspace_errors.WorkspaceConfigNotFound(config_path=pathlib.Path("donna.toml")) + return workspace_errors.WorkspaceConfigNotFound(config_path=ProjectConfigPath(pathlib.Path("donna.toml"))) class TestWorkspaceConfigDirNotFound(_EnvironmentErrorCase): def error(self) -> workspace_errors.WorkspaceError: - return workspace_errors.WorkspaceConfigDirNotFound(config_path=pathlib.Path("donna.toml")) + return workspace_errors.WorkspaceConfigDirNotFound(config_path=ProjectConfigPath(pathlib.Path("donna.toml"))) class TestJournalCommandConfigInvalid(_EnvironmentErrorCase): diff --git a/donna/workspaces/tests/test_initialization.py b/donna/workspaces/tests/test_initialization.py index b0aa3f5b..7d22a168 100644 --- a/donna/workspaces/tests/test_initialization.py +++ b/donna/workspaces/tests/test_initialization.py @@ -1,10 +1,12 @@ import pathlib +from pytest_mock import MockerFixture + from donna.core.result import Ok from donna.domain.constants import DONNA_CONFIG_NAME -from donna.workspaces import errors as workspace_errors from donna.protocol.modes import Mode from donna.workspaces import config as workspace_config +from donna.workspaces import errors as workspace_errors from donna.workspaces.config import GlobalConfig from donna.workspaces.initialization import initialize_runtime, initialize_workspace, load_workspace @@ -26,7 +28,7 @@ def test_explicit_config_path__loads_workspace(self, tmp_path: pathlib.Path) -> assert workspace.config.session_dir == pathlib.Path(".session/custom") assert workspace.config.workflow_dirs == [pathlib.Path("workflows")] - def test_discovery__uses_nearest_project_config(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_discovery__uses_nearest_project_config(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: config_path = tmp_path / DONNA_CONFIG_NAME config_path.write_text("version = 1", encoding="utf-8") mocker.patch("donna.workspaces.utils.discover_project_dir", return_value=Ok(tmp_path)) @@ -71,7 +73,9 @@ def test_unknown_config_fields__return_validation_error(self, tmp_path: pathlib. class TestInitializeRuntime: - def test_loads_workspace_installs_protocol_and_workspace(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_loads_workspace_installs_protocol_and_workspace( + self, mocker: MockerFixture, tmp_path: pathlib.Path + ) -> None: protocol = GlobalConfig[Mode]() mocker.patch.object(workspace_config, "protocol", protocol) install_workspace = mocker.patch("donna.workspaces.config.install_workspace") @@ -85,7 +89,7 @@ def test_loads_workspace_installs_protocol_and_workspace(self, mocker: object, t assert protocol.get() == Mode.llm install_workspace.assert_called_once_with(workspace) - def test_loads_workspace_without_protocol_override(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_loads_workspace_without_protocol_override(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: protocol = GlobalConfig[Mode]() mocker.patch.object(workspace_config, "protocol", protocol) config_path = tmp_path / DONNA_CONFIG_NAME @@ -98,7 +102,7 @@ def test_loads_workspace_without_protocol_override(self, mocker: object, tmp_pat class TestInitializeWorkspace: - def test_creates_starter_config_and_loads_workspace(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_creates_starter_config_and_loads_workspace(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: install_workspace = mocker.patch("donna.workspaces.config.install_workspace") config_path = tmp_path / DONNA_CONFIG_NAME diff --git a/donna/workspaces/tests/test_journal.py b/donna/workspaces/tests/test_journal.py index 97ef2eae..8782fae6 100644 --- a/donna/workspaces/tests/test_journal.py +++ b/donna/workspaces/tests/test_journal.py @@ -1,5 +1,7 @@ import subprocess # noqa: S404 +from pytest_mock import MockerFixture + from donna.workspaces import errors as workspace_errors from donna.workspaces import journal from donna.workspaces.config import Config, JournalConfig, JournalRecordAttribute @@ -129,7 +131,7 @@ def test_unsupported_placeholder_returns_config_error(self) -> None: class TestWriteRecord: - def test_no_configured_command_is_noop(self, mocker: object) -> None: + def test_no_configured_command_is_noop(self, mocker: MockerFixture) -> None: mocker.patch( "donna.workspaces.config.config", return_value=Config(journal=JournalConfig(cmd=None)), @@ -141,7 +143,7 @@ def test_no_configured_command_is_noop(self, mocker: object) -> None: assert result.is_ok() run.assert_not_called() - def test_runs_configured_command(self, mocker: object) -> None: + def test_runs_configured_command(self, mocker: MockerFixture) -> None: mocker.patch( "donna.workspaces.config.config", return_value=Config(journal=JournalConfig(cmd=["tool", "{message}"])), @@ -156,7 +158,7 @@ def test_runs_configured_command(self, mocker: object) -> None: assert result.is_ok() run.assert_called_once_with(["tool", "message"], check=False, capture_output=True, text=True) - def test_reports_command_os_error(self, mocker: object) -> None: + def test_reports_command_os_error(self, mocker: MockerFixture) -> None: mocker.patch( "donna.workspaces.config.config", return_value=Config(journal=JournalConfig(cmd=["tool"])), @@ -171,7 +173,7 @@ def test_reports_command_os_error(self, mocker: object) -> None: assert error.command == ["tool"] assert error.returncode is None - def test_reports_nonzero_command_exit(self, mocker: object) -> None: + def test_reports_nonzero_command_exit(self, mocker: MockerFixture) -> None: mocker.patch( "donna.workspaces.config.config", return_value=Config(journal=JournalConfig(cmd=["tool"])), diff --git a/donna/workspaces/tests/test_markdown_parser.py b/donna/workspaces/tests/test_markdown_parser.py index f9afc2cf..89d5f48b 100644 --- a/donna/workspaces/tests/test_markdown_parser.py +++ b/donna/workspaces/tests/test_markdown_parser.py @@ -1,23 +1,25 @@ -from typing import Any, ClassVar +from typing import ClassVar + +from pytest_mock import MockerFixture from donna.core.errors import ErrorsList from donna.core.result import Err, Ok, Result from donna.domain.artifact_ids import ArtifactId +from donna.domain.id_paths import NormalizedRawIdPath from donna.domain.ids import SectionId from donna.domain.python_path import PythonPath from donna.machine.artifacts import Artifact, ArtifactSection, ArtifactSectionConfig from donna.machine.primitives import Primitive from donna.primitives.sections.text import Text from donna.workspaces import errors as workspace_errors -from donna.workspaces.markdown import CodeSource, SectionLevel, SectionSource from donna.workspaces import markdown_parser from donna.workspaces.artifacts import RENDER_CONTEXT_VIEW +from donna.workspaces.markdown import CodeSource, SectionLevel, SectionSource from donna.workspaces.markdown_parser import MarkdownSectionMixin, construct_sections_from_markdown from donna.workspaces.tests import make - -TEXT_KIND = PythonPath("donna.primitives.sections.text.Text") -LIB_TEXT_KIND = PythonPath("donna.lib.text") +TEXT_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.text.Text")) +LIB_TEXT_KIND = PythonPath(NormalizedRawIdPath("donna.lib.text")) class _MarkdownPrimitive(MarkdownSectionMixin, Primitive): @@ -29,7 +31,7 @@ def markdown_construct_section( self, artifact_id: ArtifactId, source: SectionSource, - config: dict[str, Any], + config: dict[str, object], primary: bool = False, ) -> Result[ArtifactSection, ErrorsList]: return Err([workspace_errors.MarkdownArtifactWithoutSections(artifact_id=artifact_id)]) @@ -99,7 +101,7 @@ def test_returns_error_for_source_without_sections(self) -> None: assert result.is_err() assert isinstance(result.unwrap_err()[0], workspace_errors.MarkdownArtifactWithoutSections) - def test_raises_internal_error_for_analysis_section_count_mismatch(self, mocker: object) -> None: + def test_raises_internal_error_for_analysis_section_count_mismatch(self, mocker: MockerFixture) -> None: mocker.patch.object( markdown_parser, "render", @@ -116,7 +118,7 @@ def test_raises_internal_error_for_analysis_section_count_mismatch(self, mocker: class TestConstructArtifactFromBytes: - def test_decodes_bytes_and_constructs_artifact_from_markdown(self, mocker: object) -> None: + def test_decodes_bytes_and_constructs_artifact_from_markdown(self, mocker: MockerFixture) -> None: expected = Artifact(id=make.ARTIFACT_ID, sections=[]) construct = mocker.patch.object( markdown_parser, @@ -185,7 +187,7 @@ def test_parses_raw_string_kind_at_workspace_boundary(self) -> None: assert constructed_section.kind == TEXT_KIND assert constructed_section.title == "Section" - def test_generates_missing_section_id(self, mocker: object) -> None: + def test_generates_missing_section_id(self, mocker: MockerFixture) -> None: mocker.patch("uuid.uuid4", return_value=type("FakeUuid", (), {"hex": "abc"})()) section = make.section_source(configs=[CodeSource(format="toml", properties={"config": True}, content="")]) @@ -222,7 +224,7 @@ def test_returns_override_when_present(self) -> None: assert result.is_ok() assert result.unwrap() == primitive - def test_uses_machine_resolver_without_override(self, mocker: object) -> None: + def test_uses_machine_resolver_without_override(self, mocker: MockerFixture) -> None: primitive = Text() resolve_primitive = mocker.patch.object(markdown_parser, "resolve_primitive", return_value=Ok(primitive)) diff --git a/donna/workspaces/tests/test_sessions.py b/donna/workspaces/tests/test_sessions.py index 26d4a265..26d2fdfe 100644 --- a/donna/workspaces/tests/test_sessions.py +++ b/donna/workspaces/tests/test_sessions.py @@ -1,18 +1,24 @@ import pathlib +from pytest_mock import MockerFixture + from donna.domain.constants import STATE_FILE_NAME +from donna.domain.paths import RelativeProjectPath from donna.workspaces import sessions from donna.workspaces.config import Config from donna.workspaces.files import FileFingerprint -def _patch_session_globals(mocker: object, tmp_path: pathlib.Path) -> None: +def _patch_session_globals(mocker: MockerFixture, tmp_path: pathlib.Path) -> None: mocker.patch("donna.workspaces.sessions.project_dir", return_value=tmp_path) - mocker.patch("donna.workspaces.sessions.config", return_value=Config(session_dir=".session/donna")) + mocker.patch( + "donna.workspaces.sessions.config", + return_value=Config(session_dir=RelativeProjectPath(pathlib.Path(".session/donna"))), + ) class TestDir: - def test_dir__creates_session_directory(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_dir__creates_session_directory(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: _patch_session_globals(mocker, tmp_path) session_dir = sessions.dir() @@ -22,7 +28,9 @@ def test_dir__creates_session_directory(self, mocker: object, tmp_path: pathlib. class TestPath: - def test_returns_configured_session_path_without_creating_it(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_returns_configured_session_path_without_creating_it( + self, mocker: MockerFixture, tmp_path: pathlib.Path + ) -> None: _patch_session_globals(mocker, tmp_path) assert sessions._path() == tmp_path / ".session" / "donna" @@ -30,14 +38,14 @@ def test_returns_configured_session_path_without_creating_it(self, mocker: objec class TestStatePath: - def test_returns_state_file_path_under_session_dir(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_returns_state_file_path_under_session_dir(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: _patch_session_globals(mocker, tmp_path) assert sessions._state_path() == tmp_path / ".session" / "donna" / STATE_FILE_NAME class TestEnsureDir: - def test_ensure_dir__creates_session_directory(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_ensure_dir__creates_session_directory(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: _patch_session_globals(mocker, tmp_path) sessions.ensure_dir() @@ -47,7 +55,7 @@ def test_ensure_dir__creates_session_directory(self, mocker: object, tmp_path: p class TestResetDir: def test_reset_dir__removes_existing_content_and_recreates_directory( - self, mocker: object, tmp_path: pathlib.Path + self, mocker: MockerFixture, tmp_path: pathlib.Path ) -> None: _patch_session_globals(mocker, tmp_path) state_path = tmp_path / ".session" / "donna" / STATE_FILE_NAME @@ -61,12 +69,14 @@ def test_reset_dir__removes_existing_content_and_recreates_directory( class TestReadState: - def test_read_state__returns_none_for_missing_state_file(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_read_state__returns_none_for_missing_state_file( + self, mocker: MockerFixture, tmp_path: pathlib.Path + ) -> None: _patch_session_globals(mocker, tmp_path) assert sessions.read_state() is None - def test_read_state__returns_state_bytes(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_read_state__returns_state_bytes(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: _patch_session_globals(mocker, tmp_path) sessions.write_state(b"state") @@ -74,7 +84,7 @@ def test_read_state__returns_state_bytes(self, mocker: object, tmp_path: pathlib class TestWriteState: - def test_write_state__writes_state_bytes(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_write_state__writes_state_bytes(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: _patch_session_globals(mocker, tmp_path) sessions.dir() @@ -85,14 +95,14 @@ def test_write_state__writes_state_bytes(self, mocker: object, tmp_path: pathlib class TestStateFingerprint: def test_state_fingerprint__returns_none_for_missing_state_file( - self, mocker: object, tmp_path: pathlib.Path + self, mocker: MockerFixture, tmp_path: pathlib.Path ) -> None: _patch_session_globals(mocker, tmp_path) assert sessions.state_fingerprint() is None def test_state_fingerprint__returns_fingerprint_for_state_file( - self, mocker: object, tmp_path: pathlib.Path + self, mocker: MockerFixture, tmp_path: pathlib.Path ) -> None: _patch_session_globals(mocker, tmp_path) sessions.write_state(b"state") diff --git a/donna/workspaces/tests/test_templates.py b/donna/workspaces/tests/test_templates.py index 1050c72e..c4ab7239 100644 --- a/donna/workspaces/tests/test_templates.py +++ b/donna/workspaces/tests/test_templates.py @@ -1,10 +1,10 @@ -from typing import Any - import pytest +from pytest_mock import MockerFixture from donna.core.errors import EnvironmentErrorsProxy, ErrorsList from donna.core.result import Err, Ok, Result from donna.machine.templates import Directive, RenderMode +from donna.machine.templates_context import DirectiveContext from donna.workspaces import errors as workspace_errors from donna.workspaces import templates from donna.workspaces.artifacts import ArtifactRenderContext @@ -15,21 +15,21 @@ class _Directive(Directive): analyze_id: str = "sample" - def render_view(self, context: dict[str, Any], *argv: Any) -> Result[Any, ErrorsList]: + def render_view(self, context: DirectiveContext, *argv: object) -> Result[object, ErrorsList]: return Ok(f"{context['render_mode']}:{','.join(str(arg) for arg in argv)}") class _FailingDirective(Directive): analyze_id: str = "failing" - def render_view(self, context: dict[str, Any], *argv: Any) -> Result[Any, ErrorsList]: + def render_view(self, context: DirectiveContext, *argv: object) -> Result[object, ErrorsList]: return Err([workspace_errors.MarkdownArtifactWithoutSections(artifact_id=make.ARTIFACT_ID)]) class _ExplodingDirective(Directive): analyze_id: str = "exploding" - def render_view(self, context: dict[str, Any], *argv: Any) -> Result[Any, ErrorsList]: + def render_view(self, context: DirectiveContext, *argv: object) -> Result[object, ErrorsList]: raise RuntimeError("boom") @@ -73,7 +73,9 @@ def test_call__reports_incomplete_path(self) -> None: with pytest.raises(EnvironmentErrorsProxy) as exception_info: builder({"artifact_id": make.ARTIFACT_ID}) - error = exception_info.value.arguments["errors"][0] + errors = exception_info.value.arguments["errors"] + assert isinstance(errors, list) + error = errors[0] assert isinstance(error, workspace_errors.DirectivePathIncomplete) assert error.path == "donna" @@ -83,18 +85,22 @@ def test_call__reports_unimportable_module(self) -> None: with pytest.raises(EnvironmentErrorsProxy) as exception_info: builder({"artifact_id": make.ARTIFACT_ID}) - error = exception_info.value.arguments["errors"][0] + errors = exception_info.value.arguments["errors"] + assert isinstance(errors, list) + error = errors[0] assert isinstance(error, workspace_errors.DirectiveModuleNotImportable) assert error.module_path == "donna.workspaces.tests.missing" - def test_call__reports_unexpected_import_error(self, mocker: object) -> None: + def test_call__reports_unexpected_import_error(self, mocker: MockerFixture) -> None: builder = DirectivePathBuilder(("donna", "workspaces", "tests", "test_templates", "sample_directive")) mocker.patch("importlib.import_module", side_effect=RuntimeError("boom")) with pytest.raises(EnvironmentErrorsProxy) as exception_info: builder({"artifact_id": make.ARTIFACT_ID}) - error = exception_info.value.arguments["errors"][0] + errors = exception_info.value.arguments["errors"] + assert isinstance(errors, list) + error = errors[0] assert isinstance(error, workspace_errors.DirectiveUnexpectedError) assert error.directive_path == "donna.workspaces.tests.test_templates.sample_directive" @@ -104,7 +110,9 @@ def test_call__reports_missing_directive(self) -> None: with pytest.raises(EnvironmentErrorsProxy) as exception_info: builder({"artifact_id": make.ARTIFACT_ID}) - error = exception_info.value.arguments["errors"][0] + errors = exception_info.value.arguments["errors"] + assert isinstance(errors, list) + error = errors[0] assert isinstance(error, workspace_errors.DirectiveNotAvailable) assert error.module_path == "donna.workspaces.tests.test_templates" assert error.directive_name == "missing" @@ -115,7 +123,9 @@ def test_call__reports_non_directive_object(self) -> None: with pytest.raises(EnvironmentErrorsProxy) as exception_info: builder({"artifact_id": make.ARTIFACT_ID}) - error = exception_info.value.arguments["errors"][0] + errors = exception_info.value.arguments["errors"] + assert isinstance(errors, list) + error = errors[0] assert isinstance(error, workspace_errors.DirectiveNotDirective) def test_call__passes_directive_result_errors(self) -> None: @@ -124,7 +134,9 @@ def test_call__passes_directive_result_errors(self) -> None: with pytest.raises(EnvironmentErrorsProxy) as exception_info: builder({"render_mode": RenderMode.view, "artifact_id": make.ARTIFACT_ID}) - error = exception_info.value.arguments["errors"][0] + errors = exception_info.value.arguments["errors"] + assert isinstance(errors, list) + error = errors[0] assert isinstance(error, workspace_errors.MarkdownArtifactWithoutSections) assert error.artifact_id == make.ARTIFACT_ID @@ -134,7 +146,9 @@ def test_call__reports_unexpected_directive_error(self) -> None: with pytest.raises(EnvironmentErrorsProxy) as exception_info: builder({"render_mode": RenderMode.view, "artifact_id": make.ARTIFACT_ID}) - error = exception_info.value.arguments["errors"][0] + errors = exception_info.value.arguments["errors"] + assert isinstance(errors, list) + error = errors[0] assert isinstance(error, workspace_errors.DirectiveUnexpectedError) assert error.directive_path == "donna.workspaces.tests.test_templates.exploding_directive" diff --git a/donna/workspaces/tests/test_utils.py b/donna/workspaces/tests/test_utils.py index e6492c2f..e2af278b 100644 --- a/donna/workspaces/tests/test_utils.py +++ b/donna/workspaces/tests/test_utils.py @@ -1,11 +1,13 @@ import pathlib +from pytest_mock import MockerFixture + from donna.workspaces import errors as workspace_errors from donna.workspaces import utils class TestFirstProjectDirWithConfig: - def test_returns_nearest_parent_with_config(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_returns_nearest_parent_with_config(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: project = tmp_path / "project" nested = project / "nested" nested.mkdir(parents=True) @@ -14,14 +16,14 @@ def test_returns_nearest_parent_with_config(self, mocker: object, tmp_path: path assert utils.first_project_dir_with_config("donna.toml") == project - def test_returns_none_when_config_is_not_found(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_returns_none_when_config_is_not_found(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: mocker.patch("pathlib.Path.cwd", return_value=tmp_path) assert utils.first_project_dir_with_config("donna.toml") is None class TestDiscoverProjectDir: - def test_returns_discovered_project_dir(self, mocker: object, tmp_path: pathlib.Path) -> None: + def test_returns_discovered_project_dir(self, mocker: MockerFixture, tmp_path: pathlib.Path) -> None: mocker.patch.object(utils, "first_project_dir_with_config", return_value=tmp_path) result = utils.discover_project_dir("donna.toml") @@ -29,7 +31,7 @@ def test_returns_discovered_project_dir(self, mocker: object, tmp_path: pathlib. assert result.is_ok() assert result.unwrap() == tmp_path - def test_reports_missing_config(self, mocker: object) -> None: + def test_reports_missing_config(self, mocker: MockerFixture) -> None: mocker.patch.object(utils, "first_project_dir_with_config", return_value=None) result = utils.discover_project_dir("donna.toml") diff --git a/pyproject.toml b/pyproject.toml index bc73b9f8..d55ba65a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,6 +114,7 @@ plugins = ["pydantic.mypy"] strict = true implicit_reexport = true allow_untyped_calls = true +disallow_any_explicit = true [tool.pydantic-mypy] init_forbid_extra = true From e6769a3fb645f34cea54a576b842c916262fca18 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 14:54:49 +0200 Subject: [PATCH 53/98] mypu --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d55ba65a..64a0c583 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,7 +113,7 @@ recursive = true plugins = ["pydantic.mypy"] strict = true implicit_reexport = true -allow_untyped_calls = true +disallow_untyped_calls = true disallow_any_explicit = true [tool.pydantic-mypy] From 4e5cfbc55d3267bfb0e7f6a10c536a63891b27a9 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 15:07:08 +0200 Subject: [PATCH 54/98] less code in tests --- donna/context/tests/helpers.py | 27 +++++ donna/context/tests/make.py | 13 --- donna/context/tests/test_context.py | 2 +- donna/context/tests/test_journal.py | 2 +- donna/machine/tests/helpers.py | 10 +- .../artifacts/tests/test_workflow.py | 38 ++++--- .../primitives/directives/tests/test_goto.py | 7 +- .../sections/tests/test_finish_workflow.py | 17 +-- .../primitives/sections/tests/test_output.py | 29 +++-- .../sections/tests/test_request_action.py | 18 +++- .../sections/tests/test_run_script.py | 31 ++++-- donna/primitives/tests/make.py | 101 +----------------- donna/runtime/tests/make.py | 22 +--- donna/workspaces/tests/make.py | 32 ++---- donna/workspaces/tests/test_journal.py | 41 ++++--- 15 files changed, 161 insertions(+), 229 deletions(-) create mode 100644 donna/context/tests/helpers.py diff --git a/donna/context/tests/helpers.py b/donna/context/tests/helpers.py new file mode 100644 index 00000000..d9311cab --- /dev/null +++ b/donna/context/tests/helpers.py @@ -0,0 +1,27 @@ +from donna.core.errors import ErrorsList +from donna.core.result import Ok, Result +from donna.protocol.cells import Cell +from donna.protocol.journal import JournalRecord + + +class FakeOutputEmitter: + def __init__(self) -> None: + self.cells: list[Cell] = [] + self.journal_records: list[JournalRecord] = [] + + def emit_cell(self, cell: Cell) -> None: + self.cells.append(cell) + + def emit_journal(self, record: JournalRecord) -> None: + self.journal_records.append(record) + + +class FakeJournal: + def __init__(self) -> None: + self.messages: list[tuple[str | None, str]] = [] + self.records: list[dict[str, object]] = [] + + def add(self, message: str, actor_id: str | None = None) -> Result[object, ErrorsList]: + self.messages.append((actor_id, message)) + self.records.append({"message": message, "actor_id": actor_id}) + return Ok(None) diff --git a/donna/context/tests/make.py b/donna/context/tests/make.py index 3e503228..ad96a3b5 100644 --- a/donna/context/tests/make.py +++ b/donna/context/tests/make.py @@ -4,7 +4,6 @@ from donna.core.result import Ok, Result from donna.machine.artifacts import Artifact from donna.machine.templates import RenderMode -from donna.protocol.journal import JournalRecord from donna.workspaces.artifacts import ArtifactRenderContext @@ -17,15 +16,3 @@ def __init__(self, path: pathlib.Path, artifact: Artifact) -> None: def render(self, artifact_id: object, render_context: ArtifactRenderContext) -> Result[Artifact, ErrorsList]: self.render_modes.append(render_context.primary_mode) return Ok(self.artifact) - - -class FakeOutputEmitter: - def __init__(self) -> None: - self.cells: list[object] = [] - self.journal_records: list[JournalRecord] = [] - - def emit_cell(self, cell: object) -> None: - self.cells.append(cell) - - def emit_journal(self, record: JournalRecord) -> None: - self.journal_records.append(record) diff --git a/donna/context/tests/test_context.py b/donna/context/tests/test_context.py index 6c5c0e82..19362821 100644 --- a/donna/context/tests/test_context.py +++ b/donna/context/tests/test_context.py @@ -6,7 +6,7 @@ from donna.context.output import NoopEmitter from donna.context.primitives import PrimitivesCache from donna.context.state import StateCache -from donna.context.tests.make import FakeOutputEmitter +from donna.context.tests.helpers import FakeOutputEmitter class TestContext: diff --git a/donna/context/tests/test_journal.py b/donna/context/tests/test_journal.py index 527febed..f7521232 100644 --- a/donna/context/tests/test_journal.py +++ b/donna/context/tests/test_journal.py @@ -6,7 +6,7 @@ from donna.context.context import Context from donna.context.journal import Journal -from donna.context.tests.make import FakeOutputEmitter +from donna.context.tests.helpers import FakeOutputEmitter from donna.core.result import Ok from donna.domain.artifact_ids import ArtifactSectionId from donna.domain.internal_ids import WorkUnitId diff --git a/donna/machine/tests/helpers.py b/donna/machine/tests/helpers.py index 2dd03c46..4dfc6b86 100644 --- a/donna/machine/tests/helpers.py +++ b/donna/machine/tests/helpers.py @@ -1,3 +1,4 @@ +from donna.context.tests.helpers import FakeJournal from donna.core.errors import ErrorsList from donna.core.result import Err, Ok, Result from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId @@ -50,15 +51,6 @@ def resolve(self, primitive_id: PythonPath) -> Result[Primitive, ErrorsList]: return Ok(self.primitive) -class FakeJournal: - def __init__(self) -> None: - self.records: list[dict[str, object]] = [] - - def add(self, message: str, actor_id: str | None = None) -> Result[object, ErrorsList]: - self.records.append({"message": message, "actor_id": actor_id}) - return Ok(None) - - class FakeMachineContext: def __init__( self, diff --git a/donna/primitives/artifacts/tests/test_workflow.py b/donna/primitives/artifacts/tests/test_workflow.py index 8629f65d..154e86df 100644 --- a/donna/primitives/artifacts/tests/test_workflow.py +++ b/donna/primitives/artifacts/tests/test_workflow.py @@ -3,6 +3,7 @@ from donna.machine.changes import ChangeAddWorkUnit from donna.machine.errors import ArtifactSectionNotFound from donna.machine.operations import FsmMode, OperationMeta +from donna.machine.tests import make as machine_make from donna.primitives.artifacts.workflow import ( FinalOperationHasTransitions, NoOutgoingTransitions, @@ -18,7 +19,7 @@ def workflow_section(meta: WorkflowMeta | None = None) -> ArtifactSection: - return make.artifact_section( + return machine_make.artifact_section( id=make.WORKFLOW_SECTION_ID, kind=make.WORKFLOW_KIND, title="Workflow", @@ -33,8 +34,9 @@ def operation_section( transitions: set[SectionId] | None = None, fsm_mode: FsmMode = FsmMode.normal, ) -> ArtifactSection: - return make.artifact_section( + return machine_make.artifact_section( id=id, + kind=make.TEXT_KIND, meta=OperationMeta( fsm_mode=fsm_mode, allowed_transtions=transitions or set(), @@ -44,7 +46,7 @@ def operation_section( class TestFindWorkflowSections: def test_follows_operation_transitions_once(self) -> None: - artifact = make.artifact( + artifact = machine_make.artifact( [ operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID}), operation_section(id=make.NEXT_SECTION_ID, transitions={make.START_SECTION_ID}), @@ -56,10 +58,10 @@ def test_follows_operation_transitions_once(self) -> None: assert sections == {make.START_SECTION_ID, make.NEXT_SECTION_ID} def test_stops_at_missing_or_non_operation_sections(self) -> None: - artifact = make.artifact( + artifact = machine_make.artifact( [ operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID, make.OTHER_SECTION_ID}), - make.artifact_section(id=make.NEXT_SECTION_ID), + machine_make.artifact_section(id=make.NEXT_SECTION_ID, kind=make.TEXT_KIND), ] ) @@ -82,23 +84,25 @@ def test_cells_meta__omits_missing_start_operation(self) -> None: class TestWorkflow: def test_execute_section__adds_work_unit_for_resolved_start_operation(self) -> None: - artifact = make.artifact( + artifact = machine_make.artifact( [ workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID)), operation_section(id=make.START_SECTION_ID, fsm_mode=FsmMode.final), ] ) - result = Workflow().execute_section(make.task(), make.work_unit(), artifact, make.WORKFLOW_SECTION_ID) + result = Workflow().execute_section( + machine_make.task(), machine_make.work_unit(), artifact, make.WORKFLOW_SECTION_ID + ) assert result.is_ok() change = result.unwrap()[0] assert isinstance(change, ChangeAddWorkUnit) - assert change.task_id == make.TASK_ID + assert change.task_id == machine_make.TASK_ID assert change.operation_id == make.START_OPERATION_ID def test_validate_section__accepts_connected_workflow_ending_in_final_operation(self) -> None: - artifact = make.artifact( + artifact = machine_make.artifact( [ workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID)), operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID}), @@ -111,7 +115,7 @@ def test_validate_section__accepts_connected_workflow_ending_in_final_operation( assert result.is_ok() def test_validate_section__uses_first_tail_section_as_default_start_operation(self) -> None: - artifact = make.artifact( + artifact = machine_make.artifact( [ workflow_section(WorkflowMeta()), operation_section(id=make.START_SECTION_ID, fsm_mode=FsmMode.final), @@ -123,7 +127,9 @@ def test_validate_section__uses_first_tail_section_as_default_start_operation(se assert result.is_ok() def test_validate_section__rejects_non_workflow_section_meta(self) -> None: - artifact = make.artifact([make.artifact_section(id=make.WORKFLOW_SECTION_ID, primary=True)]) + artifact = machine_make.artifact( + [machine_make.artifact_section(id=make.WORKFLOW_SECTION_ID, kind=make.TEXT_KIND, primary=True)] + ) result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) @@ -131,7 +137,7 @@ def test_validate_section__rejects_non_workflow_section_meta(self) -> None: assert isinstance(result.unwrap_err()[0], WorkflowSectionNotWorkflow) def test_validate_section__requires_start_operation_when_workflow_has_no_tail_sections(self) -> None: - artifact = make.artifact([workflow_section(WorkflowMeta())]) + artifact = machine_make.artifact([workflow_section(WorkflowMeta())]) result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) @@ -139,7 +145,7 @@ def test_validate_section__requires_start_operation_when_workflow_has_no_tail_se assert isinstance(result.unwrap_err()[0], StartOperationMissing) def test_validate_section__reports_wrong_explicit_start_operation(self) -> None: - artifact = make.artifact([workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID))]) + artifact = machine_make.artifact([workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID))]) result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) @@ -149,11 +155,11 @@ def test_validate_section__reports_wrong_explicit_start_operation(self) -> None: assert isinstance(errors[1], WrongStartOperation) def test_validate_section__reports_non_operation_workflow_section(self) -> None: - artifact = make.artifact( + artifact = machine_make.artifact( [ workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID)), operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID}), - make.artifact_section(id=make.NEXT_SECTION_ID), + machine_make.artifact_section(id=make.NEXT_SECTION_ID, kind=make.TEXT_KIND), ] ) @@ -163,7 +169,7 @@ def test_validate_section__reports_non_operation_workflow_section(self) -> None: assert isinstance(result.unwrap_err()[0], SectionIsNotAnOperation) def test_validate_section__reports_invalid_transition_rules(self) -> None: - artifact = make.artifact( + artifact = machine_make.artifact( [ workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID)), operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID, make.DONE_SECTION_ID}), diff --git a/donna/primitives/directives/tests/test_goto.py b/donna/primitives/directives/tests/test_goto.py index 13fca633..0294b694 100644 --- a/donna/primitives/directives/tests/test_goto.py +++ b/donna/primitives/directives/tests/test_goto.py @@ -3,6 +3,7 @@ from pytest_mock import MockerFixture from donna.domain.artifact_ids import ArtifactSectionId +from donna.machine.tests import make as machine_make from donna.primitives.directives import goto from donna.primitives.directives.goto import GoTo, GoToInvalidArguments from donna.primitives.tests import make @@ -11,7 +12,9 @@ class TestGoTo: def test_prepare_arguments__requires_one_argument(self) -> None: - result = GoTo(analyze_id="goto")._prepare_arguments(make.template_context(artifact_id=make.ARTIFACT_ID)) + result = GoTo(analyze_id="goto")._prepare_arguments( + make.template_context(artifact_id=machine_make.ARTIFACT_ID) + ) assert result.is_err() error = result.unwrap_err()[0] @@ -20,7 +23,7 @@ def test_prepare_arguments__requires_one_argument(self) -> None: def test_prepare_arguments__builds_artifact_section_id_in_current_artifact(self) -> None: result = GoTo(analyze_id="goto")._prepare_arguments( - make.template_context(artifact_id=make.ARTIFACT_ID), "next" + make.template_context(artifact_id=machine_make.ARTIFACT_ID), "next" ) assert result.is_ok() diff --git a/donna/primitives/sections/tests/test_finish_workflow.py b/donna/primitives/sections/tests/test_finish_workflow.py index d55f7f54..4a992b75 100644 --- a/donna/primitives/sections/tests/test_finish_workflow.py +++ b/donna/primitives/sections/tests/test_finish_workflow.py @@ -2,15 +2,17 @@ from donna.machine.changes import ChangeFinishTask from donna.machine.operations import FsmMode, OperationMeta +from donna.machine.tests import make as machine_make from donna.primitives.sections.finish_workflow import FinishWorkflow, FinishWorkflowConfig from donna.primitives.tests import make +from donna.workspaces.tests import make as workspace_make class TestFinishWorkflow: def test_markdown_construct_meta__creates_final_operation_without_transitions(self) -> None: result = FinishWorkflow().markdown_construct_meta( - artifact_id=make.ARTIFACT_ID, - source=make.section_source(), + artifact_id=machine_make.ARTIFACT_ID, + source=workspace_make.section_source(), section_config=FinishWorkflowConfig(id=make.DONE_SECTION_ID, kind=make.FINISH_WORKFLOW_KIND), description="done", ) @@ -24,17 +26,20 @@ def test_markdown_construct_meta__creates_final_operation_without_transitions(se def test_execute_section__emits_message_and_finishes_task(self, mocker: MockerFixture) -> None: runtime_context = make.FakeRuntimeContext() mocker.patch("donna.primitives.sections.finish_workflow.context", return_value=runtime_context) - artifact = make.artifact( + artifact = machine_make.artifact( [ - make.artifact_section( + machine_make.artifact_section( id=make.DONE_SECTION_ID, + kind=make.FINISH_WORKFLOW_KIND, description="Finished", meta=OperationMeta(fsm_mode=FsmMode.final, allowed_transtions=set()), ) ] ) - result = FinishWorkflow().execute_section(make.task(), make.work_unit(), artifact, make.DONE_SECTION_ID) + result = FinishWorkflow().execute_section( + machine_make.task(), machine_make.work_unit(), artifact, make.DONE_SECTION_ID + ) assert result.is_ok() cell = runtime_context.output.cells[0] @@ -42,4 +47,4 @@ def test_execute_section__emits_message_and_finishes_task(self, mocker: MockerFi assert cell.content == "Finished" change = result.unwrap()[0] assert isinstance(change, ChangeFinishTask) - assert change.task_id == make.TASK_ID + assert change.task_id == machine_make.TASK_ID diff --git a/donna/primitives/sections/tests/test_output.py b/donna/primitives/sections/tests/test_output.py index 1842399d..754f4448 100644 --- a/donna/primitives/sections/tests/test_output.py +++ b/donna/primitives/sections/tests/test_output.py @@ -2,15 +2,17 @@ from donna.machine.changes import ChangeAddWorkUnit from donna.machine.operations import FsmMode +from donna.machine.tests import make as machine_make from donna.primitives.sections.output import Output, OutputConfig, OutputMeta, OutputMissingNextOperation from donna.primitives.tests import make +from donna.workspaces.tests import make as workspace_make class TestOutput: def test_markdown_construct_meta__records_next_operation_transition(self) -> None: result = Output().markdown_construct_meta( - artifact_id=make.ARTIFACT_ID, - source=make.section_source(), + artifact_id=machine_make.ARTIFACT_ID, + source=workspace_make.section_source(), section_config=OutputConfig( id=make.START_SECTION_ID, kind=make.OUTPUT_KIND, @@ -29,8 +31,8 @@ def test_markdown_construct_meta__records_next_operation_transition(self) -> Non def test_markdown_construct_meta__allows_missing_next_operation_for_validation(self) -> None: result = Output().markdown_construct_meta( - artifact_id=make.ARTIFACT_ID, - source=make.section_source(), + artifact_id=machine_make.ARTIFACT_ID, + source=workspace_make.section_source(), section_config=OutputConfig(id=make.START_SECTION_ID, kind=make.OUTPUT_KIND), description="message", ) @@ -42,10 +44,11 @@ def test_markdown_construct_meta__allows_missing_next_operation_for_validation(s assert meta.allowed_transtions == set() def test_validate_section__requires_next_operation(self) -> None: - artifact = make.artifact( + artifact = machine_make.artifact( [ - make.artifact_section( + machine_make.artifact_section( id=make.START_SECTION_ID, + kind=make.TEXT_KIND, meta=OutputMeta(allowed_transtions=set(), next_operation_id=None), ) ] @@ -59,10 +62,11 @@ def test_validate_section__requires_next_operation(self) -> None: def test_execute_section__emits_message_and_adds_next_work_unit(self, mocker: MockerFixture) -> None: runtime_context = make.FakeRuntimeContext() mocker.patch("donna.primitives.sections.output.context", return_value=runtime_context) - artifact = make.artifact( + artifact = machine_make.artifact( [ - make.artifact_section( + machine_make.artifact_section( id=make.START_SECTION_ID, + kind=make.TEXT_KIND, description="Agent message", meta=OutputMeta( allowed_transtions={make.NEXT_SECTION_ID}, @@ -72,7 +76,12 @@ def test_execute_section__emits_message_and_adds_next_work_unit(self, mocker: Mo ] ) - result = Output().execute_section(make.task(), make.work_unit(), artifact, make.START_SECTION_ID) + result = Output().execute_section( + machine_make.task(), + machine_make.work_unit(operation_id=make.START_OPERATION_ID), + artifact, + make.START_SECTION_ID, + ) assert result.is_ok() cell = runtime_context.output.cells[0] @@ -80,4 +89,4 @@ def test_execute_section__emits_message_and_adds_next_work_unit(self, mocker: Mo assert cell.content == "Agent message" change = result.unwrap()[0] assert isinstance(change, ChangeAddWorkUnit) - assert change.operation_id == make.ARTIFACT_ID + ":next" + assert change.operation_id == machine_make.ARTIFACT_ID + ":next" diff --git a/donna/primitives/sections/tests/test_request_action.py b/donna/primitives/sections/tests/test_request_action.py index f94125d1..6a2f3917 100644 --- a/donna/primitives/sections/tests/test_request_action.py +++ b/donna/primitives/sections/tests/test_request_action.py @@ -4,8 +4,10 @@ from donna.domain.errors import InvalidIdentifier from donna.machine.changes import ChangeAddActionRequest from donna.machine.operations import FsmMode, OperationMeta +from donna.machine.tests import make as machine_make from donna.primitives.sections.request_action import RequestAction, RequestActionConfig, extract_transitions from donna.primitives.tests import make +from donna.workspaces.tests import make as workspace_make class TestExtractTransitions: @@ -40,13 +42,13 @@ def test_validate_fsm_mode__rejects_final_mode(self) -> None: class TestRequestAction: def test_markdown_construct_meta__uses_analysis_markdown_for_allowed_transitions(self) -> None: - source = make.section_source_from_markdown( + source = workspace_make.section_source_from_markdown( "# Workflow\n\n## Ask\n\nChoose one.\n\n$$donna goto next donna$$\n", section_index=1, ) result = RequestAction().markdown_construct_meta( - artifact_id=make.ARTIFACT_ID, + artifact_id=machine_make.ARTIFACT_ID, source=source, section_config=RequestActionConfig(id=make.START_SECTION_ID, kind=make.REQUEST_ACTION_KIND), description="Choose one.", @@ -58,10 +60,11 @@ def test_markdown_construct_meta__uses_analysis_markdown_for_allowed_transitions assert meta.allowed_transtions == {make.NEXT_SECTION_ID} def test_execute_section__adds_action_request_for_current_operation(self) -> None: - artifact = make.artifact( + artifact = machine_make.artifact( [ - make.artifact_section( + machine_make.artifact_section( id=make.START_SECTION_ID, + kind=make.REQUEST_ACTION_KIND, title="Ask agent", description="Do the work", meta=OperationMeta(fsm_mode=FsmMode.normal, allowed_transtions={make.NEXT_SECTION_ID}), @@ -69,7 +72,12 @@ def test_execute_section__adds_action_request_for_current_operation(self) -> Non ] ) - result = RequestAction().execute_section(make.task(), make.work_unit(), artifact, make.START_SECTION_ID) + result = RequestAction().execute_section( + machine_make.task(), + machine_make.work_unit(operation_id=make.START_OPERATION_ID), + artifact, + make.START_SECTION_ID, + ) assert result.is_ok() change = result.unwrap()[0] diff --git a/donna/primitives/sections/tests/test_run_script.py b/donna/primitives/sections/tests/test_run_script.py index 240e2063..bae9ef4f 100644 --- a/donna/primitives/sections/tests/test_run_script.py +++ b/donna/primitives/sections/tests/test_run_script.py @@ -3,6 +3,7 @@ from pytest_mock import MockerFixture from donna.machine.changes import ChangeAddWorkUnit, ChangeSetTaskContext +from donna.machine.tests import make as machine_make from donna.primitives.sections import run_script from donna.primitives.sections.run_script import ( RunScript, @@ -16,6 +17,7 @@ _coerce_output, ) from donna.primitives.tests import make +from donna.workspaces.tests import make as workspace_make class TestRunScriptMeta: @@ -50,9 +52,9 @@ def test_select_next_operation__falls_back_to_failure_transition(self) -> None: class TestRunScript: def test_markdown_construct_meta__builds_script_meta_and_allowed_transitions(self) -> None: - source = make.section_source( + source = workspace_make.section_source( configs=[ - make.code_source( + workspace_make.code_source( "bash", "echo ok", donna=True, @@ -62,7 +64,7 @@ def test_markdown_construct_meta__builds_script_meta_and_allowed_transitions(sel ) result = RunScript().markdown_construct_meta( - artifact_id=make.ARTIFACT_ID, + artifact_id=machine_make.ARTIFACT_ID, source=source, section_config=RunScriptConfig( id=make.START_SECTION_ID, @@ -88,8 +90,8 @@ def test_markdown_construct_meta__builds_script_meta_and_allowed_transitions(sel def test_markdown_construct_meta__requires_script_block(self) -> None: result = RunScript().markdown_construct_meta( - artifact_id=make.ARTIFACT_ID, - source=make.section_source(), + artifact_id=machine_make.ARTIFACT_ID, + source=workspace_make.section_source(), section_config=RunScriptConfig(id=make.START_SECTION_ID, kind=make.RUN_SCRIPT_KIND), description="run", ) @@ -98,10 +100,11 @@ def test_markdown_construct_meta__requires_script_block(self) -> None: assert isinstance(result.unwrap_err()[0], RunScriptMissingScriptBlock) def test_validate_section__collects_transition_config_errors(self) -> None: - artifact = make.artifact( + artifact = machine_make.artifact( [ - make.artifact_section( + machine_make.artifact_section( id=make.START_SECTION_ID, + kind=make.RUN_SCRIPT_KIND, meta=RunScriptMeta( allowed_transtions={make.NEXT_SECTION_ID}, goto_on_code={"not-an-int": make.NEXT_SECTION_ID, "0": make.DONE_SECTION_ID}, @@ -125,10 +128,11 @@ def test_execute_section__stores_outputs_and_adds_selected_next_work_unit(self, mocker.patch("donna.primitives.sections.run_script.context", return_value=runtime_context) mocker.patch.object(run_script.workspace_config, "project_dir", return_value=Path("/project")) run = mocker.patch.object(run_script, "_run_script", return_value=("stdout", "stderr", 2)) - artifact = make.artifact( + artifact = machine_make.artifact( [ - make.artifact_section( + machine_make.artifact_section( id=make.START_SECTION_ID, + kind=make.RUN_SCRIPT_KIND, title="Run checks", meta=RunScriptMeta( allowed_transtions={make.NEXT_SECTION_ID, make.DONE_SECTION_ID, make.OTHER_SECTION_ID}, @@ -143,7 +147,12 @@ def test_execute_section__stores_outputs_and_adds_selected_next_work_unit(self, ] ) - result = RunScript().execute_section(make.task(), make.work_unit(), artifact, make.START_SECTION_ID) + result = RunScript().execute_section( + machine_make.task(), + machine_make.work_unit(operation_id=make.START_OPERATION_ID), + artifact, + make.START_SECTION_ID, + ) assert result.is_ok() run.assert_called_once_with(script="echo ok", timeout=60, project_dir=Path("/project")) @@ -159,7 +168,7 @@ def test_execute_section__stores_outputs_and_adds_selected_next_work_unit(self, assert changes[1].key == "stderr_key" assert changes[1].value == "stderr" assert isinstance(changes[2], ChangeAddWorkUnit) - assert changes[2].operation_id == make.ARTIFACT_ID + ":other" + assert changes[2].operation_id == machine_make.ARTIFACT_ID + ":other" class TestCoerceOutput: diff --git a/donna/primitives/tests/make.py b/donna/primitives/tests/make.py index 3686dccf..1b060161 100644 --- a/donna/primitives/tests/make.py +++ b/donna/primitives/tests/make.py @@ -1,31 +1,18 @@ from typing import cast -from donna.core.entities import BaseEntity -from donna.core.errors import ErrorsList -from donna.core.result import Ok, Result -from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId +from donna.context.tests.helpers import FakeJournal, FakeOutputEmitter +from donna.domain.artifact_ids import ArtifactSectionId from donna.domain.id_paths import NormalizedRawIdPath from donna.domain.ids import SectionId -from donna.domain.internal_ids import TaskId, WorkUnitId from donna.domain.python_path import PythonPath -from donna.machine.artifacts import Artifact, ArtifactSection, ArtifactSectionMeta -from donna.machine.tasks import Task, WorkUnit from donna.machine.templates_context import DirectiveContext -from donna.protocol.cells import Cell -from donna.protocol.journal import JournalRecord -from donna.workspaces import markdown -from donna.workspaces.markdown import CodeSource, SectionLevel, SectionSource -ARTIFACT_ID = ArtifactId("@/workflows/test.donna.md") WORKFLOW_SECTION_ID = SectionId("workflow") START_SECTION_ID = SectionId("start") NEXT_SECTION_ID = SectionId("next") DONE_SECTION_ID = SectionId("done") OTHER_SECTION_ID = SectionId("other") -WORKFLOW_OPERATION_ID = ArtifactSectionId("@/workflows/test.donna.md:workflow") START_OPERATION_ID = ArtifactSectionId("@/workflows/test.donna.md:start") -TASK_ID = TaskId("T-1-b") -WORK_UNIT_ID = WorkUnitId("WU-2-c") WORKFLOW_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.artifacts.workflow.Workflow")) TEXT_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.text.Text")) @@ -35,94 +22,10 @@ FINISH_WORKFLOW_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.finish_workflow.FinishWorkflow")) -def artifact_section( - *, - id: SectionId = START_SECTION_ID, - kind: PythonPath = TEXT_KIND, - title: str = "Step", - description: str = "Step description", - primary: bool = False, - meta: ArtifactSectionMeta | None = None, -) -> ArtifactSection: - return ArtifactSection( - id=id, - artifact_id=ARTIFACT_ID, - kind=kind, - title=title, - description=description, - primary=primary, - meta=meta or ArtifactSectionMeta(), - ) - - -def artifact(sections: list[ArtifactSection]) -> Artifact: - return Artifact(id=ARTIFACT_ID, sections=sections) - - -def task(*, id: TaskId = TASK_ID, workflow_id: ArtifactSectionId = WORKFLOW_OPERATION_ID) -> Task: - return Task.build(id=id, workflow_id=workflow_id) - - -def work_unit( - *, - id: WorkUnitId = WORK_UNIT_ID, - task_id: TaskId = TASK_ID, - operation_id: ArtifactSectionId = START_OPERATION_ID, - context: dict[str, object] | None = None, -) -> WorkUnit: - return WorkUnit.build(id=id, task_id=task_id, operation_id=operation_id, context=context) - - -def code_source(format: str = "toml", content: str = "", **properties: str | bool) -> CodeSource: - return CodeSource(format=format, content=content, properties=properties) - - -def section_source( - *, - level: SectionLevel = SectionLevel.h2, - title: str | None = "Step", - configs: list[CodeSource] | None = None, -) -> SectionSource: - return SectionSource( - level=level, - title=title, - configs=configs or [], - original_tokens=[], - analysis_tokens=[], - ) - - -def section_source_from_markdown(text: str, section_index: int = 0) -> SectionSource: - source = markdown.parse(text, artifact_id=ARTIFACT_ID).unwrap()[section_index] - source.analysis_tokens.extend(source.original_tokens) - return source - - def template_context(**values: object) -> DirectiveContext: return cast(DirectiveContext, values) -class FakeOutputEmitter: - def __init__(self) -> None: - self.cells: list[Cell] = [] - self.journal_records: list[JournalRecord] = [] - - def emit_cell(self, cell: Cell) -> None: - self.cells.append(cell) - - def emit_journal(self, record: JournalRecord) -> None: - self.journal_records.append(record) - - -class FakeJournal: - def __init__(self) -> None: - self.messages: list[tuple[str | None, str]] = [] - - def add(self, message: str, actor_id: str | None = None) -> Result[BaseEntity, ErrorsList]: - self.messages.append((actor_id, message)) - return Ok(BaseEntity()) - - class FakeRuntimeContext: def __init__(self) -> None: self.output = FakeOutputEmitter() diff --git a/donna/runtime/tests/make.py b/donna/runtime/tests/make.py index 945bb26a..91f7600e 100644 --- a/donna/runtime/tests/make.py +++ b/donna/runtime/tests/make.py @@ -4,6 +4,7 @@ from donna.context.context import Context from donna.context.context import reset_context as reset_runtime_context from donna.context.context import set_context as set_runtime_context +from donna.context.tests.helpers import FakeJournal, FakeOutputEmitter from donna.core.errors import ErrorsList from donna.core.result import Err, Ok, Result from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId @@ -16,7 +17,6 @@ from donna.machine.primitives import Primitive from donna.machine.state import ConsistentState from donna.machine.tasks import Task, WorkUnit -from donna.protocol.cells import Cell from donna.workspaces.artifacts import ArtifactRenderContext @@ -78,26 +78,6 @@ def resolve(self, primitive_id: PythonPath) -> Result[Primitive, ErrorsList]: return Ok(self.primitive) -class FakeJournal: - def __init__(self) -> None: - self.records: list[dict[str, object]] = [] - - def add(self, message: str, actor_id: str | None = None) -> Result[object, ErrorsList]: - self.records.append({"message": message, "actor_id": actor_id}) - return Ok(None) - - -class FakeOutputEmitter: - def __init__(self) -> None: - self.cells: list[Cell] = [] - - def emit_cell(self, cell: Cell) -> None: - self.cells.append(cell) - - def emit_journal(self, record: object) -> None: - pass - - class FakeRuntimeContext: def __init__(self, *, state: ConsistentState | None, artifact: Artifact, primitive: Primitive) -> None: self.state = FakeStateStore(state=state) diff --git a/donna/workspaces/tests/make.py b/donna/workspaces/tests/make.py index 37f0f804..f33bc1e3 100644 --- a/donna/workspaces/tests/make.py +++ b/donna/workspaces/tests/make.py @@ -1,19 +1,16 @@ -import datetime import pathlib -from donna.domain.artifact_ids import ArtifactId, ArtifactSectionId +from donna.domain.artifact_ids import ArtifactId from donna.domain.id_paths import NormalizedRawIdPath from donna.domain.ids import SectionId -from donna.domain.internal_ids import TaskId, WorkUnitId from donna.domain.paths import ProjectConfigPath, ProjectRootPath from donna.domain.python_path import PythonPath from donna.machine.artifacts import ArtifactSectionConfig -from donna.protocol.journal import JournalRecord +from donna.workspaces import markdown from donna.workspaces.config import Config, Workspace from donna.workspaces.markdown import CodeSource, SectionLevel, SectionSource ARTIFACT_ID = ArtifactId("@/workflows/test.donna.md") -OPERATION_ID = ArtifactSectionId("@/workflows/test.donna.md:primary") TEXT_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.text.Text")) @@ -25,23 +22,6 @@ def workspace(root: pathlib.Path, config: Config | None = None) -> Workspace: ) -def journal_record( - *, - actor_id: str | None = "agent", - current_task_id: TaskId | None = TaskId("T-1-b"), - current_work_unit_id: WorkUnitId | None = WorkUnitId("WU-2-c"), - current_operation_id: ArtifactSectionId | None = OPERATION_ID, -) -> JournalRecord: - return JournalRecord( - timestamp=datetime.datetime(2026, 5, 18, 10, 30, tzinfo=datetime.UTC), - actor_id=actor_id, - message="message", - current_task_id=current_task_id, - current_work_unit_id=current_work_unit_id, - current_operation_id=current_operation_id, - ) - - def section_source( *, level: SectionLevel = SectionLevel.h2, @@ -57,10 +37,16 @@ def section_source( ) -def code_source(format: str, content: str, **properties: str | bool) -> CodeSource: +def code_source(format: str = "toml", content: str = "", **properties: str | bool) -> CodeSource: return CodeSource(format=format, content=content, properties=properties) +def section_source_from_markdown(text: str, section_index: int = 0) -> SectionSource: + source = markdown.parse(text, artifact_id=ARTIFACT_ID).unwrap()[section_index] + source.analysis_tokens.extend(source.original_tokens) + return source + + def section_config( *, id: SectionId = SectionId("section"), diff --git a/donna/workspaces/tests/test_journal.py b/donna/workspaces/tests/test_journal.py index 8782fae6..95264e7f 100644 --- a/donna/workspaces/tests/test_journal.py +++ b/donna/workspaces/tests/test_journal.py @@ -1,11 +1,28 @@ +import datetime import subprocess # noqa: S404 from pytest_mock import MockerFixture +from donna.domain.artifact_ids import ArtifactSectionId +from donna.domain.internal_ids import TaskId, WorkUnitId +from donna.protocol.journal import JournalRecord +from donna.protocol.tests import make as protocol_make from donna.workspaces import errors as workspace_errors from donna.workspaces import journal from donna.workspaces.config import Config, JournalConfig, JournalRecordAttribute -from donna.workspaces.tests import make + + +def _journal_record(**kwargs: object) -> JournalRecord: + values = { + "timestamp": datetime.datetime(2026, 5, 18, 10, 30, tzinfo=datetime.UTC), + "actor_id": "agent", + "message": "message", + "current_task_id": TaskId("T-1-b"), + "current_work_unit_id": WorkUnitId("WU-2-c"), + "current_operation_id": ArtifactSectionId("@/workflows/test.donna.md:primary"), + } + values.update(kwargs) + return protocol_make.journal_record(**values) class TestIsVariableArgument: @@ -33,7 +50,7 @@ def test_reports_unsupported_attribute(self) -> None: class TestFormatRecordAttribute: def test_formats_record_attributes(self) -> None: - record = make.journal_record() + record = _journal_record() assert ( journal._format_record_attribute(JournalRecordAttribute.timestamp, record) == "2026-05-18T10:30:00+00:00" @@ -48,7 +65,7 @@ def test_formats_record_attributes(self) -> None: ) def test_formats_missing_optional_attributes_as_empty_strings(self) -> None: - record = make.journal_record( + record = _journal_record( actor_id=None, current_task_id=None, current_work_unit_id=None, @@ -63,13 +80,13 @@ def test_formats_missing_optional_attributes_as_empty_strings(self) -> None: class TestResolveCommandArgument: def test_returns_literal_argument(self) -> None: - result = journal._resolve_command_argument("literal:{message}", make.journal_record()) + result = journal._resolve_command_argument("literal:{message}", _journal_record()) assert result.is_ok() assert result.unwrap() == "literal:{message}" def test_resolves_placeholder_argument(self) -> None: - result = journal._resolve_command_argument("{message}", make.journal_record()) + result = journal._resolve_command_argument("{message}", _journal_record()) assert result.is_ok() assert result.unwrap() == "message" @@ -77,7 +94,7 @@ def test_resolves_placeholder_argument(self) -> None: class TestBuildCommandArgs: def test_replaces_whole_argument_placeholders(self) -> None: - record = make.journal_record() + record = _journal_record() result = journal._build_command_args( [ @@ -106,7 +123,7 @@ def test_replaces_whole_argument_placeholders(self) -> None: ] def test_replaces_missing_optional_record_values_with_empty_strings(self) -> None: - record = make.journal_record( + record = _journal_record( actor_id=None, current_task_id=None, current_work_unit_id=None, @@ -122,7 +139,7 @@ def test_replaces_missing_optional_record_values_with_empty_strings(self) -> Non assert result.unwrap() == ["", "", "", ""] def test_unsupported_placeholder_returns_config_error(self) -> None: - result = journal._build_command_args(["{missing}"], make.journal_record()) + result = journal._build_command_args(["{missing}"], _journal_record()) assert result.is_err() error = result.unwrap_err()[0] @@ -138,7 +155,7 @@ def test_no_configured_command_is_noop(self, mocker: MockerFixture) -> None: ) run = mocker.patch("subprocess.run") - result = journal.write_record(make.journal_record()) + result = journal.write_record(_journal_record()) assert result.is_ok() run.assert_not_called() @@ -153,7 +170,7 @@ def test_runs_configured_command(self, mocker: MockerFixture) -> None: return_value=subprocess.CompletedProcess(args=["tool", "message"], returncode=0, stdout="", stderr=""), ) - result = journal.write_record(make.journal_record()) + result = journal.write_record(_journal_record()) assert result.is_ok() run.assert_called_once_with(["tool", "message"], check=False, capture_output=True, text=True) @@ -165,7 +182,7 @@ def test_reports_command_os_error(self, mocker: MockerFixture) -> None: ) mocker.patch("subprocess.run", side_effect=OSError("missing")) - result = journal.write_record(make.journal_record()) + result = journal.write_record(_journal_record()) assert result.is_err() error = result.unwrap_err()[0] @@ -183,7 +200,7 @@ def test_reports_nonzero_command_exit(self, mocker: MockerFixture) -> None: return_value=subprocess.CompletedProcess(args=["tool"], returncode=2, stdout="", stderr="bad"), ) - result = journal.write_record(make.journal_record()) + result = journal.write_record(_journal_record()) assert result.is_err() error = result.unwrap_err()[0] From 5f4d9fa43eb8771ec00a50912d22ca026851af0f Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 15:15:22 +0200 Subject: [PATCH 55/98] less code --- .../artifacts/tests/test_workflow.py | 97 +++++++++++-------- .../sections/tests/test_finish_workflow.py | 11 ++- .../primitives/sections/tests/test_output.py | 33 ++++--- .../sections/tests/test_request_action.py | 25 ++--- .../sections/tests/test_run_script.py | 73 +++++++------- donna/primitives/tests/make.py | 24 +++-- donna/workspaces/tests/make.py | 9 +- 7 files changed, 155 insertions(+), 117 deletions(-) diff --git a/donna/primitives/artifacts/tests/test_workflow.py b/donna/primitives/artifacts/tests/test_workflow.py index 154e86df..59c12b50 100644 --- a/donna/primitives/artifacts/tests/test_workflow.py +++ b/donna/primitives/artifacts/tests/test_workflow.py @@ -20,11 +20,11 @@ def workflow_section(meta: WorkflowMeta | None = None) -> ArtifactSection: return machine_make.artifact_section( - id=make.WORKFLOW_SECTION_ID, - kind=make.WORKFLOW_KIND, + id=make.section_id("workflow"), + kind=make.primitive_kind("donna.primitives.artifacts.workflow.Workflow"), title="Workflow", primary=True, - meta=meta or WorkflowMeta(start_operation_id=make.START_SECTION_ID), + meta=meta or WorkflowMeta(start_operation_id=make.section_id("start")), ) @@ -36,7 +36,7 @@ def operation_section( ) -> ArtifactSection: return machine_make.artifact_section( id=id, - kind=make.TEXT_KIND, + kind=make.primitive_kind("donna.primitives.sections.text.Text"), meta=OperationMeta( fsm_mode=fsm_mode, allowed_transtions=transitions or set(), @@ -48,31 +48,37 @@ class TestFindWorkflowSections: def test_follows_operation_transitions_once(self) -> None: artifact = machine_make.artifact( [ - operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID}), - operation_section(id=make.NEXT_SECTION_ID, transitions={make.START_SECTION_ID}), + operation_section(id=make.section_id("start"), transitions={make.section_id("next")}), + operation_section(id=make.section_id("next"), transitions={make.section_id("start")}), ] ) - sections = find_workflow_sections(make.START_SECTION_ID, artifact) + sections = find_workflow_sections(make.section_id("start"), artifact) - assert sections == {make.START_SECTION_ID, make.NEXT_SECTION_ID} + assert sections == {make.section_id("start"), make.section_id("next")} def test_stops_at_missing_or_non_operation_sections(self) -> None: artifact = machine_make.artifact( [ - operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID, make.OTHER_SECTION_ID}), - machine_make.artifact_section(id=make.NEXT_SECTION_ID, kind=make.TEXT_KIND), + operation_section( + id=make.section_id("start"), + transitions={make.section_id("next"), make.section_id("other")}, + ), + machine_make.artifact_section( + id=make.section_id("next"), + kind=make.primitive_kind("donna.primitives.sections.text.Text"), + ), ] ) - sections = find_workflow_sections(make.START_SECTION_ID, artifact) + sections = find_workflow_sections(make.section_id("start"), artifact) - assert sections == {make.START_SECTION_ID, make.NEXT_SECTION_ID, make.OTHER_SECTION_ID} + assert sections == {make.section_id("start"), make.section_id("next"), make.section_id("other")} class TestWorkflowMeta: def test_cells_meta__serializes_explicit_start_operation(self) -> None: - meta = WorkflowMeta(start_operation_id=make.START_SECTION_ID) + meta = WorkflowMeta(start_operation_id=make.section_id("start")) assert meta.cells_meta() == {"start_operation_id": "start"} @@ -86,31 +92,34 @@ class TestWorkflow: def test_execute_section__adds_work_unit_for_resolved_start_operation(self) -> None: artifact = machine_make.artifact( [ - workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID)), - operation_section(id=make.START_SECTION_ID, fsm_mode=FsmMode.final), + workflow_section(WorkflowMeta(start_operation_id=make.section_id("start"))), + operation_section(id=make.section_id("start"), fsm_mode=FsmMode.final), ] ) result = Workflow().execute_section( - machine_make.task(), machine_make.work_unit(), artifact, make.WORKFLOW_SECTION_ID + machine_make.task(), + machine_make.work_unit(), + artifact, + make.section_id("workflow"), ) assert result.is_ok() change = result.unwrap()[0] assert isinstance(change, ChangeAddWorkUnit) assert change.task_id == machine_make.TASK_ID - assert change.operation_id == make.START_OPERATION_ID + assert change.operation_id == make.operation_id("start") def test_validate_section__accepts_connected_workflow_ending_in_final_operation(self) -> None: artifact = machine_make.artifact( [ - workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID)), - operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID}), - operation_section(id=make.NEXT_SECTION_ID, fsm_mode=FsmMode.final), + workflow_section(WorkflowMeta(start_operation_id=make.section_id("start"))), + operation_section(id=make.section_id("start"), transitions={make.section_id("next")}), + operation_section(id=make.section_id("next"), fsm_mode=FsmMode.final), ] ) - result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) + result = Workflow().validate_section(artifact, make.section_id("workflow")) assert result.is_ok() @@ -118,20 +127,26 @@ def test_validate_section__uses_first_tail_section_as_default_start_operation(se artifact = machine_make.artifact( [ workflow_section(WorkflowMeta()), - operation_section(id=make.START_SECTION_ID, fsm_mode=FsmMode.final), + operation_section(id=make.section_id("start"), fsm_mode=FsmMode.final), ] ) - result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) + result = Workflow().validate_section(artifact, make.section_id("workflow")) assert result.is_ok() def test_validate_section__rejects_non_workflow_section_meta(self) -> None: artifact = machine_make.artifact( - [machine_make.artifact_section(id=make.WORKFLOW_SECTION_ID, kind=make.TEXT_KIND, primary=True)] + [ + machine_make.artifact_section( + id=make.section_id("workflow"), + kind=make.primitive_kind("donna.primitives.sections.text.Text"), + primary=True, + ) + ] ) - result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) + result = Workflow().validate_section(artifact, make.section_id("workflow")) assert result.is_err() assert isinstance(result.unwrap_err()[0], WorkflowSectionNotWorkflow) @@ -139,15 +154,15 @@ def test_validate_section__rejects_non_workflow_section_meta(self) -> None: def test_validate_section__requires_start_operation_when_workflow_has_no_tail_sections(self) -> None: artifact = machine_make.artifact([workflow_section(WorkflowMeta())]) - result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) + result = Workflow().validate_section(artifact, make.section_id("workflow")) assert result.is_err() assert isinstance(result.unwrap_err()[0], StartOperationMissing) def test_validate_section__reports_wrong_explicit_start_operation(self) -> None: - artifact = machine_make.artifact([workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID))]) + artifact = machine_make.artifact([workflow_section(WorkflowMeta(start_operation_id=make.section_id("start")))]) - result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) + result = Workflow().validate_section(artifact, make.section_id("workflow")) assert result.is_err() errors = result.unwrap_err() @@ -157,13 +172,16 @@ def test_validate_section__reports_wrong_explicit_start_operation(self) -> None: def test_validate_section__reports_non_operation_workflow_section(self) -> None: artifact = machine_make.artifact( [ - workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID)), - operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID}), - machine_make.artifact_section(id=make.NEXT_SECTION_ID, kind=make.TEXT_KIND), + workflow_section(WorkflowMeta(start_operation_id=make.section_id("start"))), + operation_section(id=make.section_id("start"), transitions={make.section_id("next")}), + machine_make.artifact_section( + id=make.section_id("next"), + kind=make.primitive_kind("donna.primitives.sections.text.Text"), + ), ] ) - result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) + result = Workflow().validate_section(artifact, make.section_id("workflow")) assert result.is_err() assert isinstance(result.unwrap_err()[0], SectionIsNotAnOperation) @@ -171,18 +189,21 @@ def test_validate_section__reports_non_operation_workflow_section(self) -> None: def test_validate_section__reports_invalid_transition_rules(self) -> None: artifact = machine_make.artifact( [ - workflow_section(WorkflowMeta(start_operation_id=make.START_SECTION_ID)), - operation_section(id=make.START_SECTION_ID, transitions={make.NEXT_SECTION_ID, make.DONE_SECTION_ID}), + workflow_section(WorkflowMeta(start_operation_id=make.section_id("start"))), + operation_section( + id=make.section_id("start"), + transitions={make.section_id("next"), make.section_id("done")}, + ), operation_section( - id=make.NEXT_SECTION_ID, - transitions={make.DONE_SECTION_ID}, + id=make.section_id("next"), + transitions={make.section_id("done")}, fsm_mode=FsmMode.final, ), - operation_section(id=make.DONE_SECTION_ID), + operation_section(id=make.section_id("done")), ] ) - result = Workflow().validate_section(artifact, make.WORKFLOW_SECTION_ID) + result = Workflow().validate_section(artifact, make.section_id("workflow")) assert result.is_err() errors = result.unwrap_err() diff --git a/donna/primitives/sections/tests/test_finish_workflow.py b/donna/primitives/sections/tests/test_finish_workflow.py index 4a992b75..f98fe9bb 100644 --- a/donna/primitives/sections/tests/test_finish_workflow.py +++ b/donna/primitives/sections/tests/test_finish_workflow.py @@ -13,7 +13,10 @@ def test_markdown_construct_meta__creates_final_operation_without_transitions(se result = FinishWorkflow().markdown_construct_meta( artifact_id=machine_make.ARTIFACT_ID, source=workspace_make.section_source(), - section_config=FinishWorkflowConfig(id=make.DONE_SECTION_ID, kind=make.FINISH_WORKFLOW_KIND), + section_config=FinishWorkflowConfig( + id=make.section_id("done"), + kind=make.primitive_kind("donna.primitives.sections.finish_workflow.FinishWorkflow"), + ), description="done", ) @@ -29,8 +32,8 @@ def test_execute_section__emits_message_and_finishes_task(self, mocker: MockerFi artifact = machine_make.artifact( [ machine_make.artifact_section( - id=make.DONE_SECTION_ID, - kind=make.FINISH_WORKFLOW_KIND, + id=make.section_id("done"), + kind=make.primitive_kind("donna.primitives.sections.finish_workflow.FinishWorkflow"), description="Finished", meta=OperationMeta(fsm_mode=FsmMode.final, allowed_transtions=set()), ) @@ -38,7 +41,7 @@ def test_execute_section__emits_message_and_finishes_task(self, mocker: MockerFi ) result = FinishWorkflow().execute_section( - machine_make.task(), machine_make.work_unit(), artifact, make.DONE_SECTION_ID + machine_make.task(), machine_make.work_unit(), artifact, make.section_id("done") ) assert result.is_ok() diff --git a/donna/primitives/sections/tests/test_output.py b/donna/primitives/sections/tests/test_output.py index 754f4448..c7d1e7b2 100644 --- a/donna/primitives/sections/tests/test_output.py +++ b/donna/primitives/sections/tests/test_output.py @@ -14,10 +14,10 @@ def test_markdown_construct_meta__records_next_operation_transition(self) -> Non artifact_id=machine_make.ARTIFACT_ID, source=workspace_make.section_source(), section_config=OutputConfig( - id=make.START_SECTION_ID, - kind=make.OUTPUT_KIND, + id=make.section_id("start"), + kind=make.primitive_kind("donna.primitives.sections.output.Output"), fsm_mode=FsmMode.start, - next_operation_id=make.NEXT_SECTION_ID, + next_operation_id=make.section_id("next"), ), description="message", ) @@ -26,14 +26,17 @@ def test_markdown_construct_meta__records_next_operation_transition(self) -> Non meta = result.unwrap() assert isinstance(meta, OutputMeta) assert meta.fsm_mode == FsmMode.start - assert meta.next_operation_id == make.NEXT_SECTION_ID - assert meta.allowed_transtions == {make.NEXT_SECTION_ID} + assert meta.next_operation_id == make.section_id("next") + assert meta.allowed_transtions == {make.section_id("next")} def test_markdown_construct_meta__allows_missing_next_operation_for_validation(self) -> None: result = Output().markdown_construct_meta( artifact_id=machine_make.ARTIFACT_ID, source=workspace_make.section_source(), - section_config=OutputConfig(id=make.START_SECTION_ID, kind=make.OUTPUT_KIND), + section_config=OutputConfig( + id=make.section_id("start"), + kind=make.primitive_kind("donna.primitives.sections.output.Output"), + ), description="message", ) @@ -47,14 +50,14 @@ def test_validate_section__requires_next_operation(self) -> None: artifact = machine_make.artifact( [ machine_make.artifact_section( - id=make.START_SECTION_ID, - kind=make.TEXT_KIND, + id=make.section_id("start"), + kind=make.primitive_kind("donna.primitives.sections.text.Text"), meta=OutputMeta(allowed_transtions=set(), next_operation_id=None), ) ] ) - result = Output().validate_section(artifact, make.START_SECTION_ID) + result = Output().validate_section(artifact, make.section_id("start")) assert result.is_err() assert isinstance(result.unwrap_err()[0], OutputMissingNextOperation) @@ -65,12 +68,12 @@ def test_execute_section__emits_message_and_adds_next_work_unit(self, mocker: Mo artifact = machine_make.artifact( [ machine_make.artifact_section( - id=make.START_SECTION_ID, - kind=make.TEXT_KIND, + id=make.section_id("start"), + kind=make.primitive_kind("donna.primitives.sections.text.Text"), description="Agent message", meta=OutputMeta( - allowed_transtions={make.NEXT_SECTION_ID}, - next_operation_id=make.NEXT_SECTION_ID, + allowed_transtions={make.section_id("next")}, + next_operation_id=make.section_id("next"), ), ) ] @@ -78,9 +81,9 @@ def test_execute_section__emits_message_and_adds_next_work_unit(self, mocker: Mo result = Output().execute_section( machine_make.task(), - machine_make.work_unit(operation_id=make.START_OPERATION_ID), + machine_make.work_unit(operation_id=make.operation_id("start")), artifact, - make.START_SECTION_ID, + make.section_id("start"), ) assert result.is_ok() diff --git a/donna/primitives/sections/tests/test_request_action.py b/donna/primitives/sections/tests/test_request_action.py index 6a2f3917..856e2367 100644 --- a/donna/primitives/sections/tests/test_request_action.py +++ b/donna/primitives/sections/tests/test_request_action.py @@ -23,7 +23,7 @@ def test_extracts_unique_goto_directive_targets(self) -> None: transitions = extract_transitions(text) - assert transitions == {make.NEXT_SECTION_ID, make.DONE_SECTION_ID} + assert transitions == {make.section_id("next"), make.section_id("done")} def test_rejects_goto_target_that_is_not_a_section_id(self) -> None: with pytest.raises(InvalidIdentifier): @@ -34,8 +34,8 @@ class TestRequestActionConfig: def test_validate_fsm_mode__rejects_final_mode(self) -> None: with pytest.raises(ValidationError): RequestActionConfig( - id=make.START_SECTION_ID, - kind=make.REQUEST_ACTION_KIND, + id=make.section_id("start"), + kind=make.primitive_kind("donna.primitives.sections.request_action.RequestAction"), fsm_mode=FsmMode.final, ) @@ -50,33 +50,36 @@ def test_markdown_construct_meta__uses_analysis_markdown_for_allowed_transitions result = RequestAction().markdown_construct_meta( artifact_id=machine_make.ARTIFACT_ID, source=source, - section_config=RequestActionConfig(id=make.START_SECTION_ID, kind=make.REQUEST_ACTION_KIND), + section_config=RequestActionConfig( + id=make.section_id("start"), + kind=make.primitive_kind("donna.primitives.sections.request_action.RequestAction"), + ), description="Choose one.", ) assert result.is_ok() meta = result.unwrap() assert isinstance(meta, OperationMeta) - assert meta.allowed_transtions == {make.NEXT_SECTION_ID} + assert meta.allowed_transtions == {make.section_id("next")} def test_execute_section__adds_action_request_for_current_operation(self) -> None: artifact = machine_make.artifact( [ machine_make.artifact_section( - id=make.START_SECTION_ID, - kind=make.REQUEST_ACTION_KIND, + id=make.section_id("start"), + kind=make.primitive_kind("donna.primitives.sections.request_action.RequestAction"), title="Ask agent", description="Do the work", - meta=OperationMeta(fsm_mode=FsmMode.normal, allowed_transtions={make.NEXT_SECTION_ID}), + meta=OperationMeta(fsm_mode=FsmMode.normal, allowed_transtions={make.section_id("next")}), ) ] ) result = RequestAction().execute_section( machine_make.task(), - machine_make.work_unit(operation_id=make.START_OPERATION_ID), + machine_make.work_unit(operation_id=make.operation_id("start")), artifact, - make.START_SECTION_ID, + make.section_id("start"), ) assert result.is_ok() @@ -85,4 +88,4 @@ def test_execute_section__adds_action_request_for_current_operation(self) -> Non assert change.action_request.id is None assert change.action_request.title == "Ask agent" assert change.action_request.request == "Do the work" - assert change.action_request.operation_id == make.START_OPERATION_ID + assert change.action_request.operation_id == make.operation_id("start") diff --git a/donna/primitives/sections/tests/test_run_script.py b/donna/primitives/sections/tests/test_run_script.py index bae9ef4f..69334693 100644 --- a/donna/primitives/sections/tests/test_run_script.py +++ b/donna/primitives/sections/tests/test_run_script.py @@ -23,31 +23,31 @@ class TestRunScriptMeta: def test_select_next_operation__uses_success_transition_for_zero_exit_code(self) -> None: meta = RunScriptMeta( - allowed_transtions={make.NEXT_SECTION_ID, make.DONE_SECTION_ID}, - goto_on_success=make.NEXT_SECTION_ID, - goto_on_failure=make.DONE_SECTION_ID, + allowed_transtions={make.section_id("next"), make.section_id("done")}, + goto_on_success=make.section_id("next"), + goto_on_failure=make.section_id("done"), ) - assert meta.select_next_operation(0) == make.NEXT_SECTION_ID + assert meta.select_next_operation(0) == make.section_id("next") def test_select_next_operation__uses_exit_code_specific_transition(self) -> None: meta = RunScriptMeta( - allowed_transtions={make.NEXT_SECTION_ID, make.DONE_SECTION_ID}, - goto_on_success=make.NEXT_SECTION_ID, - goto_on_failure=make.DONE_SECTION_ID, - goto_on_code={"2": make.OTHER_SECTION_ID}, + allowed_transtions={make.section_id("next"), make.section_id("done")}, + goto_on_success=make.section_id("next"), + goto_on_failure=make.section_id("done"), + goto_on_code={"2": make.section_id("other")}, ) - assert meta.select_next_operation(2) == make.OTHER_SECTION_ID + assert meta.select_next_operation(2) == make.section_id("other") def test_select_next_operation__falls_back_to_failure_transition(self) -> None: meta = RunScriptMeta( - allowed_transtions={make.NEXT_SECTION_ID, make.DONE_SECTION_ID}, - goto_on_success=make.NEXT_SECTION_ID, - goto_on_failure=make.DONE_SECTION_ID, + allowed_transtions={make.section_id("next"), make.section_id("done")}, + goto_on_success=make.section_id("next"), + goto_on_failure=make.section_id("done"), ) - assert meta.select_next_operation(1) == make.DONE_SECTION_ID + assert meta.select_next_operation(1) == make.section_id("done") class TestRunScript: @@ -67,11 +67,11 @@ def test_markdown_construct_meta__builds_script_meta_and_allowed_transitions(sel artifact_id=machine_make.ARTIFACT_ID, source=source, section_config=RunScriptConfig( - id=make.START_SECTION_ID, - kind=make.RUN_SCRIPT_KIND, - goto_on_success=make.NEXT_SECTION_ID, - goto_on_failure=make.DONE_SECTION_ID, - goto_on_code={"2": make.OTHER_SECTION_ID}, + id=make.section_id("start"), + kind=make.primitive_kind("donna.primitives.sections.run_script.RunScript"), + goto_on_success=make.section_id("next"), + goto_on_failure=make.section_id("done"), + goto_on_code={"2": make.section_id("other")}, save_stdout_to="stdout", save_stderr_to="stderr", timeout=5, @@ -86,13 +86,16 @@ def test_markdown_construct_meta__builds_script_meta_and_allowed_transitions(sel assert meta.save_stdout_to == "stdout" assert meta.save_stderr_to == "stderr" assert meta.timeout == 5 - assert meta.allowed_transtions == {make.NEXT_SECTION_ID, make.DONE_SECTION_ID, make.OTHER_SECTION_ID} + assert meta.allowed_transtions == {make.section_id("next"), make.section_id("done"), make.section_id("other")} def test_markdown_construct_meta__requires_script_block(self) -> None: result = RunScript().markdown_construct_meta( artifact_id=machine_make.ARTIFACT_ID, source=workspace_make.section_source(), - section_config=RunScriptConfig(id=make.START_SECTION_ID, kind=make.RUN_SCRIPT_KIND), + section_config=RunScriptConfig( + id=make.section_id("start"), + kind=make.primitive_kind("donna.primitives.sections.run_script.RunScript"), + ), description="run", ) @@ -103,17 +106,17 @@ def test_validate_section__collects_transition_config_errors(self) -> None: artifact = machine_make.artifact( [ machine_make.artifact_section( - id=make.START_SECTION_ID, - kind=make.RUN_SCRIPT_KIND, + id=make.section_id("start"), + kind=make.primitive_kind("donna.primitives.sections.run_script.RunScript"), meta=RunScriptMeta( - allowed_transtions={make.NEXT_SECTION_ID}, - goto_on_code={"not-an-int": make.NEXT_SECTION_ID, "0": make.DONE_SECTION_ID}, + allowed_transtions={make.section_id("next")}, + goto_on_code={"not-an-int": make.section_id("next"), "0": make.section_id("done")}, ), ) ] ) - result = RunScript().validate_section(artifact, make.START_SECTION_ID) + result = RunScript().validate_section(artifact, make.section_id("start")) assert result.is_err() assert {type(error) for error in result.unwrap_err()} == { @@ -131,17 +134,21 @@ def test_execute_section__stores_outputs_and_adds_selected_next_work_unit(self, artifact = machine_make.artifact( [ machine_make.artifact_section( - id=make.START_SECTION_ID, - kind=make.RUN_SCRIPT_KIND, + id=make.section_id("start"), + kind=make.primitive_kind("donna.primitives.sections.run_script.RunScript"), title="Run checks", meta=RunScriptMeta( - allowed_transtions={make.NEXT_SECTION_ID, make.DONE_SECTION_ID, make.OTHER_SECTION_ID}, + allowed_transtions={ + make.section_id("next"), + make.section_id("done"), + make.section_id("other"), + }, script="echo ok", save_stdout_to="stdout_key", save_stderr_to="stderr_key", - goto_on_success=make.NEXT_SECTION_ID, - goto_on_failure=make.DONE_SECTION_ID, - goto_on_code={"2": make.OTHER_SECTION_ID}, + goto_on_success=make.section_id("next"), + goto_on_failure=make.section_id("done"), + goto_on_code={"2": make.section_id("other")}, ), ) ] @@ -149,9 +156,9 @@ def test_execute_section__stores_outputs_and_adds_selected_next_work_unit(self, result = RunScript().execute_section( machine_make.task(), - machine_make.work_unit(operation_id=make.START_OPERATION_ID), + machine_make.work_unit(operation_id=make.operation_id("start")), artifact, - make.START_SECTION_ID, + make.section_id("start"), ) assert result.is_ok() diff --git a/donna/primitives/tests/make.py b/donna/primitives/tests/make.py index 1b060161..5b136ef4 100644 --- a/donna/primitives/tests/make.py +++ b/donna/primitives/tests/make.py @@ -7,19 +7,17 @@ from donna.domain.python_path import PythonPath from donna.machine.templates_context import DirectiveContext -WORKFLOW_SECTION_ID = SectionId("workflow") -START_SECTION_ID = SectionId("start") -NEXT_SECTION_ID = SectionId("next") -DONE_SECTION_ID = SectionId("done") -OTHER_SECTION_ID = SectionId("other") -START_OPERATION_ID = ArtifactSectionId("@/workflows/test.donna.md:start") - -WORKFLOW_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.artifacts.workflow.Workflow")) -TEXT_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.text.Text")) -REQUEST_ACTION_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.request_action.RequestAction")) -OUTPUT_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.output.Output")) -RUN_SCRIPT_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.run_script.RunScript")) -FINISH_WORKFLOW_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.finish_workflow.FinishWorkflow")) + +def primitive_kind(path: str) -> PythonPath: + return PythonPath(NormalizedRawIdPath(path)) + + +def section_id(value: str) -> SectionId: + return SectionId(value) + + +def operation_id(section: str) -> ArtifactSectionId: + return ArtifactSectionId(f"@/workflows/test.donna.md:{section}") def template_context(**values: object) -> DirectiveContext: diff --git a/donna/workspaces/tests/make.py b/donna/workspaces/tests/make.py index f33bc1e3..f1f96b54 100644 --- a/donna/workspaces/tests/make.py +++ b/donna/workspaces/tests/make.py @@ -11,7 +11,10 @@ from donna.workspaces.markdown import CodeSource, SectionLevel, SectionSource ARTIFACT_ID = ArtifactId("@/workflows/test.donna.md") -TEXT_KIND = PythonPath(NormalizedRawIdPath("donna.primitives.sections.text.Text")) + + +def text_kind() -> PythonPath: + return PythonPath(NormalizedRawIdPath("donna.primitives.sections.text.Text")) def workspace(root: pathlib.Path, config: Config | None = None) -> Workspace: @@ -50,6 +53,6 @@ def section_source_from_markdown(text: str, section_index: int = 0) -> SectionSo def section_config( *, id: SectionId = SectionId("section"), - kind: PythonPath = TEXT_KIND, + kind: PythonPath | None = None, ) -> ArtifactSectionConfig: - return ArtifactSectionConfig(id=id, kind=kind) + return ArtifactSectionConfig(id=id, kind=kind or text_kind()) From a69719cdebbf72221e8854f0c5c9d6fc024efe55 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 15:26:12 +0200 Subject: [PATCH 56/98] polish workflow fixed --- bin/dev.sh | 2 +- workflows/polish.donna.md | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bin/dev.sh b/bin/dev.sh index 680a0cb8..8fb0e9c4 100755 --- a/bin/dev.sh +++ b/bin/dev.sh @@ -1,3 +1,3 @@ #!/usr/bin/bash -docker compose run --rm donna $@ +docker compose run --rm donna "$@" diff --git a/workflows/polish.donna.md b/workflows/polish.donna.md index c58b4efa..5c79ad2b 100644 --- a/workflows/polish.donna.md +++ b/workflows/polish.donna.md @@ -16,7 +16,7 @@ goto_on_failure = "fix_tach" ```bash donna script #!/usr/bin/env bash -tach check 2>&1 +./bin/dev.sh poetry run tach check 2>&1 ``` ## Fix Tach Issues @@ -48,7 +48,7 @@ goto_on_failure = "fix_autoflake" ```bash donna script #!/usr/bin/env bash -autoflake ./donna +./bin/dev.sh poetry run autoflake ./donna ``` ## Fix Autoflake Issues @@ -79,7 +79,7 @@ goto_on_failure = "fix_isort" ```bash donna script #!/usr/bin/env bash -isort ./donna +./bin/dev.sh poetry run isort ./donna ``` ## Fix isort Issues @@ -110,7 +110,7 @@ goto_on_failure = "fix_black" ```bash donna script #!/usr/bin/env bash -black ./donna +./bin/dev.sh poetry run black ./donna ``` ## Fix Black Issues @@ -141,7 +141,7 @@ goto_on_failure = "fix_codespell" ```bash donna script #!/usr/bin/env bash -codespell ./donna 2>&1 +./bin/dev.sh poetry run codespell ./donna 2>&1 ``` ## Fix Codespell Issues @@ -172,7 +172,7 @@ goto_on_failure = "fix_flake8" ```bash donna script #!/usr/bin/env bash -flake8 ./donna 2>&1 +./bin/dev.sh poetry run flake8 ./donna 2>&1 ``` ## Fix Flake8 Issues @@ -210,7 +210,7 @@ goto_on_failure = "fix_mypy" ```bash donna script #!/usr/bin/env bash -mypy ./donna +./bin/dev.sh poetry run mypy ./donna ``` ## Fix Mypy Issues @@ -260,7 +260,7 @@ goto_on_failure = "fix_tests" ```bash donna script #!/usr/bin/env bash -pytest donna -o cache_dir=/tmp/donna-pytest-cache 2>&1 +./bin/dev.sh poetry run pytest donna -o cache_dir=/tmp/donna-pytest-cache 2>&1 ``` ## Fix Test Issues From 61fb1c3e0a9d8235b0925b622fa0a90798abbec8 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 16:49:30 +0200 Subject: [PATCH 57/98] changelog.md --- depmesh.toml | 22 +++++++++ specs/documentation/changelog.md | 84 ++++++++++++++++++++++++++++++++ specs/intro.md | 2 + 3 files changed, 108 insertions(+) create mode 100644 specs/documentation/changelog.md diff --git a/depmesh.toml b/depmesh.toml index 9132d793..0dc557a3 100644 --- a/depmesh.toml +++ b/depmesh.toml @@ -224,6 +224,28 @@ output = { type = "union", items = [ { type = "files", pattern = "@/donna/skills/fixtures/*.md" }, ] } +################################ +# Relation: changelog documentation +################################ + +# Changelog artifacts are governed by the changelog documentation spec. +[[rules]] +relation = "governed_by" +input = { type = "any", items = [ + { type = "one_of", artifacts = ["@/CHANGELOG.md"] }, + { type = "glob", pattern = "@/changes/*.md" }, +] } +output = { type = "list", artifacts = ["@/specs/documentation/changelog.md"] } + +# The changelog documentation spec governs changelog artifacts. +[[rules]] +relation = "governs" +input = { type = "one_of", artifacts = ["@/specs/documentation/changelog.md"] } +output = { type = "union", items = [ + { type = "list", artifacts = ["@/CHANGELOG.md"] }, + { type = "files", pattern = "@/changes/*.md" }, +] } + # Dependency mesh and Tach configuration are governed by the module layout architecture spec. [[rules]] relation = "governed_by" diff --git a/specs/documentation/changelog.md b/specs/documentation/changelog.md new file mode 100644 index 00000000..0fca6363 --- /dev/null +++ b/specs/documentation/changelog.md @@ -0,0 +1,84 @@ +# Changelog Documentation + +## Goal of the document + +This document describes the expected tooling, source files, structure, and entry format for the project changelog. + +## Scope + +The scope of this specification is limited to changelog documentation artifacts. + +The following topics are out of scope: + +- release version selection. +- package publishing. +- Git tagging. +- release automation implementation details. +- project documentation files other than changelog artifacts. + +## Dictionary + +- `changelog artifact` - a file that is either a Changy source file in `changes/` or the generated root `CHANGELOG.md`. +- `version record` - the Markdown content that describes changes for one released or unreleased version. + +## Tooling + +The project MUST use [Changy](https://github.com/Tiendil/changy/) to manage the changelog. + +Changelog source files MUST live in `changes/`. + +The root `CHANGELOG.md` MUST be generated from Changy source files. + +Unreleased changes MUST be recorded in `changes/unreleased.md`. + +## Version Record Structure + +A single changelog version record MAY contain these parts, in this order: + +1. A short introductory description of the whole version when the version contains large coordinated changes that benefit from context before individual entries. +2. A `Migration` section with instructions for users to migrate to the new version when there are breaking changes or required manual upgrade steps. +3. A `Changes` section with a bullet list of all notable changes in the version. +4. A `Deprecations` section with a bullet list of features or behaviors that are now deprecated. + +The `Migration`, `Changes`, and `Deprecations` sections MUST use `h3` Markdown headings. + +The `Changes` section SHOULD be present when the version contains notable user-visible, developer-visible, or project-maintenance changes. + +The `Migration` section MUST be present when users need to perform manual steps before, during, or after upgrading. + +The `Deprecations` section MUST be present when the version deprecates features, behaviors, APIs, commands, configuration fields, or documented workflows. + +Additional `h3` sections MAY be used when a version needs a distinct category that is not covered by `Migration`, `Changes`, or `Deprecations`. + +## Entry Format + +Each bullet entry SHOULD be linked to a task, issue, or pull request when such a reference exists. + +When an entry references a GitHub issue, the reference SHOULD use the `gh-` form. + +When an entry references a pull request or another task tracker, the reference SHOULD use the shortest stable project convention for that tracker. + +Each bullet entry SHOULD include a short description of the change, deprecation, migration instruction, or other notable item. + +Additional details MAY be added as nested bullet points under the main entry when the detail helps users understand the impact or required action. + +## Example + +```markdown + + +### Migration + +- gh-xxx - +- gh-yyy - + - + - + +### Changes + +- gh-zzz - + +### Deprecations + +- gh-www - +``` diff --git a/specs/intro.md b/specs/intro.md index 12cb56b6..ba3a8065 100644 --- a/specs/intro.md +++ b/specs/intro.md @@ -15,6 +15,7 @@ Detailed requirements for individual specifications are out of scope except for - `./specs/` — directory with all specifications. - `./specs/architecture/` — specifications related to the architecture of the system. - `./specs/behavior/` — specifications related to the behavior of the system. +- `./specs/documentation/` — specifications related to project documentation artifacts. - `./specs/meta/` — specifications related to requirements for specification documents. ## Specification documents @@ -30,4 +31,5 @@ Detailed requirements for individual specifications are out of scope except for - `./specs/behavior/config.md` — specification of the `donna.toml` configuration file behavior. - `./specs/behavior/file_paths.md` — specification of Donna local project path, artifact id, and artifact section id syntax and resolution behavior. - `./specs/behavior/skill_fixtures.md` — specification of built-in skill documentation fixture behavior. +- `./specs/documentation/changelog.md` — specification of changelog tooling, source files, version record structure, and entry format. - `./specs/meta/general.md` — general requirements for specification documents. From d95fee5b6430eb3b469bdb1a76c92e9c825afed0 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 17:19:03 +0200 Subject: [PATCH 58/98] readme spec --- depmesh.toml | 16 ++++ specs/documentation/readme.md | 141 ++++++++++++++++++++++++++++++++++ specs/intro.md | 1 + 3 files changed, 158 insertions(+) create mode 100644 specs/documentation/readme.md diff --git a/depmesh.toml b/depmesh.toml index 0dc557a3..e2338f99 100644 --- a/depmesh.toml +++ b/depmesh.toml @@ -246,6 +246,22 @@ output = { type = "union", items = [ { type = "files", pattern = "@/changes/*.md" }, ] } +################################ +# Relation: README documentation +################################ + +# README.md is governed by its documentation spec. +[[rules]] +relation = "governed_by" +input = { type = "one_of", artifacts = ["@/README.md"] } +output = { type = "list", artifacts = ["@/specs/documentation/readme.md"] } + +# The README documentation spec governs README.md. +[[rules]] +relation = "governs" +input = { type = "one_of", artifacts = ["@/specs/documentation/readme.md"] } +output = { type = "list", artifacts = ["@/README.md"] } + # Dependency mesh and Tach configuration are governed by the module layout architecture spec. [[rules]] relation = "governed_by" diff --git a/specs/documentation/readme.md b/specs/documentation/readme.md new file mode 100644 index 00000000..09f7f46b --- /dev/null +++ b/specs/documentation/readme.md @@ -0,0 +1,141 @@ +# README Documentation + +## Goal of the document + +This document describes the expected content, structure, and tone of the project `README.md`. + +## Scope + +The scope of this specification is limited to the repository root `README.md`. + +The following topics are out of scope: + +- package publishing. +- generated command help. +- detailed CLI behavior. +- detailed configuration syntax. +- detailed workflow artifact syntax. +- development environment rules for agents. +- documentation files other than the root `README.md`. + +## Audience + +`README.md` MUST be written primarily for humans who are discovering the project. + +`README.md` MAY mention coding agents because Donna is designed for agent workflows, but it MUST NOT duplicate the full built-in agent skill documentation. + +`README.md` SHOULD help readers quickly understand: + +- what problem Donna solves. +- what Donna does. +- how to try the main commands. +- where to find detailed usage, configuration, and workflow documentation. +- how to work on the project. + +## Source Material + +`README.md` MUST reflect the current behavior and architecture described in `./specs/`. + +`README.md` MAY use project metadata from `pyproject.toml` for stable package identity, repository links, and installation context. + +Existing `README.md` prose MUST NOT be treated as a source of truth when it conflicts with `./specs/` or package metadata. + +## Structure + +`README.md` MUST start with a single h1 heading containing the project name. + +The first paragraphs after the h1 heading SHOULD explain the core value proposition in plain language. + +`README.md` MUST use the following h2 sections in this order: + +1. `Example` +2. `Rationale` +3. `Features` +4. `Quick Usage` +5. `Installation` +6. `Configuration` +7. `Workflow Files` +8. `Specifications` +9. `Development` + +The `Example` section MUST be the first h2 section after the introductory paragraphs. + +Additional h2 sections MUST NOT be added without updating this specification first. Lower-level subsections MAY be added under the required h2 sections when they help human readers. + +`README.md` SHOULD avoid deep implementation details. + +## Content Requirements + +`README.md` MUST mention that Donna is a CLI tool. + +`README.md` MUST explain that Donna helps agents run predefined workflows in a deterministic way. + +`README.md` MUST explain that Donna interprets workflows as state-machine-like graphs of operations. + +`README.md` MUST explain that Donna maintains project-local session state, discovers workflow artifacts, emits action requests for agents, and accepts agent reports about the next operation to run. + +`README.md` MUST explain that a Donna project is configured by `donna.toml`. + +`README.md` MUST explain that workflow artifacts are Markdown files ending with `.donna.md`. + +`README.md` MUST link to the configuration behavior specification when describing configuration. + +`README.md` MUST link to the CLI behavior specification when describing command behavior. + +`README.md` MUST link to the skill fixture specification or mention built-in skill documentation when describing detailed user documentation. + +`README.md` MUST mention that this repository uses Donna and that its `donna.toml` and `workflows/` directory can be used as real project examples. + +`README.md` MUST explain how to create a starter configuration with `donna init`. + +`README.md` MUST include installation examples for installing Donna from PyPI with `uv` and `pip`. + +`README.md` MUST explain that the generated starter configuration should be reviewed and edited for the project. + +`README.md` MUST mention that a user can ask a coding agent to help create or adapt workflow files. + +`README.md` MUST include a short `AGENTS.md` instruction snippet that tells agents to use Donna only when instructed by a developer, project instructions, or Donna itself. + +`README.md` MUST mention the primary workflows: + +- initializing a Donna project. +- listing discovered workflow artifacts. +- validating workflow artifacts. +- rendering workflow artifacts. +- starting a workflow. +- continuing queued workflow execution. +- completing an action request. +- reading built-in skill-style documentation. + +`README.md` MUST include command examples for: + +- `donna init`. +- `donna list`. +- `donna validate`. +- `donna render ...`. +- `donna run ...`. +- `donna continue`. +- `donna complete-action-request ...`. +- `donna skill usage`. + +Human-facing command examples SHOULD use command defaults. + +`README.md` MUST mention that `--protocol llm` is intended for coding agents and that `human` is the default protocol for terminal users. + +`README.md` MUST point readers to `./specs/` as the source of project behavior and architecture. + +`README.md` MUST describe development commands through `./bin/dev.sh` and `./bin/dev-tests.sh`. + +`README.md` MUST state that development commands are run through Docker-backed project scripts. + +## Style + +`README.md` SHOULD be concise and practical. + +`README.md` SHOULD prefer short explanations, bullet lists, and command examples. + +`README.md` SHOULD use a confident but accurate tone. + +`README.md` SHOULD avoid promising behavior that is only planned or not implemented. + +`README.md` SHOULD not be an exhaustive user manual; detailed command, configuration, and workflow guidance belongs in built-in skill documentation and specifications. diff --git a/specs/intro.md b/specs/intro.md index ba3a8065..95fb48d6 100644 --- a/specs/intro.md +++ b/specs/intro.md @@ -32,4 +32,5 @@ Detailed requirements for individual specifications are out of scope except for - `./specs/behavior/file_paths.md` — specification of Donna local project path, artifact id, and artifact section id syntax and resolution behavior. - `./specs/behavior/skill_fixtures.md` — specification of built-in skill documentation fixture behavior. - `./specs/documentation/changelog.md` — specification of changelog tooling, source files, version record structure, and entry format. +- `./specs/documentation/readme.md` — specification of the root `README.md` content, structure, source material, and tone. - `./specs/meta/general.md` — general requirements for specification documents. From af6a94768a56a616684a8d6505b2e9711063e861 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 18:56:41 +0200 Subject: [PATCH 59/98] fixing readme --- README.md | 470 ++++++++++++++++++++---------------------------------- 1 file changed, 169 insertions(+), 301 deletions(-) diff --git a/README.md b/README.md index 860da1bf..a9a83ddc 100644 --- a/README.md +++ b/README.md @@ -1,412 +1,280 @@ +# Donna -**Donna is in deep refactoring due to a rethink of the core concepts. You can safely use the `0.3.0` version, but later versions will introduce significant breaking changes. Be careful with introducing it into your core workflows.** +Help agents keep long-running work on a predefined path. -# FSM Driven Development +Donna allows agents to execute deterministic workflows regardless of the complexity of the algorithm. -Your agent will generate [state machines](https://en.wikipedia.org/wiki/Finite-state_machine) while executing state machines that are generated by state machines. +You can look at Donna as a co-processor for your agent that takes care of the control flow, so the agent can focus on the actual agentic work such as reasoning, code generation, etc. -Donna allows your agent to execute hundreds of consecutive steps without swaying away from the defined workflow. Branching, loops, nested calls, and recursion — all possible. +You define a workflow in a single readable Markdown file, after that your agent may ask Donna to guide it through the workflow, and Donna will take care of the rest by given instructions to the agent on what to do at each step. -## What is Donna? +Technically, workflow is a state machine and Donna its interpreter. That means agent can start child workflows from the parent workflow, write workflows on the fly, modify them while executing, etc. -Donna is a CLI tool that helps coding agents like Codex focus on the task at hand by keeping high-level control flow in explicit Donna workflows. Donna dictates what should be done at each step of the work, so the agent can focus on the actual piece. - -However, Donna is not an orchestrator; it is a tool for an agent — it can be used with any agent and does not require API keys or other credentials. You may look at Donna as the work diary or personal secretary for your agent. - -The core idea: - -- **Most high-level work is more algorithmic than it may seem at first glance.** -- **Most low-level work is less algorithmic than it may seem at first glance.** - -For example, it may be non-trivial to fix a particular unit test, but the overall process of polishing the codebase is quite linear: - -1. Ensure all tests pass. -2. Ensure the code is formatted correctly. -3. Ensure there are no linting errors. -4. Go to step 1 if you changed something in the process. -5. Finish. - -Coding agents show great results at each step of the process; however, they often struggle to manage meta-loops — they forget steps, misinterpret results, use the wrong approaches to run tools, etc. - -Donna executes such loops for the agents, thereby saving time, context, and tokens and simultaneously increasing the overall quality of work. - -## Features - -- **Deterministic workflows** — define fixed & validated control flow for agents to follow. -- **Saves context, tokens and time** — agents do not need to think when thinking is not required. -- **Readable artifacts** — all workflows and documentation are pure Markdown files with some [Jinja2](https://github.com/pallets/jinja) templating. -- **Artifact management** — non-fuzzy navigation and smart agent-focused rendering of artifacts. -- **Agent-centric behavior** — Donna guides agents through workflows, helps them be on the path, and fixes mistakes. -- **Extensible architecture** — implement your own operations, validators, renderers, and directives. -- **Project-local workflows** — keep reusable work procedures in your repository and adjust them to your team. +For example, you can have a workflow that guides the agent through the planing process, and at the final step, the agent can generate a new workflow with a detailed plan to execute and run it immediately. ## Example -Donna is developed via Donna itself. You can find real-life workflow examples in [workflows](./workflows) and documentation in [specs](./specs). +This repository uses Donna to develop Donna. The root [donna.toml](./donna.toml) and [workflows/](./workflows/) directory are real examples of a configured Donna project. -The example below is a simplified version of the polishing workflow that formats code, runs linters, and fixes found problems until all checks pass. It uses the single operation type `donna.lib.request_action` to ask the agent to perform specific instructions. +Check the current session before starting work: +```bash +donna status ``` - no issues -[ run_black ] ──▶ [ run_mypy ] ───────────▶ [ finish ] - ▲ │ - │ issues fixed │ - └────────────────┘ -``` - -

-An example source and comments -~~~ -# Polishing Workflow +List the workflows configured for this repository: -```toml donna -kind = "donna.lib.workflow" -start_operation_id = "run_black" +```bash +donna list ``` -Polish and refine the codebase. +Run the polishing workflow: -## Run Black - -```toml donna -id = "run_black" -kind = "donna.lib.request_action" +```bash +donna run @/workflows/polish.donna.md ``` -1. Run `black .` to format the codebase. -2. `{{ goto("run_mypy") }}` +When Donna reaches an action request, it prints the requested work, an action request id, and valid next operations. After the agent performs the work, it completes the request with one of those next operations. -## Run Mypy +The ids below are placeholders; use the exact id and next operation that Donna prints: -```toml donna -id = "run_mypy" -kind = "donna.lib.request_action" +```bash +donna complete-action-request AR-12-x @/workflows/example.donna.md:next_operation ``` -1. Run `mypy .` to check the codebase for type annotation issues. -2. If there are issues found that you can fix, fix them. -3. Ask the developer to fix any remaining issues manually. -4. If you made changes `{{ goto("run_black") }}`. -5. If no issues are found `{{ goto("finish") }}`. - -## Finish +Agents should normally use the LLM protocol: -```toml donna -id = "finish" -kind = "donna.lib.finish" +```bash +donna -p llm status ``` -Polishing is complete. -~~~ +The default `human` protocol is intended for terminal users. -What you may notice: +## Rationale -- The workflow is described in a readable Markdown file. -- The workflow has a loop. -- Each H1 and H2 section has a config block, which is a TOML in code fences with `donna` marker. Those configs are invisible to the agent, but Donna uses them to understand the artifact structure. -- H1 section describes the workflow as a whole. -- H2 sections describe workflow operations. -- The workflow has two `donna.lib.request_action` operations (`run_black`, `run_mypy`) and one `donna.lib.finish` (`finish`). -- Transitions between operations are defined via `{{ goto("operation_id") }}` Jinja2 calls in the body of operations. -- `donna.lib.request_action` is an operation that tells Donna to display instructions to the agent and wait for the agent to complete them. That allows the agent to focus on short, precise instructions, execute them, and advance the workflow. -- `kind` attributes of sections are valid Python import paths, so you can easily extend Donna with your own code. +Many project workflows are partly deterministic and partly judgment-based. Running formatters, validators, tests, or small scripts is deterministic. Deciding how to fix a failed check, adjust a design, or judge whether a result satisfies a requirement needs an agent. -Directives, like `{{ goto("operation_id") }}`, render itself depending on the context: +Donna separates those concerns without requiring an API key, a hosted service, or a separate orchestrator: -- For the agent, they render an exact CLI command to run, such as `donna -p llm --config /path/to/project/donna.toml complete-action-request '@/workflows/polish.donna.md:finish'`. -- For Donna, they render a specific marker that can be extracted and used to analyze an artifact. For example, Donna uses `goto` directives to build an FSM of the workflow and validate it before running: does each operation exist, can the workflow be completed, are there unreachable operations, etc. +- workflow files define the allowed control flow. +- Donna runs deterministic operations and preserves session state. +- action requests pause execution and ask the agent to do the non-deterministic work. +- the agent completes each request by choosing one of the workflow's declared next operations. -Generally speaking, **all you need is `donna.lib.request_action` operation** — it is enough to achieve a great deal of automation by delegating some decisions to the agent. However, there are some more specific operations that simplify things and make workflows more agile or performant. +This keeps long-running work explicit, resumable, and easier to validate, while leaving judgment and file edits with the agent. -
+## Features -You can find a more complex implementation of the same workflow in the [polish.donna.md](./workflows/polish.donna.md) file. It demonstrates other Donna operations, such as running scripts directly and branching. +- CLI workflow runner for coding agents and humans supervising them. +- Project-local configuration with `donna.toml`. +- Markdown workflow artifacts ending with `.donna.md`. +- Session state stored under the configured `session_dir`. +- Workflow discovery from configured `workflow_dirs`. +- Built-in operation kinds for requesting agent action, running shell scripts, printing output, and finishing workflow tasks. +- Artifact listing, rendering, and validation commands. +- Human, LLM, and JSON Lines automation output protocols. +- Built-in skill-style documentation through `donna skill ...`. +- Optional journal forwarding through `donna.toml`. -## Installation +## Quick Usage -1. Install `donna` package. +Create a starter configuration: ```bash -uv tool install donna - -# pipx install donna +donna init ``` -2. Initialize Donna in your project. +List discovered workflow artifacts: ```bash -cd -donna init +donna list ``` -Donna will create `donna.toml` in your project root. The configured session directory is created lazily by runtime commands. - -3. Ask your agent to do something like `$donna-do Add a button that …`. The agent will discover the appropriate workflow and execute it. - -## Skills +Validate every discovered workflow artifact: -- `donna-do` — use Donna to perform a specific task in the current Donna session. Creates a new session if there is no one. -- `donna-start` — create new Donna session state and tell the agent to use Donna to perform all further work. -- `donna-stop` — stop using Donna to perform work — the agent should switch to its own flow control. - -## Usage - -**Donna is a CLI tool for agents.** You rarely need to use it directly. - -Commands you may need: - -- `donna init` — Initialize Donna in your project. -- `donna new-session` — create fresh session state. -- `donna list` — list workflows with short descriptions. - -Donna can send internal journal records to a third-party tool. Configure it in `donna.toml`: - -```toml -[journal] -cmd = ["cli-tool", "--message", "{message}"] +```bash +donna validate --all ``` -`cmd` is a list of command arguments. If an argument starts with `{` and ends with `}`, Donna treats the whole argument as a `JournalRecord` attribute name and replaces it with that value. Donna validates placeholders when loading config. - -Supported attributes: - -- `timestamp` — record creation time, formatted as ISO-8601. -- `actor_id` — actor that created the record; empty string when unknown. -- `message` — single-line journal message. -- `current_task_id` — current task id; empty string when no task is active. -- `current_work_unit_id` — current work unit id; empty string when no work unit is active. -- `current_operation_id` — current operation artifact section id; empty string when no operation is active. - -By default `journal.cmd` is omitted and Donna treats it as `None`, so no journal writing is performed. - -Donna still prints newly created internal journal records immediately using the selected protocol formatter, so agents receive live feedback even when no external journal command is configured. - -Use `donna --help` for a quick reference. +Render a workflow as Donna sees it: -You find detailed documentation in the built-in skill documents — they are readable and always accurate: +```bash +donna render @/workflows/polish.donna.md --mode view +``` -- `donna skill usage` — full list of commands and how to use them. -- `donna skill workflows` — what Donna workflows are and how to use them on the filesystem. -- `donna skill configuration` — how to configure `donna.toml`. -- `donna skill initialization` — how to initialize or refresh a Donna workspace. +Start a workflow in the current session: -The documentation below covers aspects important to humans and partially duplicates the agent's instructions. +```bash +donna run @/workflows/polish.donna.md +``` -## Batteries Included +Inspect the current session: -Donna comes with documentation that helps agents work in a smart way, while workflows live in project-local `workflows/` directories. +```bash +donna status +``` -However, **I encourage you to experiment and implement your own workflows**. Meta-programming is fun; specialized workflows are more efficient. +Continue queued workflow execution: -By default, Donna uses the next approach to introduce changes in your project: +```bash +donna continue +``` -1. Prepare a Request for Change (RFC) document that describes the required changes. -2. Create a workflow that implements the changes described in the RFC. -3. Execute new workflow. +Complete an action request with the id and next operation printed by Donna: -Additionally, Donna will: +```bash +donna complete-action-request AR-12-x @/workflows/example.donna.md:next_operation +``` -- choose fast or slow route depending on the complexity of the changes required; -- find and run (if any) polishing workflow to ensure the codebase is in a good state after the changes; +Read the built-in usage documentation: -Note that example Donna workflows are designed to be reliable and useful for a wide range of projects. They may not be optimal in terms of token usage or speed for your particular project. The intended use of Donna is to implement your own workflows that account for your project's specifics. +```bash +donna skill usage +``` -Points of interest: +Useful related commands: -- [@/workflows/rfc/specs/request_for_change.md](./workflows/rfc/specs/request_for_change.md) — documentation for the RFC document. -- [@/workflows/rfc/request.donna.md](./workflows/rfc/request.donna.md) — workflow to create a RFC document. -- [@/workflows/rfc/plan.donna.md](./workflows/rfc/plan.donna.md) — workflow to plan work on an RFC and create a new workflow. -- [@/workflows/rfc/do.donna.md](./workflows/rfc/do.donna.md) — meta workflow to automate the whole work from a developer request to final verification. +```bash +donna status +donna details +donna new-session +donna version +``` -## Artifacts on Filesystem +For coding agents, put global options before the subcommand: -- Artifacts are text files Donna reads and validates. In practice they are usually Markdown workflows stored as `.donna.md` files. General documentation uses normal `.md` files. -- Donna discovers workflow artifacts by recursively scanning the directories listed in `donna.toml:workflow_dirs`. +```bash +donna -p llm --config /path/to/project/donna.toml status +``` -By default, Donna uses these artifact areas: +## Installation -- `workflows/` — project-owned workflows. -- `.agents/donna/` — project-local Donna documentation, when present. -- `.session/donna/` — session artifacts and Donna runtime state. +Install Donna from PyPI with `uv`: -### Rendering +```bash +uv tool install donna +``` -Markdown artifacts are Jinja2 templates. Donna uses multiple rendering modes for different purposes. +Install Donna from PyPI with `pip`: -More about Jinja2 rendering is described a bit further. +```bash +python -m pip install donna +``` -### Artifacts Discovery +Donna requires Python 3.12 or newer. -Artifact ids are project-relative filepaths prefixed with `@/`. Section ids append `:section_id` to the artifact id. +After installation, initialize a project from the directory that should contain `donna.toml`: -Examples: +```bash +donna init +``` -- `@/workflows/polish.donna.md` -- `@/workflows/rfc/request.donna.md` -- `@/.session/donna/execute_rfc.donna.md:review_changes` +Review the generated configuration before relying on it. The starter file is intentionally small and may need project-specific workflow directories or journal forwarding. -You and agents can `list` workflow artifacts and `validate` Donna artifacts. +## Configuration -- `donna -p llm list` — shows workflow descriptions from their h1 sections. -- `donna -p llm validate ...` — validates one or more artifacts. -- `donna -p llm validate --all` — validates every discovered artifact. +A Donna project is configured by a TOML file named `donna.toml`. If `--config` is omitted, Donna searches upward from the current working directory until it finds `donna.toml`; the directory containing that file is the project root. -Artifact inputs accept root-anchored paths like `@/workflows/polish.donna.md`, relative paths like `./workflows/polish.donna.md`, and absolute paths inside the project root. +Minimal configuration: -You can find all workflows with `donna -p llm list`. +```toml +version = 1 +``` -## Sessions +The starter configuration generated by `donna init` includes the default session directory and workflow discovery directories: -`/.session/donna/` contains the current state of work performed by Donna: runtime state plus temporary documents and workflows created during the session. +```toml +version = 1 -Donna initializes empty session state automatically when a session-aware command needs it and no session state exists. +session_dir = ".session/donna" -The developer can explicitly reset the current session state with `donna -p human new-session`. +workflow_dirs = [ + "./workflows", + "./.session/donna", +] +``` -- `new-session` resets tasks, work units, and action requests. +Review and edit the starter configuration for the project. A coding agent can help create or adapt workflow files after the project layout and desired workflows are clear. -The agent is encouraged not to manage sessions directly, because it doesn't have enough context to decide when session artifacts may be safely removed. +The session directory stores Donna runtime state. Workflow directories are scanned recursively for `.donna.md` files; missing workflow directories are ignored. -## Workflows +Detailed configuration behavior is specified in [specs/behavior/config.md](./specs/behavior/config.md). Command behavior is specified in [specs/behavior/cli.md](./specs/behavior/cli.md). -Workflows are [state machines](https://en.wikipedia.org/wiki/Finite-state_machine). They are defined in Markdown files with an h1 section of type `donna.lib.workflow` and multiple h2 sections that implement operations. +Project agent instructions can include a short Donna rule like this: -Donna tracks dependencies between operations and validates the workflow before running it. So, if you or your agent do something wrong, you'll get a clear error message from Donna. +```markdown +Use Donna only when explicitly instructed by a developer, by project instructions, or by Donna itself. +Before using Donna in a session, read `donna -p llm skill usage`. +Use `donna -p llm ...` for agent-facing command output. +``` -You can run workflow as `donna -p llm run `. +## Workflow Files -To execute a workflow, Donna uses a simplified virtual machine (VM) that maintains the current state of all workflows executed in the current session. +Donna workflow artifacts are Markdown files ending with `.donna.md`. Donna discovers them by recursively scanning the configured `workflow_dirs`. -
-What you may want to know about workflows implementation -- `.session/donna/state.json` file contains the current state of all running workflows in the current session. -- Each workflow executes in the context of its own task, which is a distant analog of a call stack frame. So, we may look at workflows as functions. -- Of course, workflows may call other workflows as subroutines. At any moment, only last executed workflow is active. -- Task has a context that is accessible by operations. There is an issue [command to read/write task context](https://github.com/Tiendil/donna/issues/47) to allow agents and humans to edit task context. -- The internal VM operates in terms of work units. An operation can produce multiple work units, so it should be possible to implement different interesting scenarios; however. -
+A workflow file has one H1 section for the workflow and H2 sections for operations. Section config is written in fenced `toml donna` code blocks. Transitions are declared in operation config or request text, depending on the operation kind, and validated before execution. -### Operations +Common built-in operation kinds: -You can find detailed docs on built-in operations in `donna skill workflows`. +- `donna.lib.request_action` pauses and asks the agent to perform work. +- `donna.lib.run_script` runs a deterministic shell script from the project root. +- `donna.lib.output` prints information and continues. +- `donna.lib.finish` finishes the workflow task. -Here is a short list of them: +Artifact ids are project-root anchored paths such as: -- `donna.lib.request_action` — request the agent to perform specific instructions. -- `donna.lib.run_script` — run a script from the environment Donna is running in. Choose the next operation based on the return code. Can store `stdout` and `stderr` in the task context. -- `donna.lib.output` — output a cell with specific content and immediately goes to the next operation. -- `donna.lib.finish` — finish the workflow. This operation MUST be present in the workflow and MUST end all possible execution paths. +```text +@/workflows/polish.donna.md +``` -### Error handling +Artifact section ids append a section id: -Donna can detect errors (in artifacts, in execution, etc). If an error can be fixed by the agent or the developer, Donna will output a detailed error description with a list of ways to fix it. +```text +@/workflows/polish.donna.md:finish +``` -
-An example of error message from Donna +Read the detailed workflow documentation with: ```bash -$ donna -p llm run @/workflows/polish.donna.md - -kind=artifact_validation_error -media_type=text/markdown -artifact_id=@/workflows/polish.donna.md -error_code=donna.artifacts.section_not_found -section_id=run_autoflake_scriptx - -Error in artifact '@/workflows/polish.donna.md', section 'run_autoflake_scriptx': Section `run_autoflake_scriptx` is not available in artifact `@/workflows/polish.donna.md`. - -Ways to fix: - -- Check the section id for typos. -- Ensure the section exists in the artifact +donna skill workflows ``` -
- -### Generating workflows - -The power of Donna comes from the ability to create workflows on the fly and execute them immediately. So, you can create a workflow that creates a workflow that creates a workflow that does something useful :) - -You can even modify the workflow while it is executing; the only requirement is not to lose the ids of the operations referenced in action requests. - -The simplest example of such generation is currently used as a primary way for Donna to work on the current project: +The built-in skill documentation set is specified in [specs/behavior/skill_fixtures.md](./specs/behavior/skill_fixtures.md). -1. Create a document with a change description. -2. Generate a workflow that defines an order of applying changes. -3. Execute the generated workflow. +## Specifications -### Discovering workflows +Project behavior and architecture are specified in [specs/](./specs/). Start with [specs/intro.md](./specs/intro.md) for the index. -If you want to run a child workflow from an operation, you can just instruct an agent like `Run the workflow @/workflows/my-cool-workflow.donna.md` and the agent will find it and run. +Important references: -However, it is not very agile. Instead, I suggest you describe the desired outcome and let the agent find the most suitable workflow. In that case, you'll be able to define customized workflows for specific types of changes and let the agent choose the best one for the current situation. +- [specs/behavior/cli.md](./specs/behavior/cli.md) describes CLI commands, output protocols, and command behavior. +- [specs/behavior/config.md](./specs/behavior/config.md) describes `donna.toml`. +- [specs/behavior/file_paths.md](./specs/behavior/file_paths.md) describes project paths, artifact ids, and artifact section ids. +- [specs/behavior/skill_fixtures.md](./specs/behavior/skill_fixtures.md) describes built-in documentation fixtures. +- [specs/documentation/readme.md](./specs/documentation/readme.md) describes this README's required structure and content. -For example, you can have two workflows `@/workflows/write-backend-test.donna.md` and `@/workflows/write-frontend-test.donna.md`, and your operation can say `Run the workflow that will write a test for the current change`, and the agent will choose the most suitable workflow based on the context and the workflow descriptions. +The root README is an introduction, not the source of truth for detailed behavior. -## Jinja2 rendering +## Development -Markdown artifacts are Jinja2 templates that are rendered immediately upon loading, before parsing Markdown. +Development commands are run through Docker-backed project scripts. -There are multiple rendering modes that Donna uses for different purposes: +Run the full test suite: -1. `view` — artifact is rendered for displaying to the agent or the developer. -2. `execute` — artifact is rendered for execution. This mode has access to the current task context, so we can add a variable from it into the operation instructions. -3. `analyze` — artifact is rendered to be analyzed by Donna itself. For example, to extract `goto` directives from operation bodies and validate the workflow structure. - -All Jinja2 rendering is supported, except inheritance-related features. So, an artifact is self contained template. - -The rendering is performed before processing Markdown, so you can use Jinja2 features (like loops, conditionals, etc) to generate complex artifacts. - -### Directives - -Donna defines a set of built-in Jinja2 functions that provide artifacts with their special capabilities. Such functions are called directives. - -Directives are used in the next way: `{{ python.import.path() }}`. - -You can find detailed documentation of all built-in directives in `donna skill workflows`. - -Here they are: - -1. `donna.lib.goto` — references the next workflow operation to execute. In `view`/`execute` modes, it renders an exact CLI command to advance the workflow. In `analysis` mode, it is used to construct and validate an FSM for the workflow. -2. `donna.lib.task_variable` — in `view` mode renders a placeholder with a note about task-variable substitution, in `execute` mode renders the actual task-context value. In `analysis`, it will be used to control a set of variables used in the artifact. - -## Documentation - -Specifications are plain Markdown documentation. Donna does not treat them as artifacts or validate them as workflows. - -## Extending Donna - -All Donna logic is referenced by Python import paths. That means: - -- You can implement your own functionality and use it with Donna. -- You can enrich your Python packages with additional code to work with Donna. - -What you can implement: - -- Custom sections (including operations) for Donna artifacts. Check [./donna/primitives/artifacts](./donna/primitives/artifacts) and [./donna/primitives/sections](./donna/primitives/sections) subpackages for examples. -- Custom rendering directives. Check [./donna/primitives/directives](./donna/primitives/directives) subpackage for examples. - -Donna workflow artifacts are Markdown files ending with `.donna.md` inside directories configured by `workflow_dirs` in your project `donna.toml`. - -Sections and directives are used directly in artifacts by their Python import paths. - -## Feedback wanted - -Donna is still young and has multiple experimental features — I really appreciate any feedback, ideas, and contributions to make it better. - -Especially, it would be nice to hear about +```bash +./bin/dev-tests.sh +``` -- problems with configuration and usage; -- real-life use cases, especially with meta programming: fun workflows that generate workflows and so on; -- your particular needs that Donna potentially can cover, but lacks some functionality now. +Run a specific development command inside the project container: -How to reach me: +```bash +./bin/dev.sh poetry run pytest +``` -- Create an [issue](https://github.com/Tiendil/donna/issues). Any format and theme is welcome. -- Comment on one of the existing issues. Feedback, especially on [proposals](https://github.com/Tiendil/donna/issues?q=is%3Aissue%20state%3Aopen%20label%3Aproposal). -- Start a [discussion](https://github.com/Tiendil/donna/discussions). +Run Donna from the current development checkout: -## Projects that use Donna +```bash +./bin/dev.sh poetry run donna --help +``` -- [Feeds Fun](https://github.com/Tiendil/feeds.fun) — news reader with tags, scoring, and AI. +The repository also uses Donna itself. Its local workflows live in [workflows/](./workflows/), and the current project configuration is [donna.toml](./donna.toml). From ea25126810a77c90b0931168cc3168f0f42c5dad Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:00:29 +0200 Subject: [PATCH 60/98] wip --- README.md | 34 ++-------------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index a9a83ddc..dfce38ba 100644 --- a/README.md +++ b/README.md @@ -14,41 +14,11 @@ For example, you can have a workflow that guides the agent through the planing p ## Example -This repository uses Donna to develop Donna. The root [donna.toml](./donna.toml) and [workflows/](./workflows/) directory are real examples of a configured Donna project. +I use Donna to develop Donna itself — that's the way ;-) So, you can find real examples of workflows in the [./workflows](./workflows) folder of this repository. You can start with [./workflows/polish.donna.md](./workflows/polish.donna.md) that goes in loop over fixing issues found by formatters, linters, type checkers and tests until the codebase is polished. -Check the current session before starting work: +Below you'll find a simplified example to show the core concepts of Donna workflows and how they work together. -```bash -donna status -``` - -List the workflows configured for this repository: - -```bash -donna list -``` - -Run the polishing workflow: - -```bash -donna run @/workflows/polish.donna.md -``` - -When Donna reaches an action request, it prints the requested work, an action request id, and valid next operations. After the agent performs the work, it completes the request with one of those next operations. - -The ids below are placeholders; use the exact id and next operation that Donna prints: - -```bash -donna complete-action-request AR-12-x @/workflows/example.donna.md:next_operation -``` - -Agents should normally use the LLM protocol: - -```bash -donna -p llm status -``` -The default `human` protocol is intended for terminal users. ## Rationale From 471266aea3778a33500c7cb5c00bb323c90acc34 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:01:12 +0200 Subject: [PATCH 61/98] wip --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dfce38ba..91bef92d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ I use Donna to develop Donna itself — that's the way ;-) So, you can find real Below you'll find a simplified example to show the core concepts of Donna workflows and how they work together. - + ## Rationale From 62bb2a14cb959049df1d67d84416c2acdfb27519 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:03:13 +0200 Subject: [PATCH 62/98] readme --- README.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 91bef92d..6fae9e10 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,89 @@ For example, you can have a workflow that guides the agent through the planing p I use Donna to develop Donna itself — that's the way ;-) So, you can find real examples of workflows in the [./workflows](./workflows) folder of this repository. You can start with [./workflows/polish.donna.md](./workflows/polish.donna.md) that goes in loop over fixing issues found by formatters, linters, type checkers and tests until the codebase is polished. -Below you'll find a simplified example to show the core concepts of Donna workflows and how they work together. +Below you'll find a simplified workflow based on the built-in workflow documentation example. It checks the current time, asks the agent whether it is time to drink tea, and branches according to the agent's answer. - +````markdown +# Example Workflow + +The H1 section describes the whole workflow. Donna shows this summary in +`donna list`. + +This workflow checks the current time, asks the agent whether it is tea time, +and branches on the answer. + +## Get Current Time + +```toml donna +# Every operation should have a stable local id. +id = "get_current_time" + +# `run_script` is deterministic: Donna can execute it without asking the agent. +kind = "donna.lib.run_script" + +# Save stdout into task context so later operations can render it. +save_stdout_to = "current_time" + +# `run_script` branches through config fields based on the command exit code. +goto_on_success = "ask_about_tea" +goto_on_failure = "finish" +``` + +```bash donna script +#!/usr/bin/env bash +date +%H:%M +``` + +## Ask About Tea + +```toml donna +id = "ask_about_tea" + +# `request_action` pauses the workflow and asks the agent to act. +kind = "donna.lib.request_action" +``` + +The current time is: + +```text +{{ donna.lib.task_variable("current_time") }} +``` + +Is it time to drink tea? + +1. If yes, `{{ donna.lib.goto("turn_on_kettle") }}`. +2. If no, `{{ donna.lib.goto("finish") }}`. + +## Turn On Kettle + +```toml donna +id = "turn_on_kettle" +kind = "donna.lib.request_action" +``` + +Turn on the kettle, then `{{ donna.lib.goto("finish") }}`. + +## Finish + +```toml donna +id = "finish" + +# `finish` completes this workflow task and prints the final message. +kind = "donna.lib.finish" +``` + +The workflow is complete. Report the result to the developer. +```` + +What this example shows: + +- H1 defines the workflow; H2 sections define operations. +- `toml donna` blocks configure how Donna interprets sections. +- `bash donna script` contains the script for a deterministic `run_script` operation. +- `save_stdout_to` stores command output in workflow task context. +- `donna.lib.task_variable(...)` renders a value from task context for the agent. +- `donna.lib.goto(...)` declares valid next operations for an action request. +- `complete-action-request` must use one of the next operations declared by the current action request. ## Rationale From c5a9e78a9882998ea7c35e24684a95e6c1b860a1 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:05:20 +0200 Subject: [PATCH 63/98] wip --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6fae9e10..fcb64a48 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Donna -Help agents keep long-running work on a predefined path. +**A CLI tool that helps agents keep long-running work on a predefined path.** Donna allows agents to execute deterministic workflows regardless of the complexity of the algorithm. @@ -8,7 +8,7 @@ You can look at Donna as a co-processor for your agent that takes care of the co You define a workflow in a single readable Markdown file, after that your agent may ask Donna to guide it through the workflow, and Donna will take care of the rest by given instructions to the agent on what to do at each step. -Technically, workflow is a state machine and Donna its interpreter. That means agent can start child workflows from the parent workflow, write workflows on the fly, modify them while executing, etc. +**Workflow is a state machine and Donna its interpreter.** That means agent can start child workflows from the parent workflow, write workflows on the fly, modify them while executing, etc. For example, you can have a workflow that guides the agent through the planing process, and at the final step, the agent can generate a new workflow with a detailed plan to execute and run it immediately. From 8323b9f3f478945018b4afb551876cf22933e25a Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:06:14 +0200 Subject: [PATCH 64/98] wip --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fcb64a48..f9410add 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ For example, you can have a workflow that guides the agent through the planing p ## Example -I use Donna to develop Donna itself — that's the way ;-) So, you can find real examples of workflows in the [./workflows](./workflows) folder of this repository. You can start with [./workflows/polish.donna.md](./workflows/polish.donna.md) that goes in loop over fixing issues found by formatters, linters, type checkers and tests until the codebase is polished. +I use Donna to develop Donna itself — you can find real examples of workflows in the [./workflows](./workflows) folder of this repository. You can start with [./workflows/polish.donna.md](./workflows/polish.donna.md) that goes in loop over fixing issues found by formatters, linters, type checkers and tests until the codebase is polished. Below you'll find a simplified workflow based on the built-in workflow documentation example. It checks the current time, asks the agent whether it is time to drink tea, and branches according to the agent's answer. From 28b2d4a3140868633a8d414d44c7ee9faa13514d Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:07:08 +0200 Subject: [PATCH 65/98] wip --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f9410add..a3e9093b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ For example, you can have a workflow that guides the agent through the planing p I use Donna to develop Donna itself — you can find real examples of workflows in the [./workflows](./workflows) folder of this repository. You can start with [./workflows/polish.donna.md](./workflows/polish.donna.md) that goes in loop over fixing issues found by formatters, linters, type checkers and tests until the codebase is polished. -Below you'll find a simplified workflow based on the built-in workflow documentation example. It checks the current time, asks the agent whether it is time to drink tea, and branches according to the agent's answer. +Below you'll find a simplified workflow that checks the current time, asks the agent whether it is time to drink tea, and branches according to the agent's answer. ````markdown # Example Workflow From 38418bbcc324274e2585f8b7fd560d28671c237e Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:08:06 +0200 Subject: [PATCH 66/98] wip --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index a3e9093b..0310442c 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,7 @@ I use Donna to develop Donna itself — you can find real examples of workflows Below you'll find a simplified workflow that checks the current time, asks the agent whether it is time to drink tea, and branches according to the agent's answer. ````markdown -# Example Workflow - -The H1 section describes the whole workflow. Donna shows this summary in -`donna list`. +# Is it time to drink tea? This workflow checks the current time, asks the agent whether it is tea time, and branches on the answer. From cb0bb239cdba043bbbf5125d2dd67f0173a89140 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:09:49 +0200 Subject: [PATCH 67/98] readme --- README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/README.md b/README.md index 0310442c..47632524 100644 --- a/README.md +++ b/README.md @@ -27,16 +27,9 @@ and branches on the answer. ## Get Current Time ```toml donna -# Every operation should have a stable local id. id = "get_current_time" - -# `run_script` is deterministic: Donna can execute it without asking the agent. kind = "donna.lib.run_script" - -# Save stdout into task context so later operations can render it. save_stdout_to = "current_time" - -# `run_script` branches through config fields based on the command exit code. goto_on_success = "ask_about_tea" goto_on_failure = "finish" ``` @@ -50,8 +43,6 @@ date +%H:%M ```toml donna id = "ask_about_tea" - -# `request_action` pauses the workflow and asks the agent to act. kind = "donna.lib.request_action" ``` @@ -79,8 +70,6 @@ Turn on the kettle, then `{{ donna.lib.goto("finish") }}`. ```toml donna id = "finish" - -# `finish` completes this workflow task and prints the final message. kind = "donna.lib.finish" ``` From 937f1ea9fdfd0b7c1caffc350f4eeee2dd679b4e Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:22:19 +0200 Subject: [PATCH 68/98] readme --- README.md | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 47632524..de984bc2 100644 --- a/README.md +++ b/README.md @@ -73,17 +73,46 @@ id = "finish" kind = "donna.lib.finish" ``` -The workflow is complete. Report the result to the developer. +The workflow is complete. You are a good butler. ```` -What this example shows: +The workflow is a simple decission tree: -- H1 defines the workflow; H2 sections define operations. -- `toml donna` blocks configure how Donna interprets sections. -- `bash donna script` contains the script for a deterministic `run_script` operation. -- `save_stdout_to` stores command output in workflow task context. -- `donna.lib.task_variable(...)` renders a value from task context for the agent. -- `donna.lib.goto(...)` declares valid next operations for an action request. +``` +[Get Current Time] + | + v + [Ask About Tea] + | + +----+----+ + | | + yes no + | | + v | + [Turn On | + Kettle] | + | | + +----+----+ + | + v + [Finish] + +``` + + +with an operation in each node. There are different kinds of operations, let's go + +How this example works: + +- The H1 section is the workflow section. It gives Donna the workflow title and summary that appear in `donna list`. +- Each H2 section is an operation section. Donna runs operation sections in the order selected by the workflow state machine. +- `toml donna` blocks configure section type and behavior. Here, the operation types are `donna.lib.run_script`, `donna.lib.request_action`, and `donna.lib.finish`. +- `Get Current Time` is a deterministic `run_script` operation. Donna runs the shell script from the project root, saves stdout as `current_time`, and moves to `ask_about_tea` on success. +- `Ask About Tea` is a `request_action` operation. The agent will see the rendered current time, the question, and the two allowed transitions: `turn_on_kettle` or `finish`. +- `donna.lib.task_variable("current_time")` is rendered for the agent as the value captured from the script output. +- `donna.lib.goto(...)` is rendered for the agent as a concrete completion option and also declares the valid next operation for Donna's validation. +- `Turn On Kettle` is another `request_action` operation. The agent will see the instruction to turn on the kettle and then complete the action request with the `finish` transition. +- `Finish` is a `finish` operation. The agent will see the final workflow message, and Donna will complete the workflow task. - `complete-action-request` must use one of the next operations declared by the current action request. ## Rationale From 91ff9ceade0c7375b54b4a1ee7eef982352260cb Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:23:24 +0200 Subject: [PATCH 69/98] readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index de984bc2..def9ca5e 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,14 @@ The workflow is a simple decission tree: ``` +```mermaid + flowchart TD + get_current_time[Get Current Time] --> ask_about_tea[Ask About Tea] + ask_about_tea -->|yes| turn_on_kettle[Turn On Kettle] + ask_about_tea -->|no| finish[Finish] + turn_on_kettle --> finish + ``` + with an operation in each node. There are different kinds of operations, let's go From e5a9e88ae13e69d9a8d540d8cd2255de204ca221 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:24:52 +0200 Subject: [PATCH 70/98] wip --- README.md | 53 +++++++++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index def9ca5e..10ce2c32 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,31 @@ I use Donna to develop Donna itself — you can find real examples of workflows Below you'll find a simplified workflow that checks the current time, asks the agent whether it is time to drink tea, and branches according to the agent's answer. +Here is its schema: + +``` +[Get Current Time] + | + v + [Ask About Tea] + | + +----+----+ + | | + yes no + | | + v | + [Turn On | + Kettle] | + | | + +----+----+ + | + v + [Finish] + +``` + +Actual workflow code: + ````markdown # Is it time to drink tea? @@ -76,36 +101,8 @@ kind = "donna.lib.finish" The workflow is complete. You are a good butler. ```` -The workflow is a simple decission tree: -``` -[Get Current Time] - | - v - [Ask About Tea] - | - +----+----+ - | | - yes no - | | - v | - [Turn On | - Kettle] | - | | - +----+----+ - | - v - [Finish] - -``` -```mermaid - flowchart TD - get_current_time[Get Current Time] --> ask_about_tea[Ask About Tea] - ask_about_tea -->|yes| turn_on_kettle[Turn On Kettle] - ask_about_tea -->|no| finish[Finish] - turn_on_kettle --> finish - ``` with an operation in each node. There are different kinds of operations, let's go From 3d17689420e0e1564da61b2c07e64a67ca70ba94 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:30:13 +0200 Subject: [PATCH 71/98] wip --- README.md | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 10ce2c32..d80e0220 100644 --- a/README.md +++ b/README.md @@ -101,24 +101,32 @@ kind = "donna.lib.finish" The workflow is complete. You are a good butler. ```` - - - - -with an operation in each node. There are different kinds of operations, let's go - How this example works: - The H1 section is the workflow section. It gives Donna the workflow title and summary that appear in `donna list`. -- Each H2 section is an operation section. Donna runs operation sections in the order selected by the workflow state machine. +- Each H2 section is an operation section. Donna runs operations in the order selected by the workflow state machine. - `toml donna` blocks configure section type and behavior. Here, the operation types are `donna.lib.run_script`, `donna.lib.request_action`, and `donna.lib.finish`. -- `Get Current Time` is a deterministic `run_script` operation. Donna runs the shell script from the project root, saves stdout as `current_time`, and moves to `ask_about_tea` on success. +- `Get Current Time` is a `run_script` operation. Donna runs the shell script from the project root, saves stdout as `current_time`, and moves to `ask_about_tea` on success. No agent interaction required here, because it is pure deterministic work. - `Ask About Tea` is a `request_action` operation. The agent will see the rendered current time, the question, and the two allowed transitions: `turn_on_kettle` or `finish`. - `donna.lib.task_variable("current_time")` is rendered for the agent as the value captured from the script output. -- `donna.lib.goto(...)` is rendered for the agent as a concrete completion option and also declares the valid next operation for Donna's validation. +- `donna.lib.goto(...)` is rendered for the agent as a concrete CLI command to exectute. - `Turn On Kettle` is another `request_action` operation. The agent will see the instruction to turn on the kettle and then complete the action request with the `finish` transition. - `Finish` is a `finish` operation. The agent will see the final workflow message, and Donna will complete the workflow task. -- `complete-action-request` must use one of the next operations declared by the current action request. + +For example, here what the agent will see on the `Get Current Time` step of the tea workflow: + +````markdown + The current time is: + + ```text + 17:16 + ``` + + Is it time to drink tea? + + 1. If yes, `donna -p llm --config '' complete-action-request '@/workflows/tea.donna.md:turn_on_kettle'`. + 2. If no, `donna -p llm --config '' complete-action-request '@/workflows/tea.donna.md:finish'`. +```` ## Rationale From 602d995078771720018173135124dd588f4488cf Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:32:12 +0200 Subject: [PATCH 72/98] wip --- README.md | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d80e0220..b7370e9d 100644 --- a/README.md +++ b/README.md @@ -116,16 +116,39 @@ How this example works: For example, here what the agent will see on the `Get Current Time` step of the tea workflow: ````markdown - The current time is: +--DONNA-CELL OuP2T9brQYmvESMHsbrDlw BEGIN-- +kind=session_state_status +media_type=text/markdown +pending_action_requests=1 +queued_work_units=0 +tasks=1 + +The session is AWAITING YOUR ACTION. You have pending action requests to address. + +- If the developer asked you to start working on a new task, you MUST ask if you should start a new session + or continue working on the current action requests. +- Otherwise, you MUST address the pending action requests before proceeding. +--DONNA-CELL OuP2T9brQYmvESMHsbrDlw END-- +--DONNA-CELL fxh3PDZ1Qy-c3zvai_T5Qw BEGIN-- +kind=action_request +media_type=text/markdown +action_request_id=AR-11-l + +**This is an action request for the agent. You MUST follow the instructions below.** - ```text - 17:16 - ``` +The current time is: + +```text +19:31 + +``` + +Is it time to drink tea? - Is it time to drink tea? +1. If yes, `donna -p llm --config '/home/tiendil/repos/mine/donna/.session/readme-example-test/donna.toml' complete-action-request '@/workflows/tea.donna.md:turn_on_kettle'`. +1. If no, `donna -p llm --config '/home/tiendil/repos/mine/donna/.session/readme-example-test/donna.toml' complete-action-request '@/workflows/tea.donna.md:finish'`. +--DONNA-CELL fxh3PDZ1Qy-c3zvai_T5Qw END-- - 1. If yes, `donna -p llm --config '' complete-action-request '@/workflows/tea.donna.md:turn_on_kettle'`. - 2. If no, `donna -p llm --config '' complete-action-request '@/workflows/tea.donna.md:finish'`. ```` ## Rationale From 7d9864962d4041dce3be9adf71f37b20f1ef2748 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:33:42 +0200 Subject: [PATCH 73/98] wip --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index b7370e9d..104900a8 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,10 @@ kind = "donna.lib.finish" The workflow is complete. You are a good butler. ```` +
+ How it works + + How this example works: - The H1 section is the workflow section. It gives Donna the workflow title and summary that appear in `donna list`. @@ -151,6 +155,8 @@ Is it time to drink tea? ```` +
+ ## Rationale Many project workflows are partly deterministic and partly judgment-based. Running formatters, validators, tests, or small scripts is deterministic. Deciding how to fix a failed check, adjust a design, or judge whether a result satisfies a requirement needs an agent. From b087c5964204fb3bace7b20c1f89d22c2313439a Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:43:31 +0200 Subject: [PATCH 74/98] wip --- README.md | 26 ++++++--- workflows/examples/time_to_drink_tea.donna.md | 55 +++++++++++++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 workflows/examples/time_to_drink_tea.donna.md diff --git a/README.md b/README.md index 104900a8..662839be 100644 --- a/README.md +++ b/README.md @@ -104,20 +104,19 @@ The workflow is complete. You are a good butler.
How it works - -How this example works: +How to read workflow's source: - The H1 section is the workflow section. It gives Donna the workflow title and summary that appear in `donna list`. - Each H2 section is an operation section. Donna runs operations in the order selected by the workflow state machine. -- `toml donna` blocks configure section type and behavior. Here, the operation types are `donna.lib.run_script`, `donna.lib.request_action`, and `donna.lib.finish`. +- `toml donna` blocks configure section id, type and behavior. Here, the operation types are `donna.lib.run_script`, `donna.lib.request_action`, and `donna.lib.finish`. - `Get Current Time` is a `run_script` operation. Donna runs the shell script from the project root, saves stdout as `current_time`, and moves to `ask_about_tea` on success. No agent interaction required here, because it is pure deterministic work. -- `Ask About Tea` is a `request_action` operation. The agent will see the rendered current time, the question, and the two allowed transitions: `turn_on_kettle` or `finish`. -- `donna.lib.task_variable("current_time")` is rendered for the agent as the value captured from the script output. -- `donna.lib.goto(...)` is rendered for the agent as a concrete CLI command to exectute. +- `Ask About Tea` is a `request_action` operation. The agent will see a request for action with the rendered current time, the question, and the two allowed transitions: `turn_on_kettle` or `finish`. +- `donna.lib.task_variable("current_time")` is rendered as the value captured from the script output. +- `donna.lib.goto(...)` is rendered as a concrete CLI command to exectute. - `Turn On Kettle` is another `request_action` operation. The agent will see the instruction to turn on the kettle and then complete the action request with the `finish` transition. - `Finish` is a `finish` operation. The agent will see the final workflow message, and Donna will complete the workflow task. -For example, here what the agent will see on the `Get Current Time` step of the tea workflow: +Here is an example of action request for the `Get Current Time` operation: ````markdown --DONNA-CELL OuP2T9brQYmvESMHsbrDlw BEGIN-- @@ -133,6 +132,7 @@ The session is AWAITING YOUR ACTION. You have pending action requests to address or continue working on the current action requests. - Otherwise, you MUST address the pending action requests before proceeding. --DONNA-CELL OuP2T9brQYmvESMHsbrDlw END-- + --DONNA-CELL fxh3PDZ1Qy-c3zvai_T5Qw BEGIN-- kind=action_request media_type=text/markdown @@ -157,6 +157,18 @@ Is it time to drink tea?
+Since Donna is a CLI tool, you can run the workflow manually in the terminal. + +```bash +uv install donna + +# Example workflow is a part of Donna repository +# git clone git@github.com:Tiendil/donna.git +# cd donna + +donna run @/workflows/tea.donna.md +``` + ## Rationale Many project workflows are partly deterministic and partly judgment-based. Running formatters, validators, tests, or small scripts is deterministic. Deciding how to fix a failed check, adjust a design, or judge whether a result satisfies a requirement needs an agent. diff --git a/workflows/examples/time_to_drink_tea.donna.md b/workflows/examples/time_to_drink_tea.donna.md new file mode 100644 index 00000000..cad53bf2 --- /dev/null +++ b/workflows/examples/time_to_drink_tea.donna.md @@ -0,0 +1,55 @@ +# Is it time to drink tea? + +This workflow checks the current time, asks the agent whether it is tea time, +and branches on the answer. + +## Get Current Time + +```toml donna +id = "get_current_time" +kind = "donna.lib.run_script" +save_stdout_to = "current_time" +goto_on_success = "ask_about_tea" +goto_on_failure = "finish" +``` + +```bash donna script +#!/usr/bin/env bash +date +%H:%M +``` + +## Ask About Tea + +```toml donna +id = "ask_about_tea" +kind = "donna.lib.request_action" +``` + +The current time is: + +```text +{{ donna.lib.task_variable("current_time") }} +``` + +Is it time to drink tea? + +1. If yes, `{{ donna.lib.goto("turn_on_kettle") }}`. +1. If no, `{{ donna.lib.goto("finish") }}`. + +## Turn On Kettle + +```toml donna +id = "turn_on_kettle" +kind = "donna.lib.request_action" +``` + +Turn on the kettle, then `{{ donna.lib.goto("finish") }}`. + +## Finish + +```toml donna +id = "finish" +kind = "donna.lib.finish" +``` + +The workflow is complete. Report the result to the developer. From 4001c6744a3cb1587a0b1a204256e7876822174e Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 19:48:11 +0200 Subject: [PATCH 75/98] wip --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 662839be..d3027312 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ uv install donna # git clone git@github.com:Tiendil/donna.git # cd donna -donna run @/workflows/tea.donna.md +donna run @/workflows/examples/time_to_drink_tea.donna.md ``` ## Rationale From 36f9ba943f3f372bb8211b1a2f16fba5ddd3bc9d Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 20:01:45 +0200 Subject: [PATCH 76/98] wip --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d3027312..ea624602 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,8 @@ uv install donna # cd donna donna run @/workflows/examples/time_to_drink_tea.donna.md + +# Follow Donna commands to finish the workflow. ``` ## Rationale From 5627a3d2aca7e946c5b0ff52b63c2a1a85b86d62 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 20:06:51 +0200 Subject: [PATCH 77/98] wip --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ea624602..f35a6c70 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ You define a workflow in a single readable Markdown file, after that your agent For example, you can have a workflow that guides the agent through the planing process, and at the final step, the agent can generate a new workflow with a detailed plan to execute and run it immediately. +As a bonus, **Donna saves tokens** because agent doesn't need to reason about the control flow or how to execute particular CLI commands and other automation tools. + ## Example I use Donna to develop Donna itself — you can find real examples of workflows in the [./workflows](./workflows) folder of this repository. You can start with [./workflows/polish.donna.md](./workflows/polish.donna.md) that goes in loop over fixing issues found by formatters, linters, type checkers and tests until the codebase is polished. From 362967583a668a32dfebd1b0a75638ab388e5cb1 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 20:12:17 +0200 Subject: [PATCH 78/98] wip --- README.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f35a6c70..0a4d2df1 100644 --- a/README.md +++ b/README.md @@ -175,16 +175,12 @@ donna run @/workflows/examples/time_to_drink_tea.donna.md ## Rationale -Many project workflows are partly deterministic and partly judgment-based. Running formatters, validators, tests, or small scripts is deterministic. Deciding how to fix a failed check, adjust a design, or judge whether a result satisfies a requirement needs an agent. +1. Most of development work is repetitive on the meta level: "run this tool, do something with the output, run another tool" or "implement function A, implement tests for function A, implement function B, ...". +2. Some parts of that work require advanced reasoning, others — not really. +3. Agents are ~~almost~~ good at reasoning, but not so good at keeping the whole process in mind, remembering what they did, etc. +4. Therefore, it looks like a good idea to separate the reasoning part from the control flow part; let agents focus on what they are good at, and keep the control flow to the classic automation tools. -Donna separates those concerns without requiring an API key, a hosted service, or a separate orchestrator: - -- workflow files define the allowed control flow. -- Donna runs deterministic operations and preserves session state. -- action requests pause execution and ask the agent to do the non-deterministic work. -- the agent completes each request by choosing one of the workflow's declared next operations. - -This keeps long-running work explicit, resumable, and easier to validate, while leaving judgment and file edits with the agent. +Donna does exactly that. ## Features From 72459e8d72667b7b54e7af03753431021deb5076 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 20:12:46 +0200 Subject: [PATCH 79/98] wip --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0a4d2df1..c7eac651 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,15 @@ For example, you can have a workflow that guides the agent through the planing p As a bonus, **Donna saves tokens** because agent doesn't need to reason about the control flow or how to execute particular CLI commands and other automation tools. +## Rationale + +1. Most of development work is repetitive on the meta level: "run this tool, do something with the output, run another tool" or "implement function A, implement tests for function A, implement function B, ...". +2. Some parts of that work require advanced reasoning, others — not really. +3. Agents are ~~almost~~ good at reasoning, but not so good at keeping the whole process in mind, remembering what they did, etc. +4. Therefore, it looks like a good idea to separate the reasoning part from the control flow part; let agents focus on what they are good at, and keep the control flow to the classic automation tools. + +Donna does exactly that. + ## Example I use Donna to develop Donna itself — you can find real examples of workflows in the [./workflows](./workflows) folder of this repository. You can start with [./workflows/polish.donna.md](./workflows/polish.donna.md) that goes in loop over fixing issues found by formatters, linters, type checkers and tests until the codebase is polished. @@ -173,15 +182,6 @@ donna run @/workflows/examples/time_to_drink_tea.donna.md # Follow Donna commands to finish the workflow. ``` -## Rationale - -1. Most of development work is repetitive on the meta level: "run this tool, do something with the output, run another tool" or "implement function A, implement tests for function A, implement function B, ...". -2. Some parts of that work require advanced reasoning, others — not really. -3. Agents are ~~almost~~ good at reasoning, but not so good at keeping the whole process in mind, remembering what they did, etc. -4. Therefore, it looks like a good idea to separate the reasoning part from the control flow part; let agents focus on what they are good at, and keep the control flow to the classic automation tools. - -Donna does exactly that. - ## Features - CLI workflow runner for coding agents and humans supervising them. From a71b63f70bb81a8af7932a31fe48df1a76527873 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 20:19:57 +0200 Subject: [PATCH 80/98] wip --- README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c7eac651..d8d3b231 100644 --- a/README.md +++ b/README.md @@ -184,16 +184,11 @@ donna run @/workflows/examples/time_to_drink_tea.donna.md ## Features -- CLI workflow runner for coding agents and humans supervising them. -- Project-local configuration with `donna.toml`. -- Markdown workflow artifacts ending with `.donna.md`. -- Session state stored under the configured `session_dir`. -- Workflow discovery from configured `workflow_dirs`. -- Built-in operation kinds for requesting agent action, running shell scripts, printing output, and finishing workflow tasks. -- Artifact listing, rendering, and validation commands. -- Human, LLM, and JSON Lines automation output protocols. -- Built-in skill-style documentation through `donna skill ...`. -- Optional journal forwarding through `donna.toml`. +- Pure CLI tool — no API keys required, no external dependencies, no network requests. +- Built-in help for agents, just ask your agent to run `donna skill` and it will find all required information. +- Workflows as a single human-friendly Markdown file with a clear structure. +- Virtual Maching for your agents: run workflows that were generated by workflows that started from workflows your agent runs. +- Configurable journal command to log execution progress via third-party tools. ## Quick Usage From bcd297358d93c8b347a196a95767cdc9c171c42d Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 20:24:02 +0200 Subject: [PATCH 81/98] wip --- README.md | 51 +++++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index d8d3b231..ddd1ebba 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,33 @@ donna run @/workflows/examples/time_to_drink_tea.donna.md - Virtual Maching for your agents: run workflows that were generated by workflows that started from workflows your agent runs. - Configurable journal command to log execution progress via third-party tools. +## Installation + +```bash +uv tool install donna + +# or +# python -m pip install +# poetry add donna +``` + +Ask your agent to initialize donna in the project: + +``` +1. Run `donna skill` +2. Initialize Donna in this project. +3. Add instructions on when and how to use Donna to the AGENTS.md. +``` + +Or install Donna manually: + +```bash +# from the root of your project +donna init +``` + +`donna init` will create a configuration file `donna.toml`, review it and edit if needed. + ## Quick Usage Create a starter configuration: @@ -261,30 +288,6 @@ For coding agents, put global options before the subcommand: donna -p llm --config /path/to/project/donna.toml status ``` -## Installation - -Install Donna from PyPI with `uv`: - -```bash -uv tool install donna -``` - -Install Donna from PyPI with `pip`: - -```bash -python -m pip install donna -``` - -Donna requires Python 3.12 or newer. - -After installation, initialize a project from the directory that should contain `donna.toml`: - -```bash -donna init -``` - -Review the generated configuration before relying on it. The starter file is intentionally small and may need project-specific workflow directories or journal forwarding. - ## Configuration A Donna project is configured by a TOML file named `donna.toml`. If `--config` is omitted, Donna searches upward from the current working directory until it finds `donna.toml`; the directory containing that file is the project root. From d7bcbab1704897b5c8a7026faf1144baa2f4d8c8 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 20:28:19 +0200 Subject: [PATCH 82/98] wip --- README.md | 101 +++++++++++++++++++++--------------------------------- 1 file changed, 40 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index ddd1ebba..700c782f 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,43 @@ donna init `donna init` will create a configuration file `donna.toml`, review it and edit if needed. +## Configuration + +A Donna project is configured by a TOML file named `donna.toml`. If `--config` is omitted, Donna searches upward from the current working directory until it finds `donna.toml`; the directory containing that file is the project root. + +Minimal configuration: + +```toml +version = 1 +``` + +The starter configuration generated by `donna init` includes the default session directory and workflow discovery directories: + +```toml +version = 1 + +session_dir = ".session/donna" + +workflow_dirs = [ + "./workflows", + "./.session/donna", +] +``` + +A coding agent can help create or adapt workflow files after the project layout and desired workflows are clear. + +The session directory stores Donna runtime state. Workflow directories are scanned recursively for `.donna.md` files; missing workflow directories are ignored. + +Detailed configuration behavior is specified in [specs/behavior/config.md](./specs/behavior/config.md). Command behavior is specified in [specs/behavior/cli.md](./specs/behavior/cli.md). + +Project agent instructions can include a short Donna rule like this: + +```markdown +Use Donna only when explicitly instructed by a developer, by project instructions, or by Donna itself. +Before using Donna in a session, read `donna -p llm skill usage`. +Use `donna -p llm ...` for agent-facing command output. +``` + ## Quick Usage Create a starter configuration: @@ -231,16 +268,10 @@ List discovered workflow artifacts: donna list ``` -Validate every discovered workflow artifact: +Start new session: ```bash -donna validate --all -``` - -Render a workflow as Donna sees it: - -```bash -donna render @/workflows/polish.donna.md --mode view +donna new-session ``` Start a workflow in the current session: @@ -264,7 +295,7 @@ donna continue Complete an action request with the id and next operation printed by Donna: ```bash -donna complete-action-request AR-12-x @/workflows/example.donna.md:next_operation +donna complete-action-request @/workflows/example.donna.md:next_operation ``` Read the built-in usage documentation: @@ -273,58 +304,6 @@ Read the built-in usage documentation: donna skill usage ``` -Useful related commands: - -```bash -donna status -donna details -donna new-session -donna version -``` - -For coding agents, put global options before the subcommand: - -```bash -donna -p llm --config /path/to/project/donna.toml status -``` - -## Configuration - -A Donna project is configured by a TOML file named `donna.toml`. If `--config` is omitted, Donna searches upward from the current working directory until it finds `donna.toml`; the directory containing that file is the project root. - -Minimal configuration: - -```toml -version = 1 -``` - -The starter configuration generated by `donna init` includes the default session directory and workflow discovery directories: - -```toml -version = 1 - -session_dir = ".session/donna" - -workflow_dirs = [ - "./workflows", - "./.session/donna", -] -``` - -Review and edit the starter configuration for the project. A coding agent can help create or adapt workflow files after the project layout and desired workflows are clear. - -The session directory stores Donna runtime state. Workflow directories are scanned recursively for `.donna.md` files; missing workflow directories are ignored. - -Detailed configuration behavior is specified in [specs/behavior/config.md](./specs/behavior/config.md). Command behavior is specified in [specs/behavior/cli.md](./specs/behavior/cli.md). - -Project agent instructions can include a short Donna rule like this: - -```markdown -Use Donna only when explicitly instructed by a developer, by project instructions, or by Donna itself. -Before using Donna in a session, read `donna -p llm skill usage`. -Use `donna -p llm ...` for agent-facing command output. -``` - ## Workflow Files Donna workflow artifacts are Markdown files ending with `.donna.md`. Donna discovers them by recursively scanning the configured `workflow_dirs`. From ea9349642bb629233662a433f579e54f29ecbd02 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 20:29:14 +0200 Subject: [PATCH 83/98] wip --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 700c782f..e4ede53c 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,8 @@ Use `donna -p llm ...` for agent-facing command output. ## Quick Usage +Detailed CLI interface is described in [specs/behavior/cli.md](./specs/behavior/cli.md). + Create a starter configuration: ```bash From bed95d1fe9f05728cd6a93d2dfd5e92451343689 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 20:34:51 +0200 Subject: [PATCH 84/98] wip --- README.md | 44 ++++++++++++++------------------------------ 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index e4ede53c..ad3fa196 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,8 @@ donna init ## Configuration +You can get a detailed documentation by running `donna skill configuration` or asking your agent to do that. + A Donna project is configured by a TOML file named `donna.toml`. If `--config` is omitted, Donna searches upward from the current working directory until it finds `donna.toml`; the directory containing that file is the project root. Minimal configuration: @@ -256,6 +258,10 @@ Use `donna -p llm ...` for agent-facing command output. ## Quick Usage +**In most cases your agent should be capable of using Donna by itself without your intervention.** + +You can get a detailed documentation by running `donna skill usage` or asking your agent to do that. + Detailed CLI interface is described in [specs/behavior/cli.md](./specs/behavior/cli.md). Create a starter configuration: @@ -308,6 +314,10 @@ donna skill usage ## Workflow Files +**In most cases your agent should be capable of creating and managing workflows by itself without your intervention.** + +You can get a detailed documentation by running `donna skill workflows` or asking your agent to do that. + Donna workflow artifacts are Markdown files ending with `.donna.md`. Donna discovers them by recursively scanning the configured `workflow_dirs`. A workflow file has one H1 section for the workflow and H2 sections for operations. Section config is written in fenced `toml donna` code blocks. Transitions are declared in operation config or request text, depending on the operation kind, and validated before execution. @@ -319,40 +329,10 @@ Common built-in operation kinds: - `donna.lib.output` prints information and continues. - `donna.lib.finish` finishes the workflow task. -Artifact ids are project-root anchored paths such as: - -```text -@/workflows/polish.donna.md -``` - -Artifact section ids append a section id: - -```text -@/workflows/polish.donna.md:finish -``` - -Read the detailed workflow documentation with: - -```bash -donna skill workflows -``` - -The built-in skill documentation set is specified in [specs/behavior/skill_fixtures.md](./specs/behavior/skill_fixtures.md). - ## Specifications Project behavior and architecture are specified in [specs/](./specs/). Start with [specs/intro.md](./specs/intro.md) for the index. -Important references: - -- [specs/behavior/cli.md](./specs/behavior/cli.md) describes CLI commands, output protocols, and command behavior. -- [specs/behavior/config.md](./specs/behavior/config.md) describes `donna.toml`. -- [specs/behavior/file_paths.md](./specs/behavior/file_paths.md) describes project paths, artifact ids, and artifact section ids. -- [specs/behavior/skill_fixtures.md](./specs/behavior/skill_fixtures.md) describes built-in documentation fixtures. -- [specs/documentation/readme.md](./specs/documentation/readme.md) describes this README's required structure and content. - -The root README is an introduction, not the source of truth for detailed behavior. - ## Development Development commands are run through Docker-backed project scripts. @@ -376,3 +356,7 @@ Run Donna from the current development checkout: ``` The repository also uses Donna itself. Its local workflows live in [workflows/](./workflows/), and the current project configuration is [donna.toml](./donna.toml). + +### Agent's harness + +Check [AGENTS.md](./AGENTS.md) for the list of additional tools that agents will expect to be installed. From 4c877fad3e86401893735c4f5da9401dd6e6aed2 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 20:46:07 +0200 Subject: [PATCH 85/98] wip --- README.md | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index ad3fa196..54cbd5b1 100644 --- a/README.md +++ b/README.md @@ -2,30 +2,24 @@ **A CLI tool that helps agents keep long-running work on a predefined path.** -Donna allows agents to execute deterministic workflows regardless of the complexity of the algorithm. +Donna exists because agent work has a control-flow problem: -You can look at Donna as a co-processor for your agent that takes care of the control flow, so the agent can focus on the actual agentic work such as reasoning, code generation, etc. - -You define a workflow in a single readable Markdown file, after that your agent may ask Donna to guide it through the workflow, and Donna will take care of the rest by given instructions to the agent on what to do at each step. - -**Workflow is a state machine and Donna its interpreter.** That means agent can start child workflows from the parent workflow, write workflows on the fly, modify them while executing, etc. - -For example, you can have a workflow that guides the agent through the planing process, and at the final step, the agent can generate a new workflow with a detailed plan to execute and run it immediately. +1. Most of development work is repetitive on the meta level: "run this tool, do something with the output, run another tool" or "implement function A, implement tests for function A, implement function B, …". +2. Some parts of that work require advanced reasoning, others — not really. +3. Agents are ~~almost~~ good at reasoning, but not so good at keeping the whole process in mind, remembering what they did, etc. +4. Therefore, we should separate the reasoning part from the control flow part — let agents focus on what they are good at, and keep the control flow to the classic automation tools. -As a bonus, **Donna saves tokens** because agent doesn't need to reason about the control flow or how to execute particular CLI commands and other automation tools. +Donna does exactly that: it runs predefined workflows as deterministic state-machines while the agent focuses on reasoning, code generation, and other agentic work. -## Rationale +You define a workflow in a single readable Markdown file. The agent asks Donna to guide it through the workflow, and Donna keeps the session state, chooses the next operation, and tells the agent what to do or report next. -1. Most of development work is repetitive on the meta level: "run this tool, do something with the output, run another tool" or "implement function A, implement tests for function A, implement function B, ...". -2. Some parts of that work require advanced reasoning, others — not really. -3. Agents are ~~almost~~ good at reasoning, but not so good at keeping the whole process in mind, remembering what they did, etc. -4. Therefore, it looks like a good idea to separate the reasoning part from the control flow part; let agents focus on what they are good at, and keep the control flow to the classic automation tools. +Workflows can start child workflows, be generated on the fly, or be modified while executing. For example, you can have a workflow that guides the agent through the planning process, and at the final step, the agent can generate a new workflow with a detailed plan to execute and run it immediately. -Donna does exactly that. +As a bonus, **Donna saves tokens** because the agent does not need to reason about control flow or how to execute particular CLI commands and other automation tools. ## Example -I use Donna to develop Donna itself — you can find real examples of workflows in the [./workflows](./workflows) folder of this repository. You can start with [./workflows/polish.donna.md](./workflows/polish.donna.md) that goes in loop over fixing issues found by formatters, linters, type checkers and tests until the codebase is polished. +I use Donna to develop Donna itself — you can find real examples of workflows in the [./workflows](./workflows) folder of this repository. You can start with [./workflows/polish.donna.md](./workflows/polish.donna.md) that loops over fixing issues found by formatters, linters, type checkers and tests until the codebase is polished. Below you'll find a simplified workflow that checks the current time, asks the agent whether it is time to drink tea, and branches according to the agent's answer. @@ -123,7 +117,7 @@ How to read workflow's source: - `Get Current Time` is a `run_script` operation. Donna runs the shell script from the project root, saves stdout as `current_time`, and moves to `ask_about_tea` on success. No agent interaction required here, because it is pure deterministic work. - `Ask About Tea` is a `request_action` operation. The agent will see a request for action with the rendered current time, the question, and the two allowed transitions: `turn_on_kettle` or `finish`. - `donna.lib.task_variable("current_time")` is rendered as the value captured from the script output. -- `donna.lib.goto(...)` is rendered as a concrete CLI command to exectute. +- `donna.lib.goto(...)` is rendered as a concrete CLI command to execute. - `Turn On Kettle` is another `request_action` operation. The agent will see the instruction to turn on the kettle and then complete the action request with the `finish` transition. - `Finish` is a `finish` operation. The agent will see the final workflow message, and Donna will complete the workflow task. @@ -187,7 +181,7 @@ donna run @/workflows/examples/time_to_drink_tea.donna.md - Pure CLI tool — no API keys required, no external dependencies, no network requests. - Built-in help for agents, just ask your agent to run `donna skill` and it will find all required information. - Workflows as a single human-friendly Markdown file with a clear structure. -- Virtual Maching for your agents: run workflows that were generated by workflows that started from workflows your agent runs. +- Virtual Machine for your agents: run workflows that were generated by workflows that started from workflows your agent runs. - Configurable journal command to log execution progress via third-party tools. ## Installation From 88529b070f4bcea52f9ba47b6b009d307e1dbda3 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 20:48:20 +0200 Subject: [PATCH 86/98] spelling --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 54cbd5b1..61b0f4f9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Donna exists because agent work has a control-flow problem: 3. Agents are ~~almost~~ good at reasoning, but not so good at keeping the whole process in mind, remembering what they did, etc. 4. Therefore, we should separate the reasoning part from the control flow part — let agents focus on what they are good at, and keep the control flow to the classic automation tools. -Donna does exactly that: it runs predefined workflows as deterministic state-machines while the agent focuses on reasoning, code generation, and other agentic work. +Donna does exactly that: it runs predefined workflows as deterministic state machines while the agent focuses on reasoning, code generation, and other agentic work. You define a workflow in a single readable Markdown file. The agent asks Donna to guide it through the workflow, and Donna keeps the session state, chooses the next operation, and tells the agent what to do or report next. @@ -213,7 +213,7 @@ donna init ## Configuration -You can get a detailed documentation by running `donna skill configuration` or asking your agent to do that. +You can get detailed documentation by running `donna skill configuration` or asking your agent to do that. A Donna project is configured by a TOML file named `donna.toml`. If `--config` is omitted, Donna searches upward from the current working directory until it finds `donna.toml`; the directory containing that file is the project root. @@ -254,7 +254,7 @@ Use `donna -p llm ...` for agent-facing command output. **In most cases your agent should be capable of using Donna by itself without your intervention.** -You can get a detailed documentation by running `donna skill usage` or asking your agent to do that. +You can get detailed documentation by running `donna skill usage` or asking your agent to do that. Detailed CLI interface is described in [specs/behavior/cli.md](./specs/behavior/cli.md). @@ -310,7 +310,7 @@ donna skill usage **In most cases your agent should be capable of creating and managing workflows by itself without your intervention.** -You can get a detailed documentation by running `donna skill workflows` or asking your agent to do that. +You can get detailed documentation by running `donna skill workflows` or asking your agent to do that. Donna workflow artifacts are Markdown files ending with `.donna.md`. Donna discovers them by recursively scanning the configured `workflow_dirs`. From 6b7f6e403e83251c84da06b75d19ea2ee1288f9a Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 21:11:35 +0200 Subject: [PATCH 87/98] features --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 61b0f4f9..d09a57d3 100644 --- a/README.md +++ b/README.md @@ -178,11 +178,14 @@ donna run @/workflows/examples/time_to_drink_tea.donna.md ## Features -- Pure CLI tool — no API keys required, no external dependencies, no network requests. -- Built-in help for agents, just ask your agent to run `donna skill` and it will find all required information. -- Workflows as a single human-friendly Markdown file with a clear structure. -- Virtual Machine for your agents: run workflows that were generated by workflows that started from workflows your agent runs. -- Configurable journal command to log execution progress via third-party tools. +- **Pure CLI tool** — No API keys, hosted services, or separate agent instances required. +- **Built-in help for agents** — Your agent can run `donna skill` to get detailed agent-focused docs. +- **Readable workflow sources** — Each workflow is a single Markdown file with a clear structure. +- **Deterministic control flow** — Donna follows explicit workflow transitions instead of relying on the agent's memory. +- **Agent-aware automation** — Scripted steps run automatically; agent work is requested when needed. +- **Local session state** — Workflow progress stays inspectable and resumable inside the project. +- **Nested workflows** — Workflows can start child workflows, generate new ones, or delegate workflow selection to the agent. +- **Progress journaling** — Workflow progress can be logged through a configured external command. ## Installation From 4a560eaa57750d9026957867ada8a19c69ca3246 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 21:13:36 +0200 Subject: [PATCH 88/98] features --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d09a57d3..e4cbbeb9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,17 @@ Workflows can start child workflows, be generated on the fly, or be modified whi As a bonus, **Donna saves tokens** because the agent does not need to reason about control flow or how to execute particular CLI commands and other automation tools. +## Features + +- **Pure CLI tool** — No API keys, hosted services, or separate agent instances required. +- **Built-in help for agents** — Your agent can run `donna skill` to get detailed agent-focused docs. +- **Readable workflow sources** — Each workflow is a single Markdown file with a clear structure. +- **Deterministic control flow** — Donna follows explicit workflow transitions instead of relying on the agent's memory. +- **Agent-aware automation** — Scripted steps run automatically; agent work is requested when needed. +- **Local session state** — Workflow progress stays inspectable and resumable inside the project. +- **Nested workflows** — Workflows can start child workflows, generate new ones, or delegate workflow selection to the agent. +- **Progress journaling** — Workflow progress can be logged through a configured external command. + ## Example I use Donna to develop Donna itself — you can find real examples of workflows in the [./workflows](./workflows) folder of this repository. You can start with [./workflows/polish.donna.md](./workflows/polish.donna.md) that loops over fixing issues found by formatters, linters, type checkers and tests until the codebase is polished. @@ -176,17 +187,6 @@ donna run @/workflows/examples/time_to_drink_tea.donna.md # Follow Donna commands to finish the workflow. ``` -## Features - -- **Pure CLI tool** — No API keys, hosted services, or separate agent instances required. -- **Built-in help for agents** — Your agent can run `donna skill` to get detailed agent-focused docs. -- **Readable workflow sources** — Each workflow is a single Markdown file with a clear structure. -- **Deterministic control flow** — Donna follows explicit workflow transitions instead of relying on the agent's memory. -- **Agent-aware automation** — Scripted steps run automatically; agent work is requested when needed. -- **Local session state** — Workflow progress stays inspectable and resumable inside the project. -- **Nested workflows** — Workflows can start child workflows, generate new ones, or delegate workflow selection to the agent. -- **Progress journaling** — Workflow progress can be logged through a configured external command. - ## Installation ```bash From de4c7bbb7060cec20d72581d8ed2662d757f0867 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 21:20:59 +0200 Subject: [PATCH 89/98] wip --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e4cbbeb9..6d3494df 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Donna exists because agent work has a control-flow problem: -1. Most of development work is repetitive on the meta level: "run this tool, do something with the output, run another tool" or "implement function A, implement tests for function A, implement function B, …". -2. Some parts of that work require advanced reasoning, others — not really. +1. Most development work is repetitive on the meta level: "run this tool, do something with the output, run another tool" or "implement function A, implement tests for function A, implement function B, …". +2. Some parts of that work require advanced reasoning, others do not. 3. Agents are ~~almost~~ good at reasoning, but not so good at keeping the whole process in mind, remembering what they did, etc. 4. Therefore, we should separate the reasoning part from the control flow part — let agents focus on what they are good at, and keep the control flow to the classic automation tools. @@ -20,7 +20,7 @@ As a bonus, **Donna saves tokens** because the agent does not need to reason abo ## Features - **Pure CLI tool** — No API keys, hosted services, or separate agent instances required. -- **Built-in help for agents** — Your agent can run `donna skill` to get detailed agent-focused docs. +- **Built-in help for agents** — Your agent can run `donna skill` to get detailed docs written for agents. - **Readable workflow sources** — Each workflow is a single Markdown file with a clear structure. - **Deterministic control flow** — Donna follows explicit workflow transitions instead of relying on the agent's memory. - **Agent-aware automation** — Scripted steps run automatically; agent work is requested when needed. @@ -176,7 +176,7 @@ Is it time to drink tea? Since Donna is a CLI tool, you can run the workflow manually in the terminal. ```bash -uv install donna +uv tool install donna # Example workflow is a part of Donna repository # git clone git@github.com:Tiendil/donna.git @@ -197,12 +197,12 @@ uv tool install donna # poetry add donna ``` -Ask your agent to initialize donna in the project: +Ask your agent to initialize Donna in the project: ``` 1. Run `donna skill` 2. Initialize Donna in this project. -3. Add instructions on when and how to use Donna to the AGENTS.md. +3. Add instructions on when and how to use Donna to the AGENTS.md file. ``` Or install Donna manually: @@ -243,7 +243,7 @@ A coding agent can help create or adapt workflow files after the project layout The session directory stores Donna runtime state. Workflow directories are scanned recursively for `.donna.md` files; missing workflow directories are ignored. -Detailed configuration behavior is specified in [specs/behavior/config.md](./specs/behavior/config.md). Command behavior is specified in [specs/behavior/cli.md](./specs/behavior/cli.md). +Detailed configuration behavior is specified in [./specs/behavior/config.md](./specs/behavior/config.md). Command behavior is specified in [./specs/behavior/cli.md](./specs/behavior/cli.md). Project agent instructions can include a short Donna rule like this: @@ -259,7 +259,7 @@ Use `donna -p llm ...` for agent-facing command output. You can get detailed documentation by running `donna skill usage` or asking your agent to do that. -Detailed CLI interface is described in [specs/behavior/cli.md](./specs/behavior/cli.md). +Detailed CLI interface is described in [./specs/behavior/cli.md](./specs/behavior/cli.md). Create a starter configuration: @@ -273,7 +273,7 @@ List discovered workflow artifacts: donna list ``` -Start new session: +Start a new session: ```bash donna new-session @@ -328,7 +328,7 @@ Common built-in operation kinds: ## Specifications -Project behavior and architecture are specified in [specs/](./specs/). Start with [specs/intro.md](./specs/intro.md) for the index. +Project behavior and architecture are specified in [./specs/](./specs/). Start with [./specs/intro.md](./specs/intro.md) for the index. ## Development @@ -352,8 +352,8 @@ Run Donna from the current development checkout: ./bin/dev.sh poetry run donna --help ``` -The repository also uses Donna itself. Its local workflows live in [workflows/](./workflows/), and the current project configuration is [donna.toml](./donna.toml). +The repository also uses Donna itself. Its local workflows live in [./workflows/](./workflows/), and the current project configuration is [./donna.toml](./donna.toml). ### Agent's harness -Check [AGENTS.md](./AGENTS.md) for the list of additional tools that agents will expect to be installed. +Check [./AGENTS.md](./AGENTS.md) for the list of additional tools that agents will expect to be installed. From f9c59b5372f1945e6b1fbe0567a0f449fc98b808 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 21:22:25 +0200 Subject: [PATCH 90/98] wip --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6d3494df..e4e87513 100644 --- a/README.md +++ b/README.md @@ -328,7 +328,7 @@ Common built-in operation kinds: ## Specifications -Project behavior and architecture are specified in [./specs/](./specs/). Start with [./specs/intro.md](./specs/intro.md) for the index. +Project behavior and architecture are specified in [./specs](./specs/). Start with [./specs/intro.md](./specs/intro.md) for the index. ## Development @@ -352,8 +352,8 @@ Run Donna from the current development checkout: ./bin/dev.sh poetry run donna --help ``` -The repository also uses Donna itself. Its local workflows live in [./workflows/](./workflows/), and the current project configuration is [./donna.toml](./donna.toml). +The repository also uses Donna itself. Its local workflows live in [./workflows](./workflows/), and the current project configuration is [donna.toml](./donna.toml). ### Agent's harness -Check [./AGENTS.md](./AGENTS.md) for the list of additional tools that agents will expect to be installed. +Check [AGENTS.md](./AGENTS.md) for the list of additional tools that agents will expect to be installed. From 374cadc15bfc7ef25682a163d7b1017f5a637275 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 21:25:43 +0200 Subject: [PATCH 91/98] wip --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e4e87513..3994b529 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Donna exists because agent work has a control-flow problem: 1. Most development work is repetitive on the meta level: "run this tool, do something with the output, run another tool" or "implement function A, implement tests for function A, implement function B, …". 2. Some parts of that work require advanced reasoning, others do not. 3. Agents are ~~almost~~ good at reasoning, but not so good at keeping the whole process in mind, remembering what they did, etc. -4. Therefore, we should separate the reasoning part from the control flow part — let agents focus on what they are good at, and keep the control flow to the classic automation tools. +4. Therefore, we should separate the reasoning part from the control flow part — let agents focus on what they are good at, and keep the control flow to traditional automation tools. Donna does exactly that: it runs predefined workflows as deterministic state machines while the agent focuses on reasoning, code generation, and other agentic work. @@ -193,8 +193,7 @@ donna run @/workflows/examples/time_to_drink_tea.donna.md uv tool install donna # or -# python -m pip install -# poetry add donna +# python -m pip install donna ``` Ask your agent to initialize Donna in the project: @@ -354,6 +353,6 @@ Run Donna from the current development checkout: The repository also uses Donna itself. Its local workflows live in [./workflows](./workflows/), and the current project configuration is [donna.toml](./donna.toml). -### Agent's harness +### Agent Harness Check [AGENTS.md](./AGENTS.md) for the list of additional tools that agents will expect to be installed. From 18be7a685f16dbb5802a2cfc6b1e097ca40eb627 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 21:27:57 +0200 Subject: [PATCH 92/98] wip --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3994b529..fae25b81 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Donna exists because agent work has a control-flow problem: 3. Agents are ~~almost~~ good at reasoning, but not so good at keeping the whole process in mind, remembering what they did, etc. 4. Therefore, we should separate the reasoning part from the control flow part — let agents focus on what they are good at, and keep the control flow to traditional automation tools. -Donna does exactly that: it runs predefined workflows as deterministic state machines while the agent focuses on reasoning, code generation, and other agentic work. +**Donna does exactly that: it runs predefined workflows as deterministic state machines while the agent focuses on reasoning, code generation, and other agentic work.** You define a workflow in a single readable Markdown file. The agent asks Donna to guide it through the workflow, and Donna keeps the session state, chooses the next operation, and tells the agent what to do or report next. @@ -20,12 +20,12 @@ As a bonus, **Donna saves tokens** because the agent does not need to reason abo ## Features - **Pure CLI tool** — No API keys, hosted services, or separate agent instances required. -- **Built-in help for agents** — Your agent can run `donna skill` to get detailed docs written for agents. -- **Readable workflow sources** — Each workflow is a single Markdown file with a clear structure. - **Deterministic control flow** — Donna follows explicit workflow transitions instead of relying on the agent's memory. - **Agent-aware automation** — Scripted steps run automatically; agent work is requested when needed. -- **Local session state** — Workflow progress stays inspectable and resumable inside the project. - **Nested workflows** — Workflows can start child workflows, generate new ones, or delegate workflow selection to the agent. +- **Readable workflow sources** — Each workflow is a single Markdown file with a clear structure. +- **Built-in help for agents** — Your agent can run `donna skill` to get detailed docs written for agents. +- **Local session state** — Workflow progress stays inspectable and resumable inside the project. - **Progress journaling** — Workflow progress can be logged through a configured external command. ## Example From c2e699feeecd249951e917d252822da383190969 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 21:28:56 +0200 Subject: [PATCH 93/98] wip --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fae25b81..78b63376 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Donna exists because agent work has a control-flow problem: 3. Agents are ~~almost~~ good at reasoning, but not so good at keeping the whole process in mind, remembering what they did, etc. 4. Therefore, we should separate the reasoning part from the control flow part — let agents focus on what they are good at, and keep the control flow to traditional automation tools. -**Donna does exactly that: it runs predefined workflows as deterministic state machines while the agent focuses on reasoning, code generation, and other agentic work.** +**Donna runs predefined workflows as deterministic state machines, so the agent can focus on reasoning, code generation, and other agentic work.** You define a workflow in a single readable Markdown file. The agent asks Donna to guide it through the workflow, and Donna keeps the session state, chooses the next operation, and tells the agent what to do or report next. From f073ad158f9a19cb2d307767e9082d142071505b Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 21:33:25 +0200 Subject: [PATCH 94/98] spec updated --- specs/documentation/readme.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/specs/documentation/readme.md b/specs/documentation/readme.md index 09f7f46b..ad1568df 100644 --- a/specs/documentation/readme.md +++ b/specs/documentation/readme.md @@ -48,17 +48,19 @@ The first paragraphs after the h1 heading SHOULD explain the core value proposit `README.md` MUST use the following h2 sections in this order: -1. `Example` -2. `Rationale` -3. `Features` -4. `Quick Usage` -5. `Installation` -6. `Configuration` -7. `Workflow Files` -8. `Specifications` -9. `Development` - -The `Example` section MUST be the first h2 section after the introductory paragraphs. +1. `Features` +2. `Example` +3. `Installation` +4. `Configuration` +5. `Quick Usage` +6. `Workflow Files` +7. `Specifications` +8. `Development` + +The `Features` section MUST be the first h2 section after the introductory paragraphs. + +The `Features` section MUST use compact bullet items with bold feature names followed by short descriptions. + Additional h2 sections MUST NOT be added without updating this specification first. Lower-level subsections MAY be added under the required h2 sections when they help human readers. @@ -84,7 +86,7 @@ Additional h2 sections MUST NOT be added without updating this specification fir `README.md` MUST link to the skill fixture specification or mention built-in skill documentation when describing detailed user documentation. -`README.md` MUST mention that this repository uses Donna and that its `donna.toml` and `workflows/` directory can be used as real project examples. +`README.md` MUST mention that this repository uses Donna and that its `donna.toml` and `./workflows` directory can be used as real project examples. `README.md` MUST explain how to create a starter configuration with `donna init`. @@ -100,9 +102,9 @@ Additional h2 sections MUST NOT be added without updating this specification fir - initializing a Donna project. - listing discovered workflow artifacts. -- validating workflow artifacts. -- rendering workflow artifacts. +- starting a new session. - starting a workflow. +- inspecting the current session. - continuing queued workflow execution. - completing an action request. - reading built-in skill-style documentation. From be4b95f10141a798d853d5263e2269ffb19024d7 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 22:09:11 +0200 Subject: [PATCH 95/98] poetry -> uv --- AGENTS.md | 2 +- README.md | 4 +- bin/dev-check-formatting.sh | 4 +- bin/dev-check-runtime.sh | 2 +- bin/dev-check-semantics.sh | 8 +- bin/dev-tests.sh | 2 +- bin/donna.sh | 2 +- bin/release-prepare.sh | 12 +- bin/tach-map-query.py | 2 +- docker/Dockerfile | 14 +- poetry.lock | 1610 ----------------------------------- pyproject.toml | 68 +- specs/architecture/tests.md | 4 +- uv.lock | 1165 +++++++++++++++++++++++++ workflows/polish.donna.md | 16 +- 15 files changed, 1241 insertions(+), 1674 deletions(-) delete mode 100644 poetry.lock create mode 100644 uv.lock diff --git a/AGENTS.md b/AGENTS.md index 1c255996..92759060 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,7 @@ Agents MUST NOT perform development-related operations directly on the host mach Allowed development commands: - `./bin/dev-tests.sh` — run all Python tests inside the container. -- `./bin/dev.sh` — run development utilities inside the container, for example `./bin/dev.sh poetry run pytest`. +- `./bin/dev.sh` — run development utilities inside the container, for example `./bin/dev.sh uv run pytest`. - `./bin/dev-build-containers.sh` — build base Docker images for development; use only after approved Docker or dependency changes. Searching, reading, and editing repository files MAY be done on the host machine. diff --git a/README.md b/README.md index 78b63376..b2bf0cb7 100644 --- a/README.md +++ b/README.md @@ -342,13 +342,13 @@ Run the full test suite: Run a specific development command inside the project container: ```bash -./bin/dev.sh poetry run pytest +./bin/dev.sh uv run pytest ``` Run Donna from the current development checkout: ```bash -./bin/dev.sh poetry run donna --help +./bin/dev.sh uv run donna --help ``` The repository also uses Donna itself. Its local workflows live in [./workflows](./workflows/), and the current project configuration is [donna.toml](./donna.toml). diff --git a/bin/dev-check-formatting.sh b/bin/dev-check-formatting.sh index bf1ac8ef..c8658192 100755 --- a/bin/dev-check-formatting.sh +++ b/bin/dev-check-formatting.sh @@ -4,8 +4,8 @@ set -e echo "run isort" -./bin/dev.sh poetry run isort --check-only ./donna +./bin/dev.sh uv run isort --check-only ./donna echo "run black" -./bin/dev.sh poetry run black --check ./donna +./bin/dev.sh uv run black --check ./donna diff --git a/bin/dev-check-runtime.sh b/bin/dev-check-runtime.sh index e67c91e8..7834c078 100755 --- a/bin/dev-check-runtime.sh +++ b/bin/dev-check-runtime.sh @@ -4,4 +4,4 @@ set -e echo "cli works" -./bin/dev.sh poetry run donna --help +./bin/dev.sh uv run donna --help diff --git a/bin/dev-check-semantics.sh b/bin/dev-check-semantics.sh index dac5b523..bf74b704 100755 --- a/bin/dev-check-semantics.sh +++ b/bin/dev-check-semantics.sh @@ -4,16 +4,16 @@ set -e echo "run tach" -./bin/dev.sh poetry run tach check +./bin/dev.sh uv run tach check echo "run autoflake" -./bin/dev.sh poetry run autoflake --check --quiet ./donna +./bin/dev.sh uv run autoflake --check --quiet ./donna echo "run flake8" -./bin/dev.sh poetry run flake8 ./donna +./bin/dev.sh uv run flake8 ./donna echo "run mypy" -./bin/dev.sh poetry run mypy --show-traceback ./donna +./bin/dev.sh uv run mypy --show-traceback ./donna diff --git a/bin/dev-tests.sh b/bin/dev-tests.sh index b377b1f6..f59b65c1 100755 --- a/bin/dev-tests.sh +++ b/bin/dev-tests.sh @@ -4,4 +4,4 @@ set -e echo "run tests" -./bin/dev.sh poetry run pytest donna -o cache_dir=/tmp/donna-pytest-cache +./bin/dev.sh uv run pytest donna -o cache_dir=/tmp/donna-pytest-cache diff --git a/bin/donna.sh b/bin/donna.sh index 927ab150..11b02fe1 100755 --- a/bin/donna.sh +++ b/bin/donna.sh @@ -5,4 +5,4 @@ # exec .venv-donna/bin/donna "$@" ROOT_DIR="$(cd "$(dirname "$0")/.."; pwd)" -poetry -P "$ROOT_DIR" run donna "$@" +uv --project "$ROOT_DIR" run donna "$@" diff --git a/bin/release-prepare.sh b/bin/release-prepare.sh index 423bc272..e95e7907 100755 --- a/bin/release-prepare.sh +++ b/bin/release-prepare.sh @@ -6,29 +6,29 @@ export BUMP_VERSION=$1 echo "Bumping version as $BUMP_VERSION" -export NEXT_VERSION=$(poetry version $BUMP_VERSION --short) +export NEXT_VERSION=$(uv version --bump $BUMP_VERSION --short) export NEXT_VERSION_TAG="release-$NEXT_VERSION" echo "Install dependencies" -poetry install +uv sync echo "Update change log" -poetry run changy version create $NEXT_VERSION +uv run changy version create $NEXT_VERSION echo "Generate changelog" -poetry run changy changelog create +uv run changy changelog create -export COMMIT_BODY=$(poetry run changy version show $NEXT_VERSION) +export COMMIT_BODY=$(uv run changy version show $NEXT_VERSION) echo "New version is $NEXT_VERSION" echo "New version tag $NEXT_VERSION_TAG" echo "Building Python package" -poetry build +uv build echo "Commit changes" diff --git a/bin/tach-map-query.py b/bin/tach-map-query.py index 22797757..5f5093f5 100644 --- a/bin/tach-map-query.py +++ b/bin/tach-map-query.py @@ -22,7 +22,7 @@ def main() -> int: completed = subprocess.run( [ "./bin/dev.sh", - "poetry", + "uv", "run", "python", "-m", diff --git a/docker/Dockerfile b/docker/Dockerfile index c7aeba4e..a96897f6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,6 @@ FROM python:3.12 -RUN pip install poetry +COPY --from=ghcr.io/astral-sh/uv:0.11.14 /uv /uvx /bin/ ARG UID=1000 ARG GID=1000 @@ -12,11 +12,15 @@ USER $USER WORKDIR /repository -COPY README.md pyproject.toml poetry.lock* ./ -COPY donna/__init__.py donna/ +ENV UV_PROJECT_ENVIRONMENT=/home/donna/.venv +ENV UV_PYTHON_DOWNLOADS=never +ENV UV_LINK_MODE=copy +ENV PATH="/home/donna/.venv/bin:${PATH}" -RUN poetry install --no-interaction --no-ansi +COPY --chown=$USER:$USER . /repository -USER $USER +RUN uv sync --locked + +ENV UV_NO_SYNC=1 CMD [] diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 36208626..00000000 --- a/poetry.lock +++ /dev/null @@ -1,1610 +0,0 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "attrs" -version = "25.4.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, - {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, -] - -[[package]] -name = "autoflake" -version = "2.3.1" -description = "Removes unused imports and unused variables" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "autoflake-2.3.1-py3-none-any.whl", hash = "sha256:3ae7495db9084b7b32818b4140e6dc4fc280b712fb414f5b8fe57b0a8e85a840"}, - {file = "autoflake-2.3.1.tar.gz", hash = "sha256:c98b75dc5b0a86459c4f01a1d32ac7eb4338ec4317a4469515ff1e687ecd909e"}, -] - -[package.dependencies] -pyflakes = ">=3.0.0" - -[[package]] -name = "bandit" -version = "1.9.2" -description = "Security oriented static analyser for python code." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "bandit-1.9.2-py3-none-any.whl", hash = "sha256:bda8d68610fc33a6e10b7a8f1d61d92c8f6c004051d5e946406be1fb1b16a868"}, - {file = "bandit-1.9.2.tar.gz", hash = "sha256:32410415cd93bf9c8b91972159d5cf1e7f063a9146d70345641cd3877de348ce"}, -] - -[package.dependencies] -colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} -PyYAML = ">=5.3.1" -rich = "*" -stevedore = ">=1.20.0" - -[package.extras] -baseline = ["GitPython (>=3.1.30)"] -sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] -test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] -toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""] -yaml = ["PyYAML"] - -[[package]] -name = "black" -version = "25.12.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "black-25.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8"}, - {file = "black-25.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a"}, - {file = "black-25.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea"}, - {file = "black-25.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f"}, - {file = "black-25.12.0-cp310-cp310-win_arm64.whl", hash = "sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da"}, - {file = "black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a"}, - {file = "black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be"}, - {file = "black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b"}, - {file = "black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5"}, - {file = "black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655"}, - {file = "black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a"}, - {file = "black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783"}, - {file = "black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59"}, - {file = "black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892"}, - {file = "black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43"}, - {file = "black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5"}, - {file = "black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f"}, - {file = "black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf"}, - {file = "black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d"}, - {file = "black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce"}, - {file = "black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5"}, - {file = "black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f"}, - {file = "black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f"}, - {file = "black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83"}, - {file = "black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b"}, - {file = "black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828"}, - {file = "black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -pytokens = ">=0.3.0" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "changy" -version = "0.4.3" -description = "Simplest changelog manager, oriented to human editing, not to special message formatting in commits and tags.." -optional = false -python-versions = "<4.0,>=3.11" -groups = ["dev"] -files = [ - {file = "changy-0.4.3-py3-none-any.whl", hash = "sha256:53d34778a0b525c154fb0c39ef02fb41c2179ef2915f7f63cfeaa0a241d0facc"}, - {file = "changy-0.4.3.tar.gz", hash = "sha256:1a38ce222243f3bbf4f51a890bc14e393521954970244dc7d669e2bcbe523f85"}, -] - -[package.dependencies] -jinja2 = ">=3.1" -pydantic = ">=2.5" -pydantic-settings = ">=2.1" -typer = ">=0.9" - -[[package]] -name = "click" -version = "8.3.1" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, - {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "codespell" -version = "2.4.1" -description = "Fix common misspellings in text files" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425"}, - {file = "codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5"}, -] - -[package.extras] -dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"] -hard-encoding-detection = ["chardet"] -toml = ["tomli ; python_version < \"3.11\""] -types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"] - -[[package]] -name = "cognitive-complexity" -version = "1.3.0" -description = "Library to calculate Python functions cognitive complexity via code" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "cognitive_complexity-1.3.0.tar.gz", hash = "sha256:a0cfbd47dee0b19f4056f892389f501694b205c3af69fb703cc744541e03dde5"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "dev"] -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} - -[[package]] -name = "eradicate" -version = "2.3.0" -description = "Removes commented-out code." -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "eradicate-2.3.0-py3-none-any.whl", hash = "sha256:2b29b3dd27171f209e4ddd8204b70c02f0682ae95eecb353f10e8d72b149c63e"}, - {file = "eradicate-2.3.0.tar.gz", hash = "sha256:06df115be3b87d0fc1c483db22a2ebb12bcf40585722810d809cc770f5031c37"}, -] - -[[package]] -name = "flake8" -version = "7.3.0" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, - {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.14.0,<2.15.0" -pyflakes = ">=3.4.0,<3.5.0" - -[[package]] -name = "flake8-absolute-import" -version = "1.0.0.3" -description = "flake8 plugin to require absolute imports" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "flake8_absolute_import-1.0.0.3-py3-none-any.whl", hash = "sha256:de62300044f5060f32dbe5cbfa0ad7b345231323c572077997d9c8ae6241b189"}, - {file = "flake8_absolute_import-1.0.0.3.tar.gz", hash = "sha256:84b2bb2dbad98227a333ca4ba30357e073117ecd6b068b46ac5906fa9a7ed39e"}, -] - -[package.dependencies] -flake8 = ">=5.0" - -[[package]] -name = "flake8-annotations-complexity" -version = "0.1.0" -description = "A flake8 extension that checks for type annotations complexity" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "flake8_annotations_complexity-0.1.0-py3-none-any.whl", hash = "sha256:102d75f5ba0c667cde9c563062f4e7c616ca84bf5bfdc9c1f960a2655133ce35"}, - {file = "flake8_annotations_complexity-0.1.0.tar.gz", hash = "sha256:98b86ef87de5331d2b61f3cf472dcf6b8ff1a5ddde46f78bc894b464f06e1414"}, -] - -[package.dependencies] -flake8 = "*" - -[[package]] -name = "flake8-bandit" -version = "4.1.1" -description = "Automated security testing with bandit and flake8." -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "flake8_bandit-4.1.1-py3-none-any.whl", hash = "sha256:4c8a53eb48f23d4ef1e59293657181a3c989d0077c9952717e98a0eace43e06d"}, - {file = "flake8_bandit-4.1.1.tar.gz", hash = "sha256:068e09287189cbfd7f986e92605adea2067630b75380c6b5733dab7d87f9a84e"}, -] - -[package.dependencies] -bandit = ">=1.7.3" -flake8 = ">=5.0.0" - -[[package]] -name = "flake8-cognitive-complexity" -version = "0.1.0" -description = "An extension for flake8 that validates cognitive functions complexity" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "flake8_cognitive_complexity-0.1.0.tar.gz", hash = "sha256:f202df054e4f6ff182b659c261922b9c684628a47beb19cb0973c50d6a7831c1"}, -] - -[package.dependencies] -cognitive_complexity = "*" -setuptools = "*" - -[[package]] -name = "flake8-docstrings" -version = "1.7.0" -description = "Extension for flake8 which uses pydocstyle to check docstrings" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, - {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, -] - -[package.dependencies] -flake8 = ">=3" -pydocstyle = ">=2.1" - -[[package]] -name = "flake8-eradicate" -version = "1.5.0" -description = "Flake8 plugin to find commented out code" -optional = false -python-versions = ">=3.8,<4.0" -groups = ["dev"] -files = [ - {file = "flake8_eradicate-1.5.0-py3-none-any.whl", hash = "sha256:18acc922ad7de623f5247c7d5595da068525ec5437dd53b22ec2259b96ce9d22"}, - {file = "flake8_eradicate-1.5.0.tar.gz", hash = "sha256:aee636cb9ecb5594a7cd92d67ad73eb69909e5cc7bd81710cf9d00970f3983a6"}, -] - -[package.dependencies] -attrs = "*" -eradicate = ">=2.0,<3.0" -flake8 = ">5" - -[[package]] -name = "flake8-functions" -version = "0.0.8" -description = "A flake8 extension that checks functions" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "flake8_functions-0.0.8-py3-none-any.whl", hash = "sha256:e1a88aa634d1aff6973f8c9dd64f30ab2beaac661e52eea96929ccc7ee7f64df"}, - {file = "flake8_functions-0.0.8.tar.gz", hash = "sha256:5446626673a9faecbf389fb411b90bdc87b002c387b72dc097b208e7a58f2a1c"}, -] - -[package.dependencies] -mr-proper = "*" -setuptools = "*" - -[[package]] -name = "flake8-print" -version = "5.0.0" -description = "print statement checker plugin for flake8" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "flake8-print-5.0.0.tar.gz", hash = "sha256:76915a2a389cc1c0879636c219eb909c38501d3a43cc8dae542081c9ba48bdf9"}, - {file = "flake8_print-5.0.0-py3-none-any.whl", hash = "sha256:84a1a6ea10d7056b804221ac5e62b1cee1aefc897ce16f2e5c42d3046068f5d8"}, -] - -[package.dependencies] -flake8 = ">=3.0" -pycodestyle = "*" - -[[package]] -name = "flake8-pyproject" -version = "1.2.4" -description = "Flake8 plug-in loading the configuration from pyproject.toml" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "flake8_pyproject-1.2.4-py3-none-any.whl", hash = "sha256:ea34c057f9a9329c76d98723bb2bb498cc6ba8ff9872c4d19932d48c91249a77"}, -] - -[package.dependencies] -Flake8 = ">=5" - -[package.extras] -dev = ["Flit (>=3.4)", "pyTest (>=7)", "pyTest-cov (>=7) ; python_version >= \"3.10\""] - -[[package]] -name = "flake8-pytest" -version = "1.4" -description = "pytest assert checker plugin for flake8" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "flake8-pytest-1.4.tar.gz", hash = "sha256:19f543b2d1cc89d61b76f19d0a9e58e9a110a035175f701b3425c363a7732c56"}, - {file = "flake8_pytest-1.4-py2.py3-none-any.whl", hash = "sha256:97328f258ffad9fe18babb3b0714a16b121505ad3ac87d4e33020874555d0784"}, -] - -[package.dependencies] -flake8 = "*" - -[[package]] -name = "gitdb" -version = "4.0.12" -description = "Git Object Database" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, - {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, -] - -[package.dependencies] -smmap = ">=3.0.1,<6" - -[[package]] -name = "gitpython" -version = "3.1.50" -description = "GitPython is a Python library used to interact with Git repositories" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9"}, - {file = "gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc"}, -] - -[package.dependencies] -gitdb = ">=4.0.1,<5" - -[package.extras] -doc = ["sphinx (>=7.4.7,<8)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] -test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy (==1.18.2) ; python_version >= \"3.9\"", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] - -[[package]] -name = "iniconfig" -version = "2.3.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, - {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, -] - -[[package]] -name = "isort" -version = "7.0.0" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.10.0" -groups = ["dev"] -files = [ - {file = "isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1"}, - {file = "isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187"}, -] - -[package.extras] -colors = ["colorama"] -plugins = ["setuptools"] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "librt" -version = "0.7.5" -description = "Mypyc runtime library" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "platform_python_implementation != \"PyPy\"" -files = [ - {file = "librt-0.7.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81056e01bba1394f1d92904ec61a4078f66df785316275edbaf51d90da8c6e26"}, - {file = "librt-0.7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d7c72c8756eeb3aefb1b9e3dac7c37a4a25db63640cac0ab6fc18e91a0edf05a"}, - {file = "librt-0.7.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddc4a16207f88f9597b397fc1f60781266d13b13de922ff61c206547a29e4bbd"}, - {file = "librt-0.7.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63055d3dda433ebb314c9f1819942f16a19203c454508fdb2d167613f7017169"}, - {file = "librt-0.7.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f85f9b5db87b0f52e53c68ad2a0c5a53e00afa439bd54a1723742a2b1021276"}, - {file = "librt-0.7.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c566a4672564c5d54d8ab65cdaae5a87ee14c1564c1a2ddc7a9f5811c750f023"}, - {file = "librt-0.7.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fee15c2a190ef389f14928135c6fb2d25cd3fdb7887bfd9a7b444bbdc8c06b96"}, - {file = "librt-0.7.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:584cb3e605ec45ba350962cec853e17be0a25a772f21f09f1e422f7044ae2a7d"}, - {file = "librt-0.7.5-cp310-cp310-win32.whl", hash = "sha256:9c08527055fbb03c641c15bbc5b79dd2942fb6a3bd8dabf141dd7e97eeea4904"}, - {file = "librt-0.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:dd810f2d39c526c42ea205e0addad5dc08ef853c625387806a29d07f9d150d9b"}, - {file = "librt-0.7.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f952e1a78c480edee8fb43aa2bf2e84dcd46c917d44f8065b883079d3893e8fc"}, - {file = "librt-0.7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75965c1f4efb7234ff52a58b729d245a21e87e4b6a26a0ec08052f02b16274e4"}, - {file = "librt-0.7.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:732e0aa0385b59a1b2545159e781c792cc58ce9c134249233a7c7250a44684c4"}, - {file = "librt-0.7.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdde31759bd8888f3ef0eebda80394a48961328a17c264dce8cc35f4b9cde35d"}, - {file = "librt-0.7.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3146d52465b3b6397d25d513f428cb421c18df65b7378667bb5f1e3cc45805"}, - {file = "librt-0.7.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29c8d2fae11d4379ea207ba7fc69d43237e42cf8a9f90ec6e05993687e6d648b"}, - {file = "librt-0.7.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb41f04046b4f22b1e7ba5ef513402cd2e3477ec610e5f92d38fe2bba383d419"}, - {file = "librt-0.7.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8bb7883c1e94ceb87c2bf81385266f032da09cd040e804cc002f2c9d6b842e2f"}, - {file = "librt-0.7.5-cp311-cp311-win32.whl", hash = "sha256:84d4a6b9efd6124f728558a18e79e7cc5c5d4efc09b2b846c910de7e564f5bad"}, - {file = "librt-0.7.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab4b0d3bee6f6ff7017e18e576ac7e41a06697d8dea4b8f3ab9e0c8e1300c409"}, - {file = "librt-0.7.5-cp311-cp311-win_arm64.whl", hash = "sha256:730be847daad773a3c898943cf67fb9845a3961d06fb79672ceb0a8cd8624cfa"}, - {file = "librt-0.7.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba1077c562a046208a2dc6366227b3eeae8f2c2ab4b41eaf4fd2fa28cece4203"}, - {file = "librt-0.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:654fdc971c76348a73af5240d8e2529265b9a7ba6321e38dd5bae7b0d4ab3abe"}, - {file = "librt-0.7.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6b7b58913d475911f6f33e8082f19dd9b120c4f4a5c911d07e395d67b81c6982"}, - {file = "librt-0.7.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e0fd344bad57026a8f4ccfaf406486c2fc991838050c2fef156170edc3b775"}, - {file = "librt-0.7.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46aa91813c267c3f60db75d56419b42c0c0b9748ec2c568a0e3588e543fb4233"}, - {file = "librt-0.7.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ddc0ab9dbc5f9ceaf2bf7a367bf01f2697660e908f6534800e88f43590b271db"}, - {file = "librt-0.7.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7a488908a470451338607650f1c064175094aedebf4a4fa37890682e30ce0b57"}, - {file = "librt-0.7.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47fc52602ffc374e69bf1b76536dc99f7f6dd876bd786c8213eaa3598be030a"}, - {file = "librt-0.7.5-cp312-cp312-win32.whl", hash = "sha256:cda8b025875946ffff5a9a7590bf9acde3eb02cb6200f06a2d3e691ef3d9955b"}, - {file = "librt-0.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:b591c094afd0ffda820e931148c9e48dc31a556dc5b2b9b3cc552fa710d858e4"}, - {file = "librt-0.7.5-cp312-cp312-win_arm64.whl", hash = "sha256:532ddc6a8a6ca341b1cd7f4d999043e4c71a212b26fe9fd2e7f1e8bb4e873544"}, - {file = "librt-0.7.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b1795c4b2789b458fa290059062c2f5a297ddb28c31e704d27e161386469691a"}, - {file = "librt-0.7.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2fcbf2e135c11f721193aa5f42ba112bb1046afafbffd407cbc81d8d735c74d0"}, - {file = "librt-0.7.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c039bbf79a9a2498404d1ae7e29a6c175e63678d7a54013a97397c40aee026c5"}, - {file = "librt-0.7.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3919c9407faeeee35430ae135e3a78acd4ecaaaa73767529e2c15ca1d73ba325"}, - {file = "librt-0.7.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26b46620e1e0e45af510d9848ea0915e7040605dd2ae94ebefb6c962cbb6f7ec"}, - {file = "librt-0.7.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9bbb8facc5375476d392990dd6a71f97e4cb42e2ac66f32e860f6e47299d5e89"}, - {file = "librt-0.7.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e9e9c988b5ffde7be02180f864cbd17c0b0c1231c235748912ab2afa05789c25"}, - {file = "librt-0.7.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:edf6b465306215b19dbe6c3fb63cf374a8f3e1ad77f3b4c16544b83033bbb67b"}, - {file = "librt-0.7.5-cp313-cp313-win32.whl", hash = "sha256:060bde69c3604f694bd8ae21a780fe8be46bb3dbb863642e8dfc75c931ca8eee"}, - {file = "librt-0.7.5-cp313-cp313-win_amd64.whl", hash = "sha256:a82d5a0ee43aeae2116d7292c77cc8038f4841830ade8aa922e098933b468b9e"}, - {file = "librt-0.7.5-cp313-cp313-win_arm64.whl", hash = "sha256:3c98a8d0ac9e2a7cb8ff8c53e5d6e8d82bfb2839abf144fdeaaa832f2a12aa45"}, - {file = "librt-0.7.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9937574e6d842f359b8585903d04f5b4ab62277a091a93e02058158074dc52f2"}, - {file = "librt-0.7.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5cd3afd71e9bc146203b6c8141921e738364158d4aa7cdb9a874e2505163770f"}, - {file = "librt-0.7.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cffa3ef0af29687455161cb446eff059bf27607f95163d6a37e27bcb37180f6"}, - {file = "librt-0.7.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f3f088482e2229387eadf8215c03f7726d56f69cce8c0c40f0795aebc9b361"}, - {file = "librt-0.7.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7aa33153a5bb0bac783d2c57885889b1162823384e8313d47800a0e10d0070e"}, - {file = "librt-0.7.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:265729b551a2dd329cc47b323a182fb7961af42abf21e913c9dd7d3331b2f3c2"}, - {file = "librt-0.7.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:168e04663e126416ba712114050f413ac306759a1791d87b7c11d4428ba75760"}, - {file = "librt-0.7.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:553dc58987d1d853adda8aeadf4db8e29749f0b11877afcc429a9ad892818ae2"}, - {file = "librt-0.7.5-cp314-cp314-win32.whl", hash = "sha256:263f4fae9eba277513357c871275b18d14de93fd49bf5e43dc60a97b81ad5eb8"}, - {file = "librt-0.7.5-cp314-cp314-win_amd64.whl", hash = "sha256:85f485b7471571e99fab4f44eeb327dc0e1f814ada575f3fa85e698417d8a54e"}, - {file = "librt-0.7.5-cp314-cp314-win_arm64.whl", hash = "sha256:49c596cd18e90e58b7caa4d7ca7606049c1802125fcff96b8af73fa5c3870e4d"}, - {file = "librt-0.7.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:54d2aef0b0f5056f130981ad45081b278602ff3657fe16c88529f5058038e802"}, - {file = "librt-0.7.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b4791202296ad51ac09a3ff58eb49d9da8e3a4009167a6d76ac418a974e5fd4"}, - {file = "librt-0.7.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e860909fea75baef941ee6436e0453612505883b9d0d87924d4fda27865b9a2"}, - {file = "librt-0.7.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f02c4337bf271c4f06637f5ff254fad2238c0b8e32a3a480ebb2fc5e26f754a5"}, - {file = "librt-0.7.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f51ffe59f4556243d3cc82d827bde74765f594fa3ceb80ec4de0c13ccd3416"}, - {file = "librt-0.7.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0b7f080ba30601dfa3e3deed3160352273e1b9bc92e652f51103c3e9298f7899"}, - {file = "librt-0.7.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb565b4219abc8ea2402e61c7ba648a62903831059ed3564fa1245cc245d58d7"}, - {file = "librt-0.7.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a3cfb15961e7333ea6ef033dc574af75153b5c230d5ad25fbcd55198f21e0cf"}, - {file = "librt-0.7.5-cp314-cp314t-win32.whl", hash = "sha256:118716de5ad6726332db1801bc90fa6d94194cd2e07c1a7822cebf12c496714d"}, - {file = "librt-0.7.5-cp314-cp314t-win_amd64.whl", hash = "sha256:3dd58f7ce20360c6ce0c04f7bd9081c7f9c19fc6129a3c705d0c5a35439f201d"}, - {file = "librt-0.7.5-cp314-cp314t-win_arm64.whl", hash = "sha256:08153ea537609d11f774d2bfe84af39d50d5c9ca3a4d061d946e0c9d8bce04a1"}, - {file = "librt-0.7.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:df2e210400b28e50994477ebf82f055698c79797b6ee47a1669d383ca33263e1"}, - {file = "librt-0.7.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d2cc7d187e8c6e9b7bdbefa9697ce897a704ea7a7ce844f2b4e0e2aa07ae51d3"}, - {file = "librt-0.7.5-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39183abee670bc37b85f11e86c44a9cad1ed6efa48b580083e89ecee13dd9717"}, - {file = "librt-0.7.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191cbd42660446d67cf7a95ac7bfa60f49b8b3b0417c64f216284a1d86fc9335"}, - {file = "librt-0.7.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea1b60b86595a5dc1f57b44a801a1c4d8209c0a69518391d349973a4491408e6"}, - {file = "librt-0.7.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:af69d9e159575e877c7546d1ee817b4ae089aa221dd1117e20c24ad8dc8659c7"}, - {file = "librt-0.7.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0e2bf8f91093fac43e3eaebacf777f12fd539dce9ec5af3efc6d8424e96ccd49"}, - {file = "librt-0.7.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8dcae24de1bc9da93aa689cb6313c70e776d7cea2fcf26b9b6160fedfe6bd9af"}, - {file = "librt-0.7.5-cp39-cp39-win32.whl", hash = "sha256:cdb001a1a0e4f41e613bca2c0fc147fc8a7396f53fc94201cbfd8ec7cd69ca4b"}, - {file = "librt-0.7.5-cp39-cp39-win_amd64.whl", hash = "sha256:a9eacbf983319b26b5f340a2e0cd47ac1ee4725a7f3a72fd0f15063c934b69d6"}, - {file = "librt-0.7.5.tar.gz", hash = "sha256:de4221a1181fa9c8c4b5f35506ed6f298948f44003d84d2a8b9885d7e01e6cfa"}, -] - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, - {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins (>=0.5.0)"] -profiling = ["gprof2dot"] -rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] - -[[package]] -name = "markupsafe" -version = "3.0.3" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, - {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, - {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, - {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, - {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, - {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, - {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, - {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, - {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, -] - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mdformat" -version = "1.0.0" -description = "CommonMark compliant Markdown formatter" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "mdformat-1.0.0-py3-none-any.whl", hash = "sha256:bca015d65a1d063a02e885a91daee303057bc7829c2cd37b2075a50dbb65944b"}, - {file = "mdformat-1.0.0.tar.gz", hash = "sha256:4954045fcae797c29f86d4ad879e43bb151fa55dbaf74ac6eaeacf1d45bb3928"}, -] - -[package.dependencies] -markdown-it-py = ">=1,<5" - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "mr-proper" -version = "0.0.7" -description = "Static Python code analyzer, that tries to check if functions in code are pure or not and why." -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "mr_proper-0.0.7-py3-none-any.whl", hash = "sha256:74a1b60240c46f10ba518707ef72811a01e5c270da0a78b5dd2dd923d99fdb14"}, - {file = "mr_proper-0.0.7.tar.gz", hash = "sha256:03b517b19e617537f711ce418b125e5f2efd82ec881539cdee83195c78c14a02"}, -] - -[package.dependencies] -click = ">=7.1.2" -setuptools = "*" -stdlib-list = ">=0.5.0" - -[[package]] -name = "mypy" -version = "1.19.1" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, - {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, - {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, - {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, - {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, - {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, - {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, - {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, - {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, - {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, - {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, - {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, - {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, - {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, - {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, - {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, - {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, - {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, - {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, - {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, - {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, - {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, - {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, - {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, - {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, - {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, - {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, - {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, - {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, - {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, - {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, - {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, - {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, - {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, - {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, - {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, - {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, - {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, -] - -[package.dependencies] -librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} -mypy_extensions = ">=1.0.0" -pathspec = ">=0.9.0" -typing_extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "networkx" -version = "3.6" -description = "Python package for creating and manipulating graphs and networks" -optional = false -python-versions = ">=3.11" -groups = ["dev"] -files = [ - {file = "networkx-3.6-py3-none-any.whl", hash = "sha256:cdb395b105806062473d3be36458d8f1459a4e4b98e236a66c3a48996e07684f"}, - {file = "networkx-3.6.tar.gz", hash = "sha256:285276002ad1f7f7da0f7b42f004bcba70d381e936559166363707fdad3d72ad"}, -] - -[package.extras] -benchmarking = ["asv", "virtualenv"] -default = ["matplotlib (>=3.8)", "numpy (>=1.25)", "pandas (>=2.0)", "scipy (>=1.11.2)"] -developer = ["mypy (>=1.15)", "pre-commit (>=4.1)"] -doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=10)", "pydata-sphinx-theme (>=0.16)", "sphinx (>=8.0)", "sphinx-gallery (>=0.18)", "texext (>=0.6.7)"] -example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "iplotx (>=0.9.0)", "momepy (>=0.7.2)", "osmnx (>=2.0.0)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] -extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] -release = ["build (>=0.10)", "changelist (==0.5)", "twine (>=4.0)", "wheel (>=0.40)"] -test = ["pytest (>=7.2)", "pytest-cov (>=4.0)", "pytest-xdist (>=3.0)"] -test-extras = ["pytest-mpl", "pytest-randomly"] - -[[package]] -name = "packaging" -version = "25.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.5.1" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, - {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, -] - -[package.extras] -docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] -type = ["mypy (>=1.18.2)"] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "prompt-toolkit" -version = "3.0.52" -description = "Library for building powerful interactive command lines in Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"}, - {file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"}, -] - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "pycodestyle" -version = "2.14.0" -description = "Python style guide checker" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, - {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, - {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.41.5" -typing-extensions = ">=4.14.1" -typing-inspection = ">=0.4.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, - {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, -] - -[package.dependencies] -typing-extensions = ">=4.14.1" - -[[package]] -name = "pydantic-settings" -version = "2.12.0" -description = "Settings management using Pydantic" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809"}, - {file = "pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0"}, -] - -[package.dependencies] -pydantic = ">=2.7.0" -python-dotenv = ">=0.21.0" -typing-inspection = ">=0.4.0" - -[package.extras] -aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] -azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] -gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] -toml = ["tomli (>=2.0.1)"] -yaml = ["pyyaml (>=6.0.1)"] - -[[package]] -name = "pydocstyle" -version = "6.3.0" -description = "Python docstring style checker" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, - {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, -] - -[package.dependencies] -snowballstemmer = ">=2.2.0" - -[package.extras] -toml = ["tomli (>=1.2.3) ; python_version < \"3.11\""] - -[[package]] -name = "pydot" -version = "4.0.1" -description = "Python interface to Graphviz's Dot" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pydot-4.0.1-py3-none-any.whl", hash = "sha256:869c0efadd2708c0be1f916eb669f3d664ca684bc57ffb7ecc08e70d5e93fee6"}, - {file = "pydot-4.0.1.tar.gz", hash = "sha256:c2148f681c4a33e08bf0e26a9e5f8e4099a82e0e2a068098f32ce86577364ad5"}, -] - -[package.dependencies] -pyparsing = ">=3.1.0" - -[package.extras] -dev = ["chardet", "parameterized", "pydot[lint]", "pydot[types]"] -lint = ["ruff"] -release = ["zest.releaser[recommended]"] -tests = ["pydot[dev]", "pytest", "pytest-cov", "pytest-xdist[psutil]", "tox"] -types = ["mypy"] - -[[package]] -name = "pyflakes" -version = "3.4.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, - {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, -] - -[[package]] -name = "pygments" -version = "2.19.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pyparsing" -version = "3.3.2" -description = "pyparsing - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, - {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pytest" -version = "9.0.3" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, - {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -iniconfig = ">=1.0.1" -packaging = ">=22" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-mock" -version = "3.15.1" -description = "Thin-wrapper around the mock package for easier use with pytest" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, - {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, -] - -[package.dependencies] -pytest = ">=6.2.5" - -[package.extras] -dev = ["pre-commit", "pytest-asyncio", "tox"] - -[[package]] -name = "python-dotenv" -version = "1.2.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, - {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "pytokens" -version = "0.3.0" -description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3"}, - {file = "pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a"}, -] - -[package.extras] -dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] - -[[package]] -name = "pyyaml" -version = "6.0.3" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, - {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, - {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, - {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, - {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, - {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, - {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, - {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, - {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, - {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, - {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, - {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, - {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, -] - -[[package]] -name = "rich" -version = "14.2.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -groups = ["main", "dev"] -files = [ - {file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"}, - {file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "setuptools" -version = "80.9.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, - {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] - -[[package]] -name = "shellingham" -version = "1.5.4" -description = "Tool to Detect Surrounding Shell" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, - {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, -] - -[[package]] -name = "smmap" -version = "5.0.3" -description = "A pure Python implementation of a sliding window memory map manager" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f"}, - {file = "smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c"}, -] - -[[package]] -name = "snowballstemmer" -version = "3.0.1" -description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" -groups = ["dev"] -files = [ - {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, - {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, -] - -[[package]] -name = "stdlib-list" -version = "0.12.0" -description = "A list of Python Standard Libraries (2.7 through 3.14)." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "stdlib_list-0.12.0-py3-none-any.whl", hash = "sha256:df2d11e97f53812a1756fb5510393a11e3b389ebd9239dc831c7f349957f62f2"}, - {file = "stdlib_list-0.12.0.tar.gz", hash = "sha256:517824f27ee89e591d8ae7c1dd9ff34f672eae50ee886ea31bb8816d77535675"}, -] - -[package.extras] -dev = ["build", "stdlib-list[doc,lint,test]"] -doc = ["furo", "sphinx"] -lint = ["mypy", "ruff"] -support = ["sphobjinv"] -test = ["coverage[toml]", "pytest", "pytest-cov"] - -[[package]] -name = "stevedore" -version = "5.6.0" -description = "Manage dynamic plugins for Python applications" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820"}, - {file = "stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945"}, -] - -[[package]] -name = "tach" -version = "0.34.1" -description = "A Python tool to maintain a modular package architecture." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "tach-0.34.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6dff869e7f8933be23055f407a751e6f9e4c8ecb0bf3a93a962a0815e106c313"}, - {file = "tach-0.34.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:70d55ec38dfcc15f1fff9e420ba17d63c36bb1b27f92abb8e8c910ffb71fd233"}, - {file = "tach-0.34.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:862d240a7fc297283a51c857eb269d8c56163f14b7e398f757b284bed1e3df4f"}, - {file = "tach-0.34.1-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcb8ca825287160c1dd37b1cb164b88675e01ee97304ce14d50d7e8e1d3b853c"}, - {file = "tach-0.34.1-cp37-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b272ee726d8b0ef3887d3a3057f618922ab0f7471e1b1d6096f78f2ae93727e4"}, - {file = "tach-0.34.1-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6042b4f040c5941d28229963451b94a8cf2b08cdbb0ab0e338a04a46887c712f"}, - {file = "tach-0.34.1-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b52a70262f16b701aaa03781656d808f88a3eb6a263762f8cea726ea9ffb980"}, - {file = "tach-0.34.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cba2d957371fde09c64cb1b70349844e33fae3d87f55e6b002929587c5c7826"}, - {file = "tach-0.34.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c600ea6f8748ea13183aaf802fb733c148238e252cc0e66a85ac8d6de9939906"}, - {file = "tach-0.34.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:7be15f69c99b89cdadc2835368fc3cfb3ad45e74c7a704a7d42bcdd2917608b2"}, - {file = "tach-0.34.1-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:d917fc81adae85e9b3f2d29e135ba572a69dc677661727012b3adf95e0216e9e"}, - {file = "tach-0.34.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f94909630d699806e8682662962de0ac76ecc05dfcf7893af2c7e1a5c71de57e"}, - {file = "tach-0.34.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1246b2718cfffecd4e6d66d6e232323fc4b3f2c68b4c990af0063464808f85fa"}, - {file = "tach-0.34.1-cp37-abi3-win32.whl", hash = "sha256:abd9d1e468a9b8bec1e0037c06797b31e9c23c62adf13bcfacf057a21310ad3f"}, - {file = "tach-0.34.1-cp37-abi3-win_amd64.whl", hash = "sha256:0220c002da4bf4656b121c6317313490e8660627e5200251e0bca3e00a27d0c4"}, - {file = "tach-0.34.1-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b54972211022eafe29a03f5fd4c34d2a7b52ed77eb0e9ff897d64f4c8c2dcd2f"}, - {file = "tach-0.34.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c396aef13bd7e5398674886115160f4d6b9cf7baae1fb9431689d9ac4237f97a"}, - {file = "tach-0.34.1.tar.gz", hash = "sha256:58b5a8f9dd4f5c9fc9b1ade875aa0b31d3ac2f2f6802c655c05197a503d5acde"}, -] - -[package.dependencies] -gitpython = ">=3.1,<4.0" -networkx = ">=2.6,<4.0" -prompt-toolkit = ">=3.0,<4.0" -pydot = ">=2,<5" -pyyaml = ">=6.0,<7.0" -rich = ">=13.0" -tomli = ">=1.2.2" -tomli-w = ">=1.0,<2.0" - -[[package]] -name = "tomli" -version = "2.4.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, - {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, - {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, - {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, - {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, - {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, - {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, - {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, - {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, - {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, - {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, - {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, - {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, - {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, - {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, - {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, - {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, - {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, - {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, -] - -[[package]] -name = "tomli-w" -version = "1.2.0" -description = "A lil' TOML writer" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"}, - {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, -] - -[[package]] -name = "typer" -version = "0.20.1" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "typer-0.20.1-py3-none-any.whl", hash = "sha256:4b3bde918a67c8e03d861aa02deca90a95bbac572e71b1b9be56ff49affdb5a8"}, - {file = "typer-0.20.1.tar.gz", hash = "sha256:68585eb1b01203689c4199bc440d6be616f0851e9f0eb41e4a778845c5a0fd5b"}, -] - -[package.dependencies] -click = ">=8.0.0" -rich = ">=10.11.0" -shellingham = ">=1.3.0" -typing-extensions = ">=3.7.4.3" - -[[package]] -name = "types-pyyaml" -version = "6.0.12.20250915" -description = "Typing stubs for PyYAML" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6"}, - {file = "types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3"}, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -description = "Runtime typing introspection tools" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, -] - -[package.dependencies] -typing-extensions = ">=4.12.0" - -[[package]] -name = "wcwidth" -version = "0.7.0" -description = "Measures the displayed width of unicode strings in a terminal" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2"}, - {file = "wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0"}, -] - -[metadata] -lock-version = "2.1" -python-versions = ">=3.12,<4.0" -content-hash = "32d87af3c1081121c7550768997104a0c3cca15e3cf46f20d6eb477f5e1a0fad" diff --git a/pyproject.toml b/pyproject.toml index 64a0c583..b0133b37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" [project] name = "donna" @@ -17,7 +17,6 @@ classifiers = [ "Development Status :: 3 - Alpha", "Environment :: Console", "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", @@ -42,33 +41,30 @@ Changelog = "https://github.com/Tiendil/donna/blob/main/CHANGELOG.md" [project.scripts] donna = "donna.cli.application:main" -[tool.poetry.group.dev.dependencies] - -changy = "0.4.*" - -isort = "7.0.*" -black = "25.12.*" -autoflake = "2.3.*" -codespell = "2.4.*" -tach = "0.34.*" - -flake8 = "7.3.*" -flake8-docstrings = "1.7.*" -flake8-functions = "0.0.*" -flake8-annotations-complexity = "0.1.*" -flake8-cognitive-complexity = "0.1.*" -flake8-pytest = "1.4.*" -flake8-bandit = "4.1.*" -flake8-absolute-import = "1.0.*" -flake8-print = "5.0.*" -flake8-pyproject = "1.2.*" -flake8-eradicate = "1.5.*" - -mypy = "1.19.*" - -types-PyYAML = "6.0.*" -pytest = "9.0.*" -pytest-mock = "3.15.*" +[dependency-groups] +dev = [ + "changy==0.4.*", + "isort==7.0.*", + "black==25.12.*", + "autoflake==2.3.*", + "codespell==2.4.*", + "tach==0.34.*", + "flake8==7.3.*", + "flake8-docstrings==1.7.*", + "flake8-functions==0.0.*", + "flake8-annotations-complexity==0.1.*", + "flake8-cognitive-complexity==0.1.*", + "flake8-pytest==1.4.*", + "flake8-bandit==4.1.*", + "flake8-absolute-import==1.0.*", + "flake8-print==5.0.*", + "flake8-pyproject==1.2.*", + "flake8-eradicate==1.5.*", + "mypy==1.19.*", + "types-PyYAML==6.0.*", + "pytest==9.0.*", + "pytest-mock==3.15.*", +] [tool.isort] profile = "black" @@ -121,3 +117,15 @@ init_forbid_extra = true init_typed = true warn_required_dynamic_aliases = true warn_untyped_fields = true + +[tool.hatch.build.targets.sdist] +include = [ + "/donna", + "/CHANGELOG.md", + "/LICENSE", + "/README.md", + "/pyproject.toml", +] + +[tool.hatch.build.targets.wheel] +packages = ["donna"] diff --git a/specs/architecture/tests.md b/specs/architecture/tests.md index d17a2b83..0e4951f5 100644 --- a/specs/architecture/tests.md +++ b/specs/architecture/tests.md @@ -33,13 +33,13 @@ Development-related test execution MUST happen through the project development c The preferred command form for running tests is: ```bash -./bin/dev.sh poetry run pytest +./bin/dev.sh uv run pytest ``` The preferred command form for running targeted tests is: ```bash -./bin/dev.sh poetry run pytest +./bin/dev.sh uv run pytest ``` Tests SHOULD be deterministic for the same repository state and filesystem state. diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..5b87cb34 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1165 @@ +version = 1 +revision = 3 +requires-python = ">=3.12, <4.0" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "autoflake" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/0b/70c277eef225133763bf05c02c88df182e57d5c5c0730d3998958096a82e/autoflake-2.3.3.tar.gz", hash = "sha256:c24809541e23999f7a7b0d2faadf15deb0bc04cdde49728a2fd943a0c8055504", size = 16515, upload-time = "2026-02-20T05:01:43.448Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/21/26f1680ec3a598ea31768f9ebcd427e42986d077a005416094b580635532/autoflake-2.3.3-py3-none-any.whl", hash = "sha256:a51a3412aff16135ee5b3ec25922459fef10c1f23ce6d6c4977188df859e8b53", size = 17715, upload-time = "2026-02-20T05:01:42.137Z" }, +] + +[[package]] +name = "bandit" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, +] + +[[package]] +name = "black" +version = "25.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/bd/26083f805115db17fda9877b3c7321d08c647df39d0df4c4ca8f8450593e/black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", size = 1924178, upload-time = "2025-12-08T01:49:51.048Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/ea00d6651561e2bdd9231c4177f4f2ae19cc13a0b0574f47602a7519b6ca/black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", size = 1742643, upload-time = "2025-12-08T01:49:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/08/2c64830cb6616278067e040acca21d4f79727b23077633953081c9445d61/black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", size = 1426197, upload-time = "2025-12-08T01:45:51.198Z" }, + { url = "https://files.pythonhosted.org/packages/d4/60/a93f55fd9b9816b7432cf6842f0e3000fdd5b7869492a04b9011a133ee37/black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", size = 1237266, upload-time = "2025-12-08T01:45:10.556Z" }, + { url = "https://files.pythonhosted.org/packages/c8/52/c551e36bc95495d2aa1a37d50566267aa47608c81a53f91daa809e03293f/black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", size = 1923809, upload-time = "2025-12-08T01:46:55.126Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f7/aac9b014140ee56d247e707af8db0aae2e9efc28d4a8aba92d0abd7ae9d1/black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", size = 1742384, upload-time = "2025-12-08T01:49:37.022Z" }, + { url = "https://files.pythonhosted.org/packages/74/98/38aaa018b2ab06a863974c12b14a6266badc192b20603a81b738c47e902e/black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", size = 1798761, upload-time = "2025-12-08T01:46:05.386Z" }, + { url = "https://files.pythonhosted.org/packages/16/3a/a8ac542125f61574a3f015b521ca83b47321ed19bb63fe6d7560f348bfe1/black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", size = 1429180, upload-time = "2025-12-08T01:45:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2d/bdc466a3db9145e946762d52cd55b1385509d9f9004fec1c97bdc8debbfb/black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", size = 1239350, upload-time = "2025-12-08T01:46:09.458Z" }, + { url = "https://files.pythonhosted.org/packages/35/46/1d8f2542210c502e2ae1060b2e09e47af6a5e5963cb78e22ec1a11170b28/black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", size = 1917015, upload-time = "2025-12-08T01:53:27.987Z" }, + { url = "https://files.pythonhosted.org/packages/41/37/68accadf977672beb8e2c64e080f568c74159c1aaa6414b4cd2aef2d7906/black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", size = 1741830, upload-time = "2025-12-08T01:54:36.861Z" }, + { url = "https://files.pythonhosted.org/packages/ac/76/03608a9d8f0faad47a3af3a3c8c53af3367f6c0dd2d23a84710456c7ac56/black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", size = 1791450, upload-time = "2025-12-08T01:44:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/06/99/b2a4bd7dfaea7964974f947e1c76d6886d65fe5d24f687df2d85406b2609/black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", size = 1452042, upload-time = "2025-12-08T01:46:13.188Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/d9825de75ae5dd7795d007681b752275ea85a1c5d83269b4b9c754c2aaab/black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", size = 1267446, upload-time = "2025-12-08T01:46:14.497Z" }, + { url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" }, +] + +[[package]] +name = "changy" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/ed/92e13e901b46547e314befcce921ad4471fa6dbaf4bdbcadf2afd3285e26/changy-0.4.3.tar.gz", hash = "sha256:1a38ce222243f3bbf4f51a890bc14e393521954970244dc7d669e2bcbe523f85", size = 7272, upload-time = "2024-12-10T09:45:43.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/67/2bfe58c28eb5f49af43982da78aa63296fbfff39291fe6d65fda88dd835f/changy-0.4.3-py3-none-any.whl", hash = "sha256:53d34778a0b525c154fb0c39ef02fb41c2179ef2915f7f63cfeaa0a241d0facc", size = 8948, upload-time = "2024-12-10T09:45:40.611Z" }, +] + +[[package]] +name = "click" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" }, +] + +[[package]] +name = "codespell" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/9d/1d0903dff693160f893ca6abcabad545088e7a2ee0a6deae7c24e958be69/codespell-2.4.2.tar.gz", hash = "sha256:3c33be9ae34543807f088aeb4832dfad8cb2dae38da61cac0a7045dd376cfdf3", size = 352058, upload-time = "2026-03-05T18:10:42.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/a1/52fa05533e95fe45bcc09bcf8a503874b1c08f221a4e35608017e0938f55/codespell-2.4.2-py3-none-any.whl", hash = "sha256:97e0c1060cf46bd1d5db89a936c98db8c2b804e1fdd4b5c645e82a1ec6b1f886", size = 353715, upload-time = "2026-03-05T18:10:41.398Z" }, +] + +[[package]] +name = "cognitive-complexity" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/ff/3cd46792fcbf742458083527407bc336efe382b168595583a06c70bf8e54/cognitive_complexity-1.3.0.tar.gz", hash = "sha256:a0cfbd47dee0b19f4056f892389f501694b205c3af69fb703cc744541e03dde5", size = 5650, upload-time = "2022-08-09T07:07:52.952Z" } + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "donna" +version = "0.3.0" +source = { editable = "." } +dependencies = [ + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdformat" }, + { name = "pydantic" }, + { name = "tomli-w" }, + { name = "typer" }, +] + +[package.dev-dependencies] +dev = [ + { name = "autoflake" }, + { name = "black" }, + { name = "changy" }, + { name = "codespell" }, + { name = "flake8" }, + { name = "flake8-absolute-import" }, + { name = "flake8-annotations-complexity" }, + { name = "flake8-bandit" }, + { name = "flake8-cognitive-complexity" }, + { name = "flake8-docstrings" }, + { name = "flake8-eradicate" }, + { name = "flake8-functions" }, + { name = "flake8-print" }, + { name = "flake8-pyproject" }, + { name = "flake8-pytest" }, + { name = "isort" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-mock" }, + { name = "tach" }, + { name = "types-pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "jinja2", specifier = ">=3.1,<3.2" }, + { name = "markdown-it-py", specifier = ">=4.0,<4.1" }, + { name = "mdformat", specifier = ">=1.0.0,<1.1.0" }, + { name = "pydantic", specifier = ">=2.12,<2.13" }, + { name = "tomli-w", specifier = ">=1.2,<1.3" }, + { name = "typer", specifier = ">=0.20,<0.21" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "autoflake", specifier = "==2.3.*" }, + { name = "black", specifier = "==25.12.*" }, + { name = "changy", specifier = "==0.4.*" }, + { name = "codespell", specifier = "==2.4.*" }, + { name = "flake8", specifier = "==7.3.*" }, + { name = "flake8-absolute-import", specifier = "==1.0.*" }, + { name = "flake8-annotations-complexity", specifier = "==0.1.*" }, + { name = "flake8-bandit", specifier = "==4.1.*" }, + { name = "flake8-cognitive-complexity", specifier = "==0.1.*" }, + { name = "flake8-docstrings", specifier = "==1.7.*" }, + { name = "flake8-eradicate", specifier = "==1.5.*" }, + { name = "flake8-functions", specifier = "==0.0.*" }, + { name = "flake8-print", specifier = "==5.0.*" }, + { name = "flake8-pyproject", specifier = "==1.2.*" }, + { name = "flake8-pytest", specifier = "==1.4.*" }, + { name = "isort", specifier = "==7.0.*" }, + { name = "mypy", specifier = "==1.19.*" }, + { name = "pytest", specifier = "==9.0.*" }, + { name = "pytest-mock", specifier = "==3.15.*" }, + { name = "tach", specifier = "==0.34.*" }, + { name = "types-pyyaml", specifier = "==6.0.*" }, +] + +[[package]] +name = "eradicate" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/e1/665186aedea2d6ebf0415cf97c0629c8123a721e7afc417deeade5598215/eradicate-2.3.0.tar.gz", hash = "sha256:06df115be3b87d0fc1c483db22a2ebb12bcf40585722810d809cc770f5031c37", size = 8536, upload-time = "2023-06-09T06:31:41.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/c2/533e1338429aeba1f089566a2314d69d3e78ab57a73006f16a923bf2b24c/eradicate-2.3.0-py3-none-any.whl", hash = "sha256:2b29b3dd27171f209e4ddd8204b70c02f0682ae95eecb353f10e8d72b149c63e", size = 6113, upload-time = "2023-06-09T06:31:40.209Z" }, +] + +[[package]] +name = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + +[[package]] +name = "flake8-absolute-import" +version = "1.0.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flake8" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/b3/99967e98c60d82884886c6d0b607d80ccb4a474c69734c60f0533cf1f9d5/flake8_absolute_import-1.0.0.3.tar.gz", hash = "sha256:84b2bb2dbad98227a333ca4ba30357e073117ecd6b068b46ac5906fa9a7ed39e", size = 10900, upload-time = "2025-11-28T02:12:01.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/97/a2f279f6c749912ad7d254af9c8ec845736352f6d162fb0fc25000d26e22/flake8_absolute_import-1.0.0.3-py3-none-any.whl", hash = "sha256:de62300044f5060f32dbe5cbfa0ad7b345231323c572077997d9c8ae6241b189", size = 5905, upload-time = "2025-11-28T02:12:08.884Z" }, +] + +[[package]] +name = "flake8-annotations-complexity" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flake8" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/68/ab6f136df474fbbddad1bff3965b9857a89327d819a385c815549a884cd7/flake8_annotations_complexity-0.1.0.tar.gz", hash = "sha256:98b86ef87de5331d2b61f3cf472dcf6b8ff1a5ddde46f78bc894b464f06e1414", size = 5365, upload-time = "2025-03-04T10:33:02.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/8c/dcd25a2c5b727851c28e1875a04dadd8e22b31564502eea63d5977cd31a8/flake8_annotations_complexity-0.1.0-py3-none-any.whl", hash = "sha256:102d75f5ba0c667cde9c563062f4e7c616ca84bf5bfdc9c1f960a2655133ce35", size = 5539, upload-time = "2025-03-04T10:33:00.558Z" }, +] + +[[package]] +name = "flake8-bandit" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bandit" }, + { name = "flake8" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/1c/4f66a7a52a246d6c64312b5c40da3af3630cd60b27af81b137796af3c0bc/flake8_bandit-4.1.1.tar.gz", hash = "sha256:068e09287189cbfd7f986e92605adea2067630b75380c6b5733dab7d87f9a84e", size = 5403, upload-time = "2022-08-29T13:48:41.225Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/5f/55bab0ac89f9ad9f4c6e38087faa80c252daec4ccb7776b4dac216ca9e3f/flake8_bandit-4.1.1-py3-none-any.whl", hash = "sha256:4c8a53eb48f23d4ef1e59293657181a3c989d0077c9952717e98a0eace43e06d", size = 4828, upload-time = "2022-08-29T13:48:39.737Z" }, +] + +[[package]] +name = "flake8-cognitive-complexity" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cognitive-complexity" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/d6/2bb09fab21521424d5afc836aa0057d15a92f5e738e506a3e3cb035be517/flake8_cognitive_complexity-0.1.0.tar.gz", hash = "sha256:f202df054e4f6ff182b659c261922b9c684628a47beb19cb0973c50d6a7831c1", size = 3061, upload-time = "2020-08-01T05:49:18.353Z" } + +[[package]] +name = "flake8-docstrings" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flake8" }, + { name = "pydocstyle" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/24/f839e3a06e18f4643ccb81370909a497297909f15106e6af2fecdef46894/flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af", size = 5995, upload-time = "2023-01-25T14:27:13.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/7d/76a278fa43250441ed9300c344f889c7fb1817080c8fb8996b840bf421c2/flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75", size = 4994, upload-time = "2023-01-25T14:27:12.32Z" }, +] + +[[package]] +name = "flake8-eradicate" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "eradicate" }, + { name = "flake8" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/72/a3975dfa4287396e9fb8fc2b4ee94a80d0809babbf92abed5af9c8e29c95/flake8_eradicate-1.5.0.tar.gz", hash = "sha256:aee636cb9ecb5594a7cd92d67ad73eb69909e5cc7bd81710cf9d00970f3983a6", size = 4508, upload-time = "2023-05-31T09:57:15.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/a9/1319b9e5eeb7d948f6db0b0ed4209bae0ec12d30ab3ee43a0ac1d8ce455f/flake8_eradicate-1.5.0-py3-none-any.whl", hash = "sha256:18acc922ad7de623f5247c7d5595da068525ec5437dd53b22ec2259b96ce9d22", size = 5144, upload-time = "2023-05-31T09:57:13.589Z" }, +] + +[[package]] +name = "flake8-functions" +version = "0.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mr-proper" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/f4/af475b75382a6403a6a184f01ae42e8c9bdd97ee8e84b4c4d6660da0f4d9/flake8_functions-0.0.8.tar.gz", hash = "sha256:5446626673a9faecbf389fb411b90bdc87b002c387b72dc097b208e7a58f2a1c", size = 5496, upload-time = "2023-04-10T15:53:16.099Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/1a/2ff8d074e42ad864fe5378cff28ccf758d4b039167c5384480b703aea222/flake8_functions-0.0.8-py3-none-any.whl", hash = "sha256:e1a88aa634d1aff6973f8c9dd64f30ab2beaac661e52eea96929ccc7ee7f64df", size = 6946, upload-time = "2023-04-10T15:53:14.448Z" }, +] + +[[package]] +name = "flake8-print" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flake8" }, + { name = "pycodestyle" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/a6/770c5832a6b563e023def7d81925d1b9f3079ebc805e48be0a5ee206f716/flake8-print-5.0.0.tar.gz", hash = "sha256:76915a2a389cc1c0879636c219eb909c38501d3a43cc8dae542081c9ba48bdf9", size = 5166, upload-time = "2022-04-30T16:19:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/2c/aa2ffda404b5d9c89dad8bcc4e0f4af673ab2de67e96997d13f04ad68b5b/flake8_print-5.0.0-py3-none-any.whl", hash = "sha256:84a1a6ea10d7056b804221ac5e62b1cee1aefc897ce16f2e5c42d3046068f5d8", size = 5687, upload-time = "2022-04-30T16:19:24.307Z" }, +] + +[[package]] +name = "flake8-pyproject" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flake8" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/6a/cdee9ff7f2b7c6ddc219fd95b7c70c0a3d9f0367a506e9793eedfc72e337/flake8_pyproject-1.2.4-py3-none-any.whl", hash = "sha256:ea34c057f9a9329c76d98723bb2bb498cc6ba8ff9872c4d19932d48c91249a77", size = 5694, upload-time = "2025-11-28T21:40:01.309Z" }, +] + +[[package]] +name = "flake8-pytest" +version = "1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flake8" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/aa/dfb17bc4660b4ef199c1cd9211a77456ee6d93b8bf79e6b8c46108435147/flake8-pytest-1.4.tar.gz", hash = "sha256:19f543b2d1cc89d61b76f19d0a9e58e9a110a035175f701b3425c363a7732c56", size = 3434, upload-time = "2022-08-08T13:47:21.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/ca/1baaca42e1ff50bc0758277e4e58452d3fd5f193418070830df90a07a10e/flake8_pytest-1.4-py2.py3-none-any.whl", hash = "sha256:97328f258ffad9fe18babb3b0714a16b121505ad3ac87d4e33020874555d0784", size = 3702, upload-time = "2022-08-08T13:47:19.321Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "isort" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "librt" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdformat" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/05/32b5e14b192b0a8a309f32232c580aefedd9d06017cb8fe8fce34bec654c/mdformat-1.0.0.tar.gz", hash = "sha256:4954045fcae797c29f86d4ad879e43bb151fa55dbaf74ac6eaeacf1d45bb3928", size = 56953, upload-time = "2025-10-16T12:05:03.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/9a/8fe71b95985ca7a4001effbcc58e5a07a1f2a2884203f74dcf48a3b08315/mdformat-1.0.0-py3-none-any.whl", hash = "sha256:bca015d65a1d063a02e885a91daee303057bc7829c2cd37b2075a50dbb65944b", size = 53288, upload-time = "2025-10-16T12:05:02.607Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mr-proper" +version = "0.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "setuptools" }, + { name = "stdlib-list" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/35/a50ba9e3097ee0d71232c996c626a17f80424140b365122c8f1bc9933118/mr_proper-0.0.7.tar.gz", hash = "sha256:03b517b19e617537f711ce418b125e5f2efd82ec881539cdee83195c78c14a02", size = 11028, upload-time = "2021-10-27T13:06:03.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/b0/2b19f1c38cc1ff29af21c1851c1160cfb185d4516834a37b7227d7df01b2/mr_proper-0.0.7-py3-none-any.whl", hash = "sha256:74a1b60240c46f10ba518707ef72811a01e5c270da0a78b5dd2dd923d99fdb14", size = 11537, upload-time = "2021-10-27T13:06:01.852Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, +] + +[[package]] +name = "pydocstyle" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "snowballstemmer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/d5385ca59fd065e3c6a5fe19f9bc9d5ea7f2509fa8c9c22fb6b2031dd953/pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1", size = 36796, upload-time = "2023-01-17T20:29:19.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/ea/99ddefac41971acad68f14114f38261c1f27dac0b3ec529824ebc739bdaa/pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", size = 38038, upload-time = "2023-01-17T20:29:18.094Z" }, +] + +[[package]] +name = "pydot" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/35/b17cb89ff865484c6a20ef46bf9d95a5f07328292578de0b295f4a6beec2/pydot-4.0.1.tar.gz", hash = "sha256:c2148f681c4a33e08bf0e26a9e5f8e4099a82e0e2a068098f32ce86577364ad5", size = 162594, upload-time = "2025-06-17T20:09:56.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl", hash = "sha256:869c0efadd2708c0be1f916eb669f3d664ca684bc57ffb7ecc08e70d5e93fee6", size = 37087, upload-time = "2025-06-17T20:09:55.25Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "stdlib-list" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/25/f1540879c8815387980e56f973e54605bd924612399ace31487f7444171c/stdlib_list-0.12.0.tar.gz", hash = "sha256:517824f27ee89e591d8ae7c1dd9ff34f672eae50ee886ea31bb8816d77535675", size = 60923, upload-time = "2025-10-24T19:21:22.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/3d/2970b27a11ae17fb2d353e7a179763a2fe6f37d6d2a9f4d40104a2f132e9/stdlib_list-0.12.0-py3-none-any.whl", hash = "sha256:df2d11e97f53812a1756fb5510393a11e3b389ebd9239dc831c7f349957f62f2", size = 87615, upload-time = "2025-10-24T19:21:20.619Z" }, +] + +[[package]] +name = "stevedore" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/88/35e4d27d9177d7df76d060e0a18f69c6c5794c96960c94042e20a12c8ba2/stevedore-5.8.0.tar.gz", hash = "sha256:b49867b32ca3016e94100e68dbf26e72aa7b8708d0a3f73c08aeb220370ac715", size = 514710, upload-time = "2026-05-18T09:15:27.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/ac/19f9941c74add59d17694930ec8105d5eddeee4ce56dd8632b765ca16d6c/stevedore-5.8.0-py3-none-any.whl", hash = "sha256:88eede9e66ca80e34085b9174e2327da2c61ac91f24f70e41c3ad76e4bb4872b", size = 54553, upload-time = "2026-05-18T09:15:25.82Z" }, +] + +[[package]] +name = "tach" +version = "0.34.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitpython" }, + { name = "networkx" }, + { name = "prompt-toolkit" }, + { name = "pydot" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tomli" }, + { name = "tomli-w" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/5b/a82de1482ede35e245951a6e039d608cdc86111aa8207344fffb8c927623/tach-0.34.1.tar.gz", hash = "sha256:58b5a8f9dd4f5c9fc9b1ade875aa0b31d3ac2f2f6802c655c05197a503d5acde", size = 765955, upload-time = "2026-04-03T06:12:28.192Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/3f/d071b841bf634da94068b84d7a2098c99c18deb21b3418969f29e152d793/tach-0.34.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6dff869e7f8933be23055f407a751e6f9e4c8ecb0bf3a93a962a0815e106c313", size = 4100880, upload-time = "2026-04-03T06:12:20.276Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/aeb7f67c34b5a9ee43d1aa556b16c6e60424cafeb3f23e6fbf3298a968cd/tach-0.34.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:70d55ec38dfcc15f1fff9e420ba17d63c36bb1b27f92abb8e8c910ffb71fd233", size = 3969195, upload-time = "2026-04-03T06:12:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/9b/de/12e565a0374436ba2b9c00ba68c3a9bf9999081ac546faf33da77162316e/tach-0.34.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:862d240a7fc297283a51c857eb269d8c56163f14b7e398f757b284bed1e3df4f", size = 4335854, upload-time = "2026-04-03T06:12:11.065Z" }, + { url = "https://files.pythonhosted.org/packages/24/f7/3c75beb144017a22229808b497bfb22cd79acc11d836c9870dc8708185a7/tach-0.34.1-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcb8ca825287160c1dd37b1cb164b88675e01ee97304ce14d50d7e8e1d3b853c", size = 4237244, upload-time = "2026-04-03T06:12:14.931Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/3459ce8c55a0aa4fa4533faf66cbbfef37eb5927aab1b87f8439c6db228b/tach-0.34.1-cp37-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b272ee726d8b0ef3887d3a3057f618922ab0f7471e1b1d6096f78f2ae93727e4", size = 4648719, upload-time = "2026-04-03T06:12:33.559Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d3/75b46da7c62a929ddde6c9eb4bb54f775693ee5a006f1b9a069a9aa2f5f2/tach-0.34.1-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6042b4f040c5941d28229963451b94a8cf2b08cdbb0ab0e338a04a46887c712f", size = 5203497, upload-time = "2026-04-03T06:12:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/12/f7/838d3c2bc0972626eddb3f69d13fdf3eaa7cc46c6132ad965fe7839fe452/tach-0.34.1-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b52a70262f16b701aaa03781656d808f88a3eb6a263762f8cea726ea9ffb980", size = 4349388, upload-time = "2026-04-03T06:12:26.288Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a0/25ac1fb225aa07faacb29c2470dc49633583fed11214c9e128eae65dece9/tach-0.34.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cba2d957371fde09c64cb1b70349844e33fae3d87f55e6b002929587c5c7826", size = 4593579, upload-time = "2026-04-03T06:12:39.73Z" }, + { url = "https://files.pythonhosted.org/packages/8c/54/29ffea4faf1c0c623bc2e97e8affc658fbaf38fcf8babf758fa63864dc1b/tach-0.34.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c600ea6f8748ea13183aaf802fb733c148238e252cc0e66a85ac8d6de9939906", size = 4513785, upload-time = "2026-04-03T06:12:13.145Z" }, + { url = "https://files.pythonhosted.org/packages/66/5e/c716a12ed58a7cc23b27f61f52e31e60410914b2a4afdb0b1a88d16008a7/tach-0.34.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:7be15f69c99b89cdadc2835368fc3cfb3ad45e74c7a704a7d42bcdd2917608b2", size = 4512695, upload-time = "2026-04-03T06:12:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/44/f6/8538205bba54d77d8b5e3f4afe8203d705bc430578f59bf4d108b3b6da47/tach-0.34.1-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:d917fc81adae85e9b3f2d29e135ba572a69dc677661727012b3adf95e0216e9e", size = 4649718, upload-time = "2026-04-03T06:12:37.908Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/340e9ba6821d4e97f1bfeea5ad36a07c888f3422bb68f4805c74c1dc576c/tach-0.34.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f94909630d699806e8682662962de0ac76ecc05dfcf7893af2c7e1a5c71de57e", size = 5333192, upload-time = "2026-04-03T06:12:24.289Z" }, + { url = "https://files.pythonhosted.org/packages/04/c2/d0e46185745af3a3d4057fd73eeac9ec003f054d5591c54e2fd835f4cbe4/tach-0.34.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1246b2718cfffecd4e6d66d6e232323fc4b3f2c68b4c990af0063464808f85fa", size = 4641868, upload-time = "2026-04-03T06:12:44.163Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d5/0209e74d57b0d32057b6ea997b77db847e38d649bd27e6261a4bd58e1da5/tach-0.34.1-cp37-abi3-win32.whl", hash = "sha256:abd9d1e468a9b8bec1e0037c06797b31e9c23c62adf13bcfacf057a21310ad3f", size = 3431420, upload-time = "2026-04-03T06:12:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6c/51f1d58eb3ebd1f79d2944f4813806c18c9c715178956424a4528e8bd281/tach-0.34.1-cp37-abi3-win_amd64.whl", hash = "sha256:0220c002da4bf4656b121c6317313490e8660627e5200251e0bca3e00a27d0c4", size = 3737055, upload-time = "2026-04-03T06:12:29.714Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "typer" +version = "0.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/c1/933d30fd7a123ed981e2a1eedafceab63cb379db0402e438a13bc51bbb15/typer-0.20.1.tar.gz", hash = "sha256:68585eb1b01203689c4199bc440d6be616f0851e9f0eb41e4a778845c5a0fd5b", size = 105968, upload-time = "2025-12-19T16:48:56.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/52/1f2df7e7d1be3d65ddc2936d820d4a3d9777a54f4204f5ca46b8513eff77/typer-0.20.1-py3-none-any.whl", hash = "sha256:4b3bde918a67c8e03d861aa02deca90a95bbac572e71b1b9be56ff49affdb5a8", size = 47381, upload-time = "2025-12-19T16:48:53.679Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260518" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/83/4a1afc3fbfcf5b8d46fc390cd95ed6b0dc9010a265f4e9f46314efffa37a/types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466", size = 17850, upload-time = "2026-05-18T06:01:58.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/a2/c01db32be2ae7d6a1689972f3c492b149ee4e164b12fdfd9f64b50888215/types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd", size = 20312, upload-time = "2026-05-18T06:01:57.368Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, +] diff --git a/workflows/polish.donna.md b/workflows/polish.donna.md index 5c79ad2b..58aebe80 100644 --- a/workflows/polish.donna.md +++ b/workflows/polish.donna.md @@ -16,7 +16,7 @@ goto_on_failure = "fix_tach" ```bash donna script #!/usr/bin/env bash -./bin/dev.sh poetry run tach check 2>&1 +./bin/dev.sh uv run tach check 2>&1 ``` ## Fix Tach Issues @@ -48,7 +48,7 @@ goto_on_failure = "fix_autoflake" ```bash donna script #!/usr/bin/env bash -./bin/dev.sh poetry run autoflake ./donna +./bin/dev.sh uv run autoflake ./donna ``` ## Fix Autoflake Issues @@ -79,7 +79,7 @@ goto_on_failure = "fix_isort" ```bash donna script #!/usr/bin/env bash -./bin/dev.sh poetry run isort ./donna +./bin/dev.sh uv run isort ./donna ``` ## Fix isort Issues @@ -110,7 +110,7 @@ goto_on_failure = "fix_black" ```bash donna script #!/usr/bin/env bash -./bin/dev.sh poetry run black ./donna +./bin/dev.sh uv run black ./donna ``` ## Fix Black Issues @@ -141,7 +141,7 @@ goto_on_failure = "fix_codespell" ```bash donna script #!/usr/bin/env bash -./bin/dev.sh poetry run codespell ./donna 2>&1 +./bin/dev.sh uv run codespell ./donna 2>&1 ``` ## Fix Codespell Issues @@ -172,7 +172,7 @@ goto_on_failure = "fix_flake8" ```bash donna script #!/usr/bin/env bash -./bin/dev.sh poetry run flake8 ./donna 2>&1 +./bin/dev.sh uv run flake8 ./donna 2>&1 ``` ## Fix Flake8 Issues @@ -210,7 +210,7 @@ goto_on_failure = "fix_mypy" ```bash donna script #!/usr/bin/env bash -./bin/dev.sh poetry run mypy ./donna +./bin/dev.sh uv run mypy ./donna ``` ## Fix Mypy Issues @@ -260,7 +260,7 @@ goto_on_failure = "fix_tests" ```bash donna script #!/usr/bin/env bash -./bin/dev.sh poetry run pytest donna -o cache_dir=/tmp/donna-pytest-cache 2>&1 +./bin/dev.sh uv run pytest donna -o cache_dir=/tmp/donna-pytest-cache 2>&1 ``` ## Fix Test Issues From 8760d8f296de1fc067caed49b7f4d5c06e2e7bb6 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 22:15:12 +0200 Subject: [PATCH 96/98] changes --- changes/unreleased.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/changes/unreleased.md b/changes/unreleased.md index a4b6cfc0..69b549d2 100644 --- a/changes/unreleased.md +++ b/changes/unreleased.md @@ -1,10 +1,10 @@ -**This release is dedicated to a full rethinking of what Donna is and how it works.** +**This release is a full rethinking of what Donna is and how it works.** -The scope of tool functionality was reduced to interpreting state machines for agents (no artifact management, no session management, etc.). +Donna's scope has been reduced to interpreting state machines for agents. Artifact management, session management, and related functionality have been removed. -That's why it is difficult to provide migration instructions or a proper list of changes. +Because of that, there is no reliable migration path and no useful item-by-item list of changes. -**Treat this version of Donna as a totally new tool** => read the documentation from scratch and adjust your usage accordingly. +**Treat this version of Donna as a new tool:** read the documentation from scratch and adjust your usage accordingly. -Sorry for the inconvenience. There should be no such breaking changes in the future. +Sorry for the disruption. Future releases should avoid breaking changes of this scale. From ebe1b764eba087563bb7767da879b1dd32b0a230 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 22:17:03 +0200 Subject: [PATCH 97/98] changes approved --- changes/{unreleased.md => next_release.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changes/{unreleased.md => next_release.md} (100%) diff --git a/changes/unreleased.md b/changes/next_release.md similarity index 100% rename from changes/unreleased.md rename to changes/next_release.md From 0ac2aaa8eb546333f2035290b70fcc68a22dba71 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 18 May 2026 22:23:58 +0200 Subject: [PATCH 98/98] spelling --- donna/machine/operations.py | 4 ++-- donna/machine/tests/test_operations.py | 4 ++-- donna/primitives/artifacts/tests/test_workflow.py | 2 +- donna/primitives/artifacts/workflow.py | 6 +++--- donna/primitives/directives/task_variable.py | 2 +- .../directives/tests/test_task_variable.py | 1 + donna/primitives/sections/finish_workflow.py | 2 +- donna/primitives/sections/output.py | 2 +- donna/primitives/sections/request_action.py | 2 +- donna/primitives/sections/run_script.py | 2 +- .../sections/tests/test_finish_workflow.py | 4 ++-- donna/primitives/sections/tests/test_output.py | 8 ++++---- .../primitives/sections/tests/test_request_action.py | 4 ++-- donna/primitives/sections/tests/test_run_script.py | 12 ++++++------ donna/runtime/sessions.py | 2 +- donna/runtime/tests/test_sessions.py | 2 +- 16 files changed, 30 insertions(+), 29 deletions(-) diff --git a/donna/machine/operations.py b/donna/machine/operations.py index 560f6de8..b9f3655e 100644 --- a/donna/machine/operations.py +++ b/donna/machine/operations.py @@ -27,7 +27,7 @@ class OperationConfig(ArtifactSectionConfig): class OperationMeta(ArtifactSectionMeta): fsm_mode: FsmMode = FsmMode.normal - allowed_transtions: set[SectionId] + allowed_transitions: set[SectionId] def cells_meta(self) -> Mapping[str, MetaValue]: - return {"fsm_mode": self.fsm_mode.value, "allowed_transtions": [str(t) for t in self.allowed_transtions]} + return {"fsm_mode": self.fsm_mode.value, "allowed_transitions": [str(t) for t in self.allowed_transitions]} diff --git a/donna/machine/tests/test_operations.py b/donna/machine/tests/test_operations.py index cb4cbdab..5bad3969 100644 --- a/donna/machine/tests/test_operations.py +++ b/donna/machine/tests/test_operations.py @@ -4,11 +4,11 @@ class TestOperationMeta: def test_cells_meta__serializes_fsm_mode_and_allowed_transitions(self) -> None: - meta = OperationMeta(fsm_mode=FsmMode.final, allowed_transtions={SectionId("next"), SectionId("done")}) + meta = OperationMeta(fsm_mode=FsmMode.final, allowed_transitions={SectionId("next"), SectionId("done")}) cell_meta = meta.cells_meta() assert cell_meta["fsm_mode"] == "final" - transitions = cell_meta["allowed_transtions"] + transitions = cell_meta["allowed_transitions"] assert isinstance(transitions, list) assert set(transitions) == {"next", "done"} diff --git a/donna/primitives/artifacts/tests/test_workflow.py b/donna/primitives/artifacts/tests/test_workflow.py index 59c12b50..05988a4a 100644 --- a/donna/primitives/artifacts/tests/test_workflow.py +++ b/donna/primitives/artifacts/tests/test_workflow.py @@ -39,7 +39,7 @@ def operation_section( kind=make.primitive_kind("donna.primitives.sections.text.Text"), meta=OperationMeta( fsm_mode=fsm_mode, - allowed_transtions=transitions or set(), + allowed_transitions=transitions or set(), ), ) diff --git a/donna/primitives/artifacts/workflow.py b/donna/primitives/artifacts/workflow.py index 0bb9cdba..14d4944c 100644 --- a/donna/primitives/artifacts/workflow.py +++ b/donna/primitives/artifacts/workflow.py @@ -97,7 +97,7 @@ def find_workflow_sections(start_operation_id: SectionId, artifact: Artifact) -> if not isinstance(section.meta, OperationMeta): continue - to_visit.extend(section.meta.allowed_transtions) + to_visit.extend(section.meta.allowed_transitions) return workflow_sections @@ -198,7 +198,7 @@ def validate_section( # noqa: CCR001, CFQ001 ) continue - if workflow_section.meta.fsm_mode == FsmMode.final and workflow_section.meta.allowed_transtions: + if workflow_section.meta.fsm_mode == FsmMode.final and workflow_section.meta.allowed_transitions: errors.append( FinalOperationHasTransitions( artifact_id=artifact.id, section_id=section_id, workflow_section_id=workflow_section.id @@ -209,7 +209,7 @@ def validate_section( # noqa: CCR001, CFQ001 if workflow_section.meta.fsm_mode == FsmMode.final: continue - if not workflow_section.meta.allowed_transtions: + if not workflow_section.meta.allowed_transitions: errors.append( NoOutgoingTransitions( artifact_id=artifact.id, section_id=section_id, workflow_section_id=workflow_section.id diff --git a/donna/primitives/directives/task_variable.py b/donna/primitives/directives/task_variable.py index 42bd6caf..0be0877c 100644 --- a/donna/primitives/directives/task_variable.py +++ b/donna/primitives/directives/task_variable.py @@ -41,7 +41,7 @@ def _prepare_arguments( def render_view(self, context: DirectiveContext, *argv: object) -> Result[object, ErrorsList]: variable_name = str(argv[0]) return Ok( - "$$donna at the time of execution of this section here will placed a value " + "$$donna at the time of execution of this section will place a value " f"of the task variable '{variable_name}' donna$$" ) diff --git a/donna/primitives/directives/tests/test_task_variable.py b/donna/primitives/directives/tests/test_task_variable.py index 0231b729..b2cb4d7a 100644 --- a/donna/primitives/directives/tests/test_task_variable.py +++ b/donna/primitives/directives/tests/test_task_variable.py @@ -27,6 +27,7 @@ def test_render_view__describes_deferred_variable_substitution(self) -> None: assert result.is_ok() content = result.unwrap() assert isinstance(content, str) + assert "will place a value" in content assert "answer" in content def test_render_execute__returns_value_from_task_context_mapping(self) -> None: diff --git a/donna/primitives/sections/finish_workflow.py b/donna/primitives/sections/finish_workflow.py index bf73dd11..38e24b32 100644 --- a/donna/primitives/sections/finish_workflow.py +++ b/donna/primitives/sections/finish_workflow.py @@ -44,4 +44,4 @@ def markdown_construct_meta( primary: bool = False, ) -> Result[ArtifactSectionMeta, ErrorsList]: finish_config = cast(FinishWorkflowConfig, section_config) - return Ok(OperationMeta(fsm_mode=finish_config.fsm_mode, allowed_transtions=set())) + return Ok(OperationMeta(fsm_mode=finish_config.fsm_mode, allowed_transitions=set())) diff --git a/donna/primitives/sections/output.py b/donna/primitives/sections/output.py index 7174227d..debf3df9 100644 --- a/donna/primitives/sections/output.py +++ b/donna/primitives/sections/output.py @@ -53,7 +53,7 @@ def markdown_construct_meta( return Ok( OutputMeta( fsm_mode=output_config.fsm_mode, - allowed_transtions=allowed_transitions, + allowed_transitions=allowed_transitions, next_operation_id=output_config.next_operation_id, ) ) diff --git a/donna/primitives/sections/request_action.py b/donna/primitives/sections/request_action.py index dbf1aa70..6b1d9f67 100644 --- a/donna/primitives/sections/request_action.py +++ b/donna/primitives/sections/request_action.py @@ -68,7 +68,7 @@ def markdown_construct_meta( return Ok( OperationMeta( fsm_mode=request_config.fsm_mode, - allowed_transtions=extract_transitions(analysis), + allowed_transitions=extract_transitions(analysis), ) ) diff --git a/donna/primitives/sections/run_script.py b/donna/primitives/sections/run_script.py index 8ccc327f..516e6e34 100644 --- a/donna/primitives/sections/run_script.py +++ b/donna/primitives/sections/run_script.py @@ -129,7 +129,7 @@ def markdown_construct_meta( return Ok( RunScriptMeta( fsm_mode=run_config.fsm_mode, - allowed_transtions=allowed_transitions, + allowed_transitions=allowed_transitions, script=script, save_stdout_to=run_config.save_stdout_to, save_stderr_to=run_config.save_stderr_to, diff --git a/donna/primitives/sections/tests/test_finish_workflow.py b/donna/primitives/sections/tests/test_finish_workflow.py index f98fe9bb..b00f1706 100644 --- a/donna/primitives/sections/tests/test_finish_workflow.py +++ b/donna/primitives/sections/tests/test_finish_workflow.py @@ -24,7 +24,7 @@ def test_markdown_construct_meta__creates_final_operation_without_transitions(se meta = result.unwrap() assert isinstance(meta, OperationMeta) assert meta.fsm_mode == FsmMode.final - assert meta.allowed_transtions == set() + assert meta.allowed_transitions == set() def test_execute_section__emits_message_and_finishes_task(self, mocker: MockerFixture) -> None: runtime_context = make.FakeRuntimeContext() @@ -35,7 +35,7 @@ def test_execute_section__emits_message_and_finishes_task(self, mocker: MockerFi id=make.section_id("done"), kind=make.primitive_kind("donna.primitives.sections.finish_workflow.FinishWorkflow"), description="Finished", - meta=OperationMeta(fsm_mode=FsmMode.final, allowed_transtions=set()), + meta=OperationMeta(fsm_mode=FsmMode.final, allowed_transitions=set()), ) ] ) diff --git a/donna/primitives/sections/tests/test_output.py b/donna/primitives/sections/tests/test_output.py index c7d1e7b2..fa460900 100644 --- a/donna/primitives/sections/tests/test_output.py +++ b/donna/primitives/sections/tests/test_output.py @@ -27,7 +27,7 @@ def test_markdown_construct_meta__records_next_operation_transition(self) -> Non assert isinstance(meta, OutputMeta) assert meta.fsm_mode == FsmMode.start assert meta.next_operation_id == make.section_id("next") - assert meta.allowed_transtions == {make.section_id("next")} + assert meta.allowed_transitions == {make.section_id("next")} def test_markdown_construct_meta__allows_missing_next_operation_for_validation(self) -> None: result = Output().markdown_construct_meta( @@ -44,7 +44,7 @@ def test_markdown_construct_meta__allows_missing_next_operation_for_validation(s meta = result.unwrap() assert isinstance(meta, OutputMeta) assert meta.next_operation_id is None - assert meta.allowed_transtions == set() + assert meta.allowed_transitions == set() def test_validate_section__requires_next_operation(self) -> None: artifact = machine_make.artifact( @@ -52,7 +52,7 @@ def test_validate_section__requires_next_operation(self) -> None: machine_make.artifact_section( id=make.section_id("start"), kind=make.primitive_kind("donna.primitives.sections.text.Text"), - meta=OutputMeta(allowed_transtions=set(), next_operation_id=None), + meta=OutputMeta(allowed_transitions=set(), next_operation_id=None), ) ] ) @@ -72,7 +72,7 @@ def test_execute_section__emits_message_and_adds_next_work_unit(self, mocker: Mo kind=make.primitive_kind("donna.primitives.sections.text.Text"), description="Agent message", meta=OutputMeta( - allowed_transtions={make.section_id("next")}, + allowed_transitions={make.section_id("next")}, next_operation_id=make.section_id("next"), ), ) diff --git a/donna/primitives/sections/tests/test_request_action.py b/donna/primitives/sections/tests/test_request_action.py index 856e2367..56ddd0b8 100644 --- a/donna/primitives/sections/tests/test_request_action.py +++ b/donna/primitives/sections/tests/test_request_action.py @@ -60,7 +60,7 @@ def test_markdown_construct_meta__uses_analysis_markdown_for_allowed_transitions assert result.is_ok() meta = result.unwrap() assert isinstance(meta, OperationMeta) - assert meta.allowed_transtions == {make.section_id("next")} + assert meta.allowed_transitions == {make.section_id("next")} def test_execute_section__adds_action_request_for_current_operation(self) -> None: artifact = machine_make.artifact( @@ -70,7 +70,7 @@ def test_execute_section__adds_action_request_for_current_operation(self) -> Non kind=make.primitive_kind("donna.primitives.sections.request_action.RequestAction"), title="Ask agent", description="Do the work", - meta=OperationMeta(fsm_mode=FsmMode.normal, allowed_transtions={make.section_id("next")}), + meta=OperationMeta(fsm_mode=FsmMode.normal, allowed_transitions={make.section_id("next")}), ) ] ) diff --git a/donna/primitives/sections/tests/test_run_script.py b/donna/primitives/sections/tests/test_run_script.py index 69334693..d5a76794 100644 --- a/donna/primitives/sections/tests/test_run_script.py +++ b/donna/primitives/sections/tests/test_run_script.py @@ -23,7 +23,7 @@ class TestRunScriptMeta: def test_select_next_operation__uses_success_transition_for_zero_exit_code(self) -> None: meta = RunScriptMeta( - allowed_transtions={make.section_id("next"), make.section_id("done")}, + allowed_transitions={make.section_id("next"), make.section_id("done")}, goto_on_success=make.section_id("next"), goto_on_failure=make.section_id("done"), ) @@ -32,7 +32,7 @@ def test_select_next_operation__uses_success_transition_for_zero_exit_code(self) def test_select_next_operation__uses_exit_code_specific_transition(self) -> None: meta = RunScriptMeta( - allowed_transtions={make.section_id("next"), make.section_id("done")}, + allowed_transitions={make.section_id("next"), make.section_id("done")}, goto_on_success=make.section_id("next"), goto_on_failure=make.section_id("done"), goto_on_code={"2": make.section_id("other")}, @@ -42,7 +42,7 @@ def test_select_next_operation__uses_exit_code_specific_transition(self) -> None def test_select_next_operation__falls_back_to_failure_transition(self) -> None: meta = RunScriptMeta( - allowed_transtions={make.section_id("next"), make.section_id("done")}, + allowed_transitions={make.section_id("next"), make.section_id("done")}, goto_on_success=make.section_id("next"), goto_on_failure=make.section_id("done"), ) @@ -86,7 +86,7 @@ def test_markdown_construct_meta__builds_script_meta_and_allowed_transitions(sel assert meta.save_stdout_to == "stdout" assert meta.save_stderr_to == "stderr" assert meta.timeout == 5 - assert meta.allowed_transtions == {make.section_id("next"), make.section_id("done"), make.section_id("other")} + assert meta.allowed_transitions == {make.section_id("next"), make.section_id("done"), make.section_id("other")} def test_markdown_construct_meta__requires_script_block(self) -> None: result = RunScript().markdown_construct_meta( @@ -109,7 +109,7 @@ def test_validate_section__collects_transition_config_errors(self) -> None: id=make.section_id("start"), kind=make.primitive_kind("donna.primitives.sections.run_script.RunScript"), meta=RunScriptMeta( - allowed_transtions={make.section_id("next")}, + allowed_transitions={make.section_id("next")}, goto_on_code={"not-an-int": make.section_id("next"), "0": make.section_id("done")}, ), ) @@ -138,7 +138,7 @@ def test_execute_section__stores_outputs_and_adds_selected_next_work_unit(self, kind=make.primitive_kind("donna.primitives.sections.run_script.RunScript"), title="Run checks", meta=RunScriptMeta( - allowed_transtions={ + allowed_transitions={ make.section_id("next"), make.section_id("done"), make.section_id("other"), diff --git a/donna/runtime/sessions.py b/donna/runtime/sessions.py index 678efe8a..090d0bc0 100644 --- a/donna/runtime/sessions.py +++ b/donna/runtime/sessions.py @@ -132,7 +132,7 @@ def _validate_operation_transition( assert isinstance(operation.meta, OperationMeta) - if next_operation_parts.section_id not in operation.meta.allowed_transtions: + if next_operation_parts.section_id not in operation.meta.allowed_transitions: return Err( [machine_errors.InvalidOperationTransition(operation_id=operation_id, next_operation_id=next_operation_id)] ) diff --git a/donna/runtime/tests/test_sessions.py b/donna/runtime/tests/test_sessions.py index da177d88..0c94645d 100644 --- a/donna/runtime/tests/test_sessions.py +++ b/donna/runtime/tests/test_sessions.py @@ -70,7 +70,7 @@ def _operation_section( title=f"Operation {id}", description=f"Description for {id}", primary=primary, - meta=OperationMeta(allowed_transtions=allowed_transitions or set()), + meta=OperationMeta(allowed_transitions=allowed_transitions or set()), )