Skip to content

api: handlers hold no logic, and one generated contract (plan 2.3, 2.4) - #91

Merged
radicalkjax merged 2 commits into
mainfrom
phase/api-contract-and-handlers
Sep 6, 2026
Merged

api: handlers hold no logic, and one generated contract (plan 2.3, 2.4)#91
radicalkjax merged 2 commits into
mainfrom
phase/api-contract-and-handlers

Conversation

@radicalkjax

Copy link
Copy Markdown
Member

Two plan items in one branch.

Handlers hold no logic (2.4, findings 11, 12, 13). The daemon's handlers held choreography that a second surface would have to repeat. Author handling, chat ingestion, bot status and memory summary, session listing and approval resolution move into runtime operations and are called from the handlers. The heartbeat becomes one operation that persists the interval and applies it to the monitor, with boot reading the stored value. Seven untyped JSON bodies become typed request structs with no silent defaults, and the events limit clamp moves into its operation. The desktop duplicates the plan mentions were already gone with the sidecar work, which was verified rather than assumed.

One generated contract (2.3, findings 6, 7). There was no machine-checkable agreement between the daemon, the command line and the two front ends. Now 138 handlers are annotated and 85 response types carry schemas, the document is served and can also be dumped without booting, and the frontend build generates TypeScript from it. The hand-maintained dashboard types shrink by about 200 lines as thirty wire shapes start coming from the generated schema. The command line emits shell completions and a man page from its build script.

The surface check script is in CI. Two deviations worth stating plainly. The command line has no machine-readable help, so the script reads the route literals out of the command sources rather than asking the binary. And the invariant does not hold yet: on its first run, of 125 routes, 78 had no command-line verb and 57 no provider method. Rather than declaring that green, the known gaps are listed in an exemptions file that works as a ratchet: a new gap fails the build, and so does a stale exemption. Closing those gaps is follow-up work the script now measures.

That first run also caught two routes under formations outside the allowed verb groups and eight duplicate operation identifiers, which is why every path now names its own.

One live bug found and fixed along the way: the safety disguise command took a positional boolean, which made any debug build panic when it was called. A test now asserts the command tree is valid.

Verification: builds and clippy -D warnings clean across runtime, daemon and command line; 325 tests green; cargo fmt clean; frontend typecheck, lint and build clean; the surface script passes.

🤖 Generated with Claude Code

https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8

2.4 — choreography the daemon's handlers were carrying moves into
`springtale-runtime::operations`, so a second surface cannot drift from
it. New operations: `authors` (the `trusted-author:` prefix and the
32-byte Ed25519 check in one place), `chat::ingest`, `bot::{status,
memory_summary}`, `sessions::list`, `approvals::{pending, resolve}`
through the runtime `ApprovalGate` trait, and `heartbeat::{get, set,
boot_interval}` — one operation that persists the interval and applies
it to the monitor, with boot reading the key back so a restart keeps the
setting. The events limit clamp moves into `operations::events::list`.
Rule create/update/delete already went through `rules::{create_and_
activate, update_and_reactivate, delete_and_deactivate}`; the desktop
holds no runtime any more, so the duplicates the plan names are gone.

Raw `serde_json::Value` bodies become typed request structs with no
silent defaults: `CreateFormationRequest` (intent and connectors are
required — a formation whose posture nobody chose is not a default),
`CompactMemoryRequest`, `Set/StepAutonomyRequest`, `AddAuthorRequest`,
`SetHeartbeatRequest`, `IncomingMessage`, `ResolveRequest`.

2.3 — 138 handlers carry `#[utoipa::path]` with a unique operation id,
85 response/request types derive `ToSchema`, and `GET /openapi.json`
serves the document unauthenticated (it is a schema, not data).
`springtaled --dump-openapi` prints it before any vault or store exists,
so CI regenerates it and diffs it against the committed copy.
`pnpm build` runs `openapi-typescript` over that copy into
`packages/types/src/api.ts`; 30 wire shapes drop out of the
hand-maintained `dashboard/types.ts`, which shrinks to the provider
contract and view models.

`scripts/check-surface.sh` proves every route has a command-line verb
and a provider method and that nothing under /formations sits outside
the four orchestration verb groups. An empty list is a failure, not a
pass. `springtale --help --json` cannot answer the CLI half — clap emits
no machine-readable help and a verb name does not carry its route — so
the check reads the CLI's own path literals instead.

The CLI's build script emits completions for five shells and a man page,
and every subcommand now honours `--json` through `output::emit`. That
work surfaced a real bug: `springtale safety disguise <bool>` panicked
in any debug build because a positional `bool` derives `SetTrue`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
Comment thread .github/workflows/ci.yml Fixed
Comment thread .github/workflows/ci.yml
permissions:
contents: read
steps:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2
Comment thread .github/workflows/ci.yml Fixed
Comment thread .github/workflows/ci.yml Fixed
…e uses

The new contract-regeneration job referenced two actions by moving tag, which
both workflow scanners reject: unpinned action reference, and mutable action
tag. Same digests as every other job in the file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
Comment thread .github/workflows/ci.yml
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
@radicalkjax
radicalkjax merged commit 52b7b3e into main Sep 6, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants