Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 11 additions & 56 deletions CLAUDE.md → AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# CLAUDE.md
# Repository Guidelines

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This file provides repository conventions for contributors.

## Project Overview

This is a **monorepo** for the FastAPI Startkit ecosystem — a modular, provider-driven framework for building Python applications with FastAPI. It contains four main components:

| Directory | Purpose | Published as |
Expand All @@ -14,9 +13,7 @@ This is a **monorepo** for the FastAPI Startkit ecosystem — a modular, provide
| `application/` | Starter application template | Not published — clone/scaffold target |

### `fastapi_startkit/` — Core Package

The PyPI package (`fastapi-startkit`, currently v0.13.6). Source lives under `src/fastapi_startkit/`. This is the foundational framework all other components depend on.

The PyPI package `fastapi-startkit`. Source lives under `src/fastapi_startkit/`. This is the foundational framework all other components depend on.
**Do not modify framework code unless explicitly necessary.** Changes to core abstractions (Container, Application, Model, Provider, Facades) can have broad breaking effects on downstream applications.

Optional extras are installed with pip/uv extras:
Expand Down Expand Up @@ -49,53 +46,11 @@ Self-contained apps demonstrating specific features. Each subdirectory is an ind

The template users clone when starting a new project. Contains the minimal scaffolding: `artisan` entrypoint, `bootstrap/`, `config/`, `providers/`, `routes/`, and `storage/`. It mirrors a typical project layout and is a uv workspace member of this monorepo.

## Task Tracking (Keera Agent MCP)

Planned work for this project is tracked in **Keera Agent** via an MCP server running locally at `http://127.0.0.1:4545`.

### Load tasks at the start of a session

```bash
# List all tasks for this project
curl -s -X POST http://127.0.0.1:4545/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list_tasks","arguments":{"project_path":"/Users/ellite/code/packages/fastapi-startkit-framework/fastapi_startkit"}},"id":1}'
```

Or open the Keera Agent UI at: `http://127.0.0.1:4545/framework`

### Available MCP tools
## Coding Standards

| Tool | Purpose |
|---|---|
| `list_tasks` | List tasks (filter by `status`: pending / in_progress / completed / cancelled) |
| `get_task` | Get full details of a task by numeric ID |
| `create_task` | Create a new task with title, description, acceptance criteria, testing methods, and validation steps |
| `update_task` | Update any field of a task |
| `update_task_status` | Change a task's status |
| `send_message_to_agent` | Send a message to another project's agent |
| `get_agent_messages` | Read messages in this project's agent inbox |

### MCP JSON-RPC usage

All calls follow the JSON-RPC 2.0 protocol — `POST http://127.0.0.1:4545/mcp` with `Content-Type: application/json`:

```bash
# Initialize (once per session)
curl -s -X POST http://127.0.0.1:4545/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"claude-code","version":"1.0"}},"id":0}'

# Call a tool
curl -s -X POST http://127.0.0.1:4545/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"<tool_name>","arguments":{...}},"id":1}'
```

The `project_path` for this repo is always:
```
/Users/ellite/code/packages/fastapi-startkit-framework/fastapi_startkit
```
1. Do not add explanatory comments or docstrings. Express intent through clear names, arguments, and structure. Behavioural directives required by tooling are allowed.
2. Before implementing a change, write an architectural decision record in `docs/adr/`. Explain the problem, alternatives, chosen design, implementation details, and validation plan.
3. Add each ADR to `docs/adr/AGENTS.md` with its date, title, and a brief explanation of the decision. Keep the index sufficient to identify relevant records without reading every ADR.

## Commands

Expand All @@ -107,10 +62,10 @@ uv sync
cd fastapi_startkit && uv build

# Run framework tests
uv run pytest fastapi_startkit/src/fastapi_startkit/tests/ -v
cd fastapi_startkit && uv run pytest tests/ -v

# Run a single test file
uv run pytest fastapi_startkit/src/fastapi_startkit/tests/configurations/test_config_merge.py -v
cd fastapi_startkit && uv run pytest tests/core/test_configuration.py -v

# Serve the docs locally
cd fastapi_startkit.github.io.git && npm run dev
Expand Down Expand Up @@ -151,7 +106,7 @@ Configured in `pyproject.toml` under `[tool.coverage.*]`:
|---|---|
| Source tracked | `src/fastapi_startkit/` |
| Omitted | `*/tests/*`, `*/migrations/*`, `*/__init__.py`, `*.pyi` |
| Minimum threshold | `fail_under = 40` |
| Minimum threshold | `fail_under = 80` |
| HTML output dir | `htmlcov/` |

The test suite **fails** if total coverage drops below the `fail_under` threshold. Raise this value in `pyproject.toml` as coverage improves.
Expand Down Expand Up @@ -246,7 +201,7 @@ Async-first fork of Masonite ORM built on SQLAlchemy async:
- `Model` auto-pluralizes table names via `inflection`
- `created_at`/`updated_at` managed as `pendulum` Carbon objects
- Relationships: `HasOne`, `HasMany`, `BelongsTo`, `BelongsToMany`, `HasOneThrough`
- `AsyncQueryBuilder` provides the chainable query interface
- `QueryBuilder` provides the chainable query interface

### Facades (`facades/`)

Expand Down
68 changes: 68 additions & 0 deletions docs/adr/001-masonite-orm-aftercommit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# 001: ORM after-commit callbacks

Date: 2026-10-01
Status: Accepted

## Problem

Applications often need to run follow-up work, such as sending a notification, only once a database change is durable. Model observers (`created`, `updated`, `saved`) fire while the model operation is still running, so they can fire before the surrounding transaction commits. The ORM offers explicit transactions and transaction context managers, but no way to hook into a commit.

Repository conventions are also moving into `AGENTS.md`, and ADRs need a concise index.

## Alternatives

- **Fire model observers after commit.** This changes the timing of existing observers and does not cover raw queries.
- **Use SQLAlchemy engine events.** Commit events fire before the commit completes and cannot await asynchronous callbacks.
- **Track callbacks in the ORM transaction lifecycle (chosen).** This covers every public transaction API and can await callbacks once the commit has succeeded.

## Decision

Add `await connection.after_commit(callback)` and `await DB.after_commit(callback, name=None)`. A callback takes no arguments and may be synchronous or return an awaitable. Callers bind arguments with a closure or `functools.partial`. Non-callables are rejected at registration.

Behaviour:

- **Active transaction:** the callback is queued.
- **No transaction (or one the ORM did not start):** the callback runs and is awaited immediately.
- **Outermost commit:** queued callbacks run sequentially in registration order.
- **Nested commit (savepoint):** callbacks merge into the parent and do not run yet.
- **Rollback:** a nested rollback discards only that scope's callbacks; an outer rollback discards everything.
- **Close, reconnect, cancellation, failed commit:** no callbacks survive into later transactions.
- **Callback errors:** the error propagates to the caller. The commit stays durable, remaining callbacks are skipped, and the queue is already cleared.

The root connection is released before callbacks run, so they can read committed data or open a new transaction, even with a pool of one connection.

This is an in-process hook, not a durable delivery guarantee. Work that must not be lost should use an outbox.

## Usage

```python
from fastapi_startkit.masoniteorm.facades import DB

async with DB.connection().transaction():
await DB.table("users").where("id", user_id).update({"verified": True})
await DB.after_commit(lambda: send_confirmation(user_id))
```

`send_confirmation` runs after the transaction exits successfully and is skipped on rollback.

## Implementation

The ORM `Connection` keeps a registry of callback frames, keyed by the SQLAlchemy connection and transaction, with each frame linking to its parent. This follows the existing ContextVar connection propagation: tasks that inherit the context share the current transaction, while independent tasks and named connections use separate physical connections and queues. SQLAlchemy's `connection.info` is avoided because reading it during invalidation can force a DBAPI reconnect and block cleanup.

A transaction the ORM did not register (for example one begun directly on the raw SQLAlchemy connection) is treated as untracked: callbacks registered inside it run immediately, and an ORM savepoint inside it acts as its own root.

Frames are created, merged and discarded in `Connection.begin_transaction`, `commit_transaction` and `rollback`, and in the `Transaction` enter, exit, commit and rollback paths. Root batches are detached before they run, and closing a connection clears its frames.

Repository guidance moves into `AGENTS.md` with corrected wording and test paths, and `docs/adr/AGENTS.md` becomes the ADR index.

## Validation

Real SQLite integration tests cover:

- sync and async callbacks, registration order, and visibility of committed data
- manual, context-manager and transaction-object APIs, including mixed use
- nested and deeply nested savepoints, commit and rollback
- callback errors, failed commits, cancellation, close and reconnect, and invalidated connections
- task isolation, named facade connections, callbacks that start new transactions, and untracked transactions

Existing SQLite transaction tests, Ruff, and the framework type checker also run.
10 changes: 10 additions & 0 deletions docs/adr/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
title: Architectural Decision Records
description: Index of architectural decisions, their dates, and the reasons for each implementation.
---

Read this index first, then open the records relevant to the change. Before implementation, add or update an ADR explaining the problem, alternatives, decision, implementation, and validation. Keep this index current.

| Index | Date | Title | Abstract |
| --- | --- | --- | --- |
| [001](001-masonite-orm-aftercommit.md) | 2026-10-01 | ORM after-commit callbacks | Queue sync and async callbacks on the active database transaction, defer nested callbacks until the outer commit, and discard callbacks on rollback. |
Loading
Loading