enlace 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. enlace-0.0.2/.claude/CLAUDE.md +113 -0
  2. enlace-0.0.2/.claude/skills/enlace/SKILL.md +249 -0
  3. enlace-0.0.2/.claude/skills/enlace/evals/evals.json +23 -0
  4. enlace-0.0.2/.claude/skills/enlace-dev/SKILL.md +169 -0
  5. enlace-0.0.2/.claude/skills/enlace-dev/evals/evals.json +17 -0
  6. enlace-0.0.2/.claude/skills/enlace-diagnose/SKILL.md +275 -0
  7. enlace-0.0.2/.github/workflows/ci.yml +256 -0
  8. enlace-0.0.2/.gitignore +13 -0
  9. enlace-0.0.2/PKG-INFO +239 -0
  10. enlace-0.0.2/README.md +220 -0
  11. enlace-0.0.2/enlace/__init__.py +35 -0
  12. enlace-0.0.2/enlace/__main__.py +242 -0
  13. enlace-0.0.2/enlace/base.py +165 -0
  14. enlace-0.0.2/enlace/compose.py +180 -0
  15. enlace-0.0.2/enlace/data/skills/enlace/SKILL.md +249 -0
  16. enlace-0.0.2/enlace/data/skills/enlace/evals/evals.json +23 -0
  17. enlace-0.0.2/enlace/data/skills/enlace-dev/SKILL.md +169 -0
  18. enlace-0.0.2/enlace/data/skills/enlace-dev/evals/evals.json +17 -0
  19. enlace-0.0.2/enlace/data/skills/enlace-diagnose/SKILL.md +275 -0
  20. enlace-0.0.2/enlace/diagnose.py +1102 -0
  21. enlace-0.0.2/enlace/discover.py +286 -0
  22. enlace-0.0.2/enlace/serve.py +123 -0
  23. enlace-0.0.2/enlace/tests/__init__.py +0 -0
  24. enlace-0.0.2/enlace/tests/conftest.py +98 -0
  25. enlace-0.0.2/enlace/tests/test_compose.py +116 -0
  26. enlace-0.0.2/enlace/tests/test_discover.py +239 -0
  27. enlace-0.0.2/enlace/util.py +38 -0
  28. enlace-0.0.2/misc/docs/asgi_composition__ASGI_composition_and_dynamic_sub_application_mounting_in_FastAPI_or_Starlette.md +520 -0
  29. enlace-0.0.2/misc/docs/auth_cross_cutting__Authentication_and_Authorization_as_Cross_Cutting_Concerns.md +669 -0
  30. enlace-0.0.2/misc/docs/convention_over_configuration__Convention_over_Configuration_in_Python.md +761 -0
  31. enlace-0.0.2/misc/docs/deployment_observability__Deployment_process_management_hot_reload_and_observability_for_a_personal_app_platform.md +755 -0
  32. enlace-0.0.2/misc/docs/design_principles__Zero_coupling_and_standalone_preservation_in_multi_app_composition.md +206 -0
  33. enlace-0.0.2/misc/docs/enlace_spec.md +782 -0
  34. enlace-0.0.2/misc/docs/frontend_serving__Multi_app_frontend_serving_under_a_single_domain.md +439 -0
  35. enlace-0.0.2/misc/docs/user_data_persistence__Multi_app_user_data_persistence_with_repository_abstractions.md +341 -0
  36. enlace-0.0.2/pyproject.toml +58 -0
  37. enlace-0.0.2/tests/__init__.py +0 -0
  38. enlace-0.0.2/tests/test_platform.py +98 -0
@@ -0,0 +1,113 @@
1
+ # enlace — Agent Instructions
2
+
3
+ ## Core Principles
4
+
5
+ These two principles govern ALL changes to enlace. Every PR, feature, and
6
+ suggestion must respect both. They are in tension — the design challenge is
7
+ balancing them.
8
+
9
+ ### 1. Apps should not need to change
10
+
11
+ enlace wraps apps from the outside. Apps don't import enlace, don't depend
12
+ on it, and don't know it exists. All aggregation logic (discovery, routing,
13
+ CORS, static serving) lives in enlace, not in the app.
14
+
15
+ When enlace encounters an app that's hard to mount:
16
+ - **First**: solve it on the enlace side (app.toml config, env vars, middleware)
17
+ - **Second**: suggest a minimal app change that preserves standalone operation
18
+ - **Last resort**: suggest an app change that breaks standalone — but flag it explicitly
19
+
20
+ ### 2. Enlaced apps must still work alone
21
+
22
+ An app that works standalone today must still work standalone after being
23
+ enlaced. When we suggest changes to an app, those changes MUST preserve
24
+ the app's ability to run independently.
25
+
26
+ The pattern: **env-var with current value as default**.
27
+
28
+ ```python
29
+ # GOOD: works standalone AND under enlace
30
+ import os
31
+ if not os.environ.get('ENLACE_MANAGED'):
32
+ app.add_middleware(CORSMiddleware, ...)
33
+
34
+ # BAD: breaks standalone
35
+ # (CORSMiddleware deleted entirely)
36
+ ```
37
+
38
+ ```typescript
39
+ // GOOD: standalone uses localhost, enlace overrides at build time
40
+ const API_BASE = process.env.NEXT_PUBLIC_API_BASE || "http://localhost:8000/api";
41
+
42
+ // BAD: breaks standalone
43
+ const API_BASE = "/api/s_conditions";
44
+ ```
45
+
46
+ ### Zero coupling
47
+
48
+ Apps do not import enlace. The dependency graph:
49
+ - enlace depends on FastAPI, Uvicorn, Pydantic, argh
50
+ - Apps depend on their own domain libs (FastAPI, numpy, etc.)
51
+ - There is NO dependency from apps to enlace
52
+
53
+ enlace provides services to apps via:
54
+ - ASGI scope injection (auth, store) — apps read `request.state.store`, not `import enlace.stores`
55
+ - Environment variables (`ENLACE_MANAGED`) — apps can condition on this but don't have to
56
+ - Convention (filesystem layout, app.toml) — external to app code
57
+
58
+ ## Architecture
59
+
60
+ Read `misc/docs/enlace_spec.md` for the full architecture and design rationale.
61
+
62
+ ```
63
+ enlace/
64
+ ├── base.py # Pydantic models: AppConfig, PlatformConfig, ConventionsConfig
65
+ ├── util.py # Pure helpers: derive_display_name, derive_route_prefix, is_skippable
66
+ ├── discover.py # ConventionDiscoverer: walks apps/, detects types, loads TOML
67
+ ├── compose.py # build_backend(): mounts sub-apps, cascade_lifespan, sets ENLACE_MANAGED
68
+ ├── diagnose.py # diagnose_app(): scan an app dir for enlace compatibility issues
69
+ ├── serve.py # Uvicorn subprocess orchestration, signal forwarding
70
+ ├── __main__.py # CLI via argh.dispatch_commands
71
+ ├── __init__.py # Public API facade
72
+ └── tests/ # Unit tests (test_discover.py, test_compose.py)
73
+ ```
74
+
75
+ **Data flow:** `PlatformConfig.from_toml()` → `ConventionDiscoverer.discover()` →
76
+ `config.check_conflicts()` → `build_backend(config)` → `uvicorn --factory`
77
+
78
+ ## Before Making Changes
79
+
80
+ - Run `enlace show-config` to understand current state
81
+ - Run `enlace check` to validate config before and after changes
82
+
83
+ ## Critical Rules
84
+
85
+ - **NEVER use `BaseHTTPMiddleware`** — it has terminal bugs (exception swallowing, ContextVar corruption). Use pure ASGI middleware (three-callable pattern) only.
86
+ - **Mount on `FastAPI()` directly**, never on `APIRouter` — known framework bug.
87
+ - **Discovery must never silently swallow ImportErrors** — distinguish "module doesn't exist" from "module has broken import".
88
+ - **All middleware must be pure ASGI** (scope, receive, send pattern).
89
+ - **Conflict detection is fail-fast** — report ALL conflicts, don't stop at first.
90
+ - **CORS on parent only** — sub-apps must not add their own CORSMiddleware. If they do, enlace still works (MEDIUM issue, not a blocker), but the diagnostic flags it.
91
+ - **`ENLACE_MANAGED=1`** — set by `build_backend()` so sub-apps can condition on it.
92
+ - **Suggestions to app developers must preserve standalone operation** — use the env-var-with-default pattern, never suggest changes that break the app's ability to run alone.
93
+
94
+ ## Research Docs
95
+
96
+ Consult these before modifying subsystems:
97
+
98
+ | Subsystem | Document |
99
+ |-----------|----------|
100
+ | App mounting, middleware | `misc/docs/asgi_composition__*.md` |
101
+ | Auth middleware | `misc/docs/auth_cross_cutting__*.md` |
102
+ | Discovery, config | `misc/docs/convention_over_configuration__*.md` |
103
+ | Deployment, logging | `misc/docs/deployment_observability__*.md` |
104
+ | Frontend serving | `misc/docs/frontend_serving__*.md` |
105
+ | Data persistence | `misc/docs/user_data_persistence__*.md` |
106
+ | Design principles | `misc/docs/design_principles__*.md` |
107
+
108
+ ## Testing
109
+
110
+ ```bash
111
+ pytest enlace/tests/ # Unit tests
112
+ pytest tests/ # Integration tests
113
+ ```
@@ -0,0 +1,249 @@
1
+ ---
2
+ name: enlace
3
+ description: >
4
+ Use when working with the enlace multi-app platform — creating apps, configuring
5
+ platform.toml or app.toml, diagnosing discovery issues, understanding conventions,
6
+ or serving/deploying apps. Triggers on: enlace CLI commands, apps/ directory
7
+ structures, platform.toml/app.toml files, multi-app ASGI composition, or when the
8
+ user mentions enlace, app discovery, app mounting, or personal app platform.
9
+ ---
10
+
11
+ # enlace — Multi-App Platform
12
+
13
+ enlace discovers Python modules and React apps in a directory, mounts them as
14
+ sub-applications under a single ASGI server, and serves them.
15
+
16
+ **enlace is not a framework.** Apps don't import it, don't depend on it, and
17
+ don't know it exists. You write a standard FastAPI app (or plain Python
18
+ functions with type hints). enlace discovers it from the outside, mounts it
19
+ alongside other apps, and serves them all — without touching your code.
20
+
21
+ **Two principles:**
22
+ 1. **Apps should not need to change** — all aggregation logic lives in enlace.
23
+ When an app is hard to mount, prefer enlace-side config (app.toml, env vars)
24
+ over app code changes.
25
+ 2. **Enlaced apps must still work alone** — if we do suggest changes, they must
26
+ preserve standalone operation. Pattern: env-var with current value as default.
27
+
28
+ ## Core Concept
29
+
30
+ Drop files in `apps/`, enlace finds and serves them:
31
+
32
+ ```
33
+ apps/
34
+ ├── my_tool/
35
+ │ └── server.py # has `app = FastAPI()` → served at /api/my_tool
36
+ ├── dashboard/
37
+ │ ├── server.py # backend
38
+ │ └── frontend/
39
+ │ └── index.html # SPA assets
40
+ ├── calculator/
41
+ │ └── server.py # has typed functions, no `app` → auto-wrapped
42
+ └── blog/
43
+ └── frontend/
44
+ └── index.html # frontend-only, no backend
45
+ ```
46
+
47
+ Everything enlace infers is inspectable (`enlace show-config`) and overridable
48
+ (via TOML or CLI flags).
49
+
50
+ ## CLI Commands
51
+
52
+ ```bash
53
+ enlace serve # Start backend (dev mode, hot reload)
54
+ enlace serve --mode prod # Production mode (2 workers)
55
+ enlace serve --port 9000 # Custom port
56
+ enlace serve --app-dirs "/a,/b" # Serve apps from specific directories
57
+ enlace show-config # Resolved config with provenance
58
+ enlace show-config --json # Machine-readable
59
+ enlace show-config --verbose # Show where each value came from
60
+ enlace check # Validate config, check route conflicts
61
+ enlace list-apps # Table: name, route, type, access
62
+ ```
63
+
64
+ ## Creating an App
65
+
66
+ ### Standalone ASGI App (most common)
67
+
68
+ Create `apps/{name}/server.py` with an `app` attribute:
69
+
70
+ ```python
71
+ # apps/my_tool/server.py
72
+ from fastapi import FastAPI
73
+
74
+ app = FastAPI()
75
+
76
+ @app.get("/hello")
77
+ def hello():
78
+ return {"message": "Hello from my_tool"}
79
+ ```
80
+
81
+ This gets mounted at `/api/my_tool/`. The `app` attribute name is configurable.
82
+
83
+ ### Function Collection (no FastAPI needed)
84
+
85
+ If the entry module has typed public functions but no `app` attribute, enlace
86
+ auto-wraps them as endpoints:
87
+
88
+ ```python
89
+ # apps/calculator/server.py
90
+ def add(a: int, b: int) -> dict:
91
+ return {"result": a + b}
92
+
93
+ def multiply(a: float, b: float) -> dict:
94
+ return {"result": a * b}
95
+ ```
96
+
97
+ Functions become POST endpoints: `/api/calculator/add`, `/api/calculator/multiply`.
98
+ Simple-type parameters are query params.
99
+
100
+ ### Frontend-Only App
101
+
102
+ Just a `frontend/` directory with `index.html`:
103
+
104
+ ```
105
+ apps/blog/
106
+ └── frontend/
107
+ └── index.html
108
+ ```
109
+
110
+ No backend mounted. Assets served at `/apps/blog/` (in production via Caddy).
111
+
112
+ ## Discovery Conventions
113
+
114
+ | What | Convention | Override key in `app.toml` |
115
+ |------|-----------|---------------------------|
116
+ | Route prefix | `/api/{directory_name}` | `route` |
117
+ | Backend entry | First of `server.py`, `app.py`, `main.py` | `entry_point` |
118
+ | ASGI app object | Attribute named `app` | `app_attr` |
119
+ | Frontend assets | `frontend/` with `index.html` | `frontend_dir` |
120
+ | Display name | Dir name, `_` → space, title-cased | `display_name` |
121
+ | Access level | `local` (default) | `access` |
122
+ | Skip directory | Starts with `_` or `.` | — |
123
+
124
+ ## Multi-Source App Discovery
125
+
126
+ enlace can discover apps from multiple locations — you don't need to move
127
+ existing projects into a single `apps/` folder.
128
+
129
+ Two source types:
130
+ - **`apps_dirs`**: directories that CONTAIN app subdirectories (walk children)
131
+ - **`app_dirs`**: individual directories that ARE apps (discover directly)
132
+
133
+ ### Enlacing existing projects
134
+
135
+ ```toml
136
+ # platform.toml
137
+ [platform]
138
+ apps_dirs = ["apps"]
139
+ app_dirs = [
140
+ "/Users/thor/projects/chord_analyzer",
141
+ "/Users/thor/projects/todo_app",
142
+ ]
143
+ ```
144
+
145
+ Or via CLI:
146
+ ```bash
147
+ enlace serve --app-dirs "/path/to/chord_analyzer,/path/to/todo_app"
148
+ enlace serve --apps-dirs "apps,/path/to/more_apps"
149
+ ```
150
+
151
+ Symlinks also work: `ln -s /path/to/my_app apps/my_app` and enlace discovers
152
+ it transparently.
153
+
154
+ App names must be globally unique across all sources. Duplicates are caught
155
+ by `enlace check`.
156
+
157
+ ## Configuration
158
+
159
+ ### `platform.toml` (global, in project root)
160
+
161
+ ```toml
162
+ [platform]
163
+ apps_dirs = ["apps"] # container directories (walk children)
164
+ app_dirs = [] # individual app directories
165
+ domain = "localhost"
166
+ backend_port = 8000
167
+
168
+ [conventions]
169
+ entry_points = ["server.py", "app.py", "main.py"]
170
+ app_attr = "app"
171
+ frontend_dir = "frontend"
172
+ ```
173
+
174
+ The legacy `apps_dir = "apps"` (scalar) is still supported for backward compat.
175
+
176
+ ### `app.toml` (per-app, in app directory)
177
+
178
+ ```toml
179
+ route = "/api/custom-route"
180
+ access = "public"
181
+ display_name = "My Custom App"
182
+ entry_point = "application.py"
183
+ app_attr = "my_app"
184
+ ```
185
+
186
+ ### Override Precedence (lowest → highest)
187
+
188
+ ```
189
+ hardcoded defaults → filesystem conventions → app.toml → platform.toml → env vars → CLI flags
190
+ ```
191
+
192
+ Environment variables: `ENLACE_APPS_DIRS`, `ENLACE_APP_DIRS` (pathsep-delimited).
193
+
194
+ ## App Types
195
+
196
+ | Type | Detection | Mounting |
197
+ |------|-----------|---------|
198
+ | `asgi_app` | Module has `app` attribute (callable) | `parent.mount(prefix, sub_app)` |
199
+ | `functions` | No `app` attr, has typed public functions | Auto-wrapped as APIRouter |
200
+ | `frontend_only` | No backend entry, has `frontend/index.html` | Static file serving only |
201
+
202
+ ## Diagnosing Issues
203
+
204
+ **App not discovered?**
205
+ 1. Run `enlace show-config --verbose` — is the app listed?
206
+ 2. Check directory isn't prefixed with `_` or `.`
207
+ 3. Check entry point file exists (`server.py`, `app.py`, or `main.py`)
208
+ 4. Check for import errors — enlace intentionally surfaces them (won't silently skip)
209
+
210
+ **Route conflict?**
211
+ - `enlace check` reports ALL conflicts at once with both app names
212
+ - Fix by changing `route` in one app's `app.toml`
213
+
214
+ **Wrong app type detected?**
215
+ - Run `enlace show-config --verbose` to see detection reason
216
+ - Ensure your `app = FastAPI()` is at module level (not inside a function)
217
+ - For function collections, ensure functions have type annotations
218
+
219
+ ## Access Levels
220
+
221
+ | Level | Description |
222
+ |-------|-------------|
223
+ | `local` | Development only (default) |
224
+ | `public` | Open to everyone |
225
+ | `protected:shared` | Single shared password gate |
226
+ | `protected:user` | Per-user accounts |
227
+
228
+ Set in `app.toml` with `access = "protected:shared"` etc.
229
+
230
+ ## Workflow
231
+
232
+ ```bash
233
+ # 1. Initialize (creates platform.toml + apps/)
234
+ enlace init
235
+
236
+ # 2. Create an app
237
+ mkdir -p apps/my_app
238
+ # Write apps/my_app/server.py
239
+
240
+ # 3. Verify discovery
241
+ enlace show-config
242
+ enlace check
243
+
244
+ # 4. Serve
245
+ enlace serve
246
+ # → http://localhost:8000/api/my_app/
247
+ ```
248
+
249
+ Always run `enlace check` after making changes to catch conflicts early.
@@ -0,0 +1,23 @@
1
+ {
2
+ "skill_name": "enlace",
3
+ "evals": [
4
+ {
5
+ "id": 1,
6
+ "prompt": "I want to add a new app to my enlace platform that exposes a couple of Python functions as API endpoints -- one that takes a text string and returns the word count, and another that reverses the string. I don't want to deal with FastAPI boilerplate, just the functions. Set it up for me in apps/text_utils/",
7
+ "expected_output": "Creates apps/text_utils/server.py with two typed functions (word_count and reverse_text), no FastAPI app object. The agent understands this will be auto-detected as a 'functions' type app and served at /api/text_utils/.",
8
+ "files": []
9
+ },
10
+ {
11
+ "id": 2,
12
+ "prompt": "I'm getting an error when I run enlace serve -- it says route conflict. I have three apps in my apps directory: todo, notes, and dashboard. The dashboard app has an app.toml that sets route = '/api/notes'. Can you help me figure out what's going on and fix it?",
13
+ "expected_output": "The agent identifies the conflict between the 'notes' app (convention route /api/notes) and 'dashboard' app (overridden to /api/notes via app.toml). Suggests running enlace check, then fixes dashboard's app.toml to use a different route like /api/dashboard or /api/dash.",
14
+ "files": []
15
+ },
16
+ {
17
+ "id": 3,
18
+ "prompt": "I need to set up a new enlace project from scratch. I want two apps: a chord_analyzer with a FastAPI backend and a React frontend, and a simple notes app that's backend-only. The chord_analyzer should be public access and the notes app should be protected with a shared password. Walk me through the whole setup.",
19
+ "expected_output": "The agent creates the full directory structure: platform.toml with conventions, apps/chord_analyzer/server.py with FastAPI app, apps/chord_analyzer/frontend/ placeholder, apps/chord_analyzer/app.toml with access=public, apps/notes/server.py with FastAPI app, apps/notes/app.toml with access=protected:shared. Mentions running enlace show-config to verify.",
20
+ "files": []
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,169 @@
1
+ ---
2
+ name: enlace-dev
3
+ description: >
4
+ Use when developing or modifying the enlace package itself — adding features,
5
+ fixing bugs, extending the ASGI composition, adding middleware, or working on
6
+ any module in the enlace/ source directory. Triggers on: editing enlace source
7
+ files (base.py, discover.py, compose.py, serve.py, auth.py, stores.py),
8
+ implementing spec phases 2-4, or when the user says "add X to enlace" or
9
+ "implement Y in enlace".
10
+ ---
11
+
12
+ # Developing enlace
13
+
14
+ enlace is a multi-app ASGI platform. This skill covers the critical rules,
15
+ architecture, and patterns needed to modify the enlace codebase safely.
16
+
17
+ ## Design Principles
18
+
19
+ These two principles govern ALL changes. Every feature, fix, and diagnostic
20
+ suggestion must respect both:
21
+
22
+ ### 1. Zero coupling — apps don't import enlace
23
+
24
+ enlace wraps apps from the outside. Apps depend on their own domain libs
25
+ (FastAPI, numpy, etc.), never on enlace. enlace provides services via:
26
+ - ASGI scope injection (auth, store) — not imports
27
+ - Environment variables (`ENLACE_MANAGED`) — optional, apps can ignore it
28
+ - Convention (filesystem layout, app.toml) — external to app code
29
+
30
+ When adding features, NEVER create patterns that require apps to
31
+ `import enlace` or depend on enlace-specific APIs.
32
+
33
+ ### 2. Preserve standalone operation
34
+
35
+ When enlace (or its diagnostic tool) suggests changes to an app, those
36
+ changes MUST preserve the app's ability to run independently. The pattern:
37
+ **env-var with current value as default**.
38
+
39
+ ```python
40
+ # GOOD: works standalone AND under enlace
41
+ if not os.environ.get('ENLACE_MANAGED'):
42
+ app.add_middleware(CORSMiddleware, ...)
43
+
44
+ # BAD: breaks standalone
45
+ # (CORSMiddleware deleted entirely)
46
+ ```
47
+
48
+ When writing diagnostic messages, fix suggestions, or documentation:
49
+ - Prefer enlace-side solutions (app.toml, build-time env vars) over app changes
50
+ - When app changes are needed, always use the env-var-with-default pattern
51
+ - Flag any suggestion that would break standalone with `breaks_standalone=True`
52
+
53
+ See `misc/docs/design_principles__*.md` for the full rationale.
54
+
55
+ ## Architecture
56
+
57
+ ```
58
+ enlace/
59
+ ├── base.py # Pydantic models: AppConfig, PlatformConfig, ConventionsConfig
60
+ ├── util.py # Pure helpers: derive_display_name, derive_route_prefix, is_skippable
61
+ ├── discover.py # ConventionDiscoverer: walks apps/, detects types, loads TOML
62
+ ├── compose.py # build_backend(): mounts sub-apps, cascade_lifespan
63
+ ├── diagnose.py # diagnose_app(): scan an app dir for enlace compatibility issues
64
+ ├── serve.py # Uvicorn subprocess orchestration, signal forwarding
65
+ ├── __main__.py # CLI via argh.dispatch_commands
66
+ ├── __init__.py # Public API facade
67
+ └── tests/ # Unit tests (test_discover.py, test_compose.py)
68
+ ```
69
+
70
+ **Data flow:** `PlatformConfig.from_toml()` → `ConventionDiscoverer.discover()` →
71
+ `config.check_conflicts()` → `build_backend(config)` → `uvicorn --factory`
72
+
73
+ ## Critical Rules
74
+
75
+ ### Never use BaseHTTPMiddleware
76
+
77
+ It has terminal, unfixable bugs: exception swallowing across mounted sub-apps,
78
+ ContextVar propagation corruption, synchronous execution of background tasks.
79
+ The Starlette maintainer has called it unfixable. Always use pure ASGI middleware:
80
+
81
+ ```python
82
+ # YES: Pure ASGI middleware
83
+ class MyMiddleware:
84
+ def __init__(self, app):
85
+ self.app = app
86
+
87
+ async def __call__(self, scope, receive, send):
88
+ if scope["type"] == "http":
89
+ # your logic here
90
+ pass
91
+ await self.app(scope, receive, send)
92
+
93
+ # NO: BaseHTTPMiddleware — NEVER use this
94
+ ```
95
+
96
+ ### Mount on FastAPI directly, not APIRouter
97
+
98
+ `app.mount()` only works on the top-level FastAPI instance. Mounting on an
99
+ APIRouter silently fails (FastAPI issues #4194, #10180).
100
+
101
+ ### Never silently swallow ImportErrors during discovery
102
+
103
+ If a module file exists but fails to import (syntax error, missing dep), that is
104
+ a real error. Propagate it. Only skip directories that have no entry point file.
105
+ This prevents the Celery autodiscover anti-pattern.
106
+
107
+ ### Conflict detection: fail-fast, report ALL
108
+
109
+ `check_conflicts()` collects every duplicate route, not just the first. Users
110
+ need to see all conflicts at once to fix them in one pass.
111
+
112
+ ### Starlette does NOT cascade lifespan events
113
+
114
+ Mounted sub-apps never receive startup/shutdown. The `cascade_lifespan` context
115
+ manager in compose.py works around this by iterating Mount routes and entering
116
+ their lifespan contexts. Any new mounting logic must preserve this.
117
+
118
+ ### CORS on parent only
119
+
120
+ If a sub-app also adds CORS middleware, responses get duplicate headers. All
121
+ cross-cutting middleware (auth, CORS, logging, store injection) goes on the
122
+ parent FastAPI app exclusively.
123
+
124
+ ## Research Documents
125
+
126
+ Detailed rationale, code patterns, and pitfalls live in `misc/docs/`:
127
+
128
+ | Module area | Consult |
129
+ |-------------|---------|
130
+ | compose.py, mounting, middleware | `asgi_composition__*.md` |
131
+ | auth.py, sessions, cookies | `auth_cross_cutting__*.md` |
132
+ | discover.py, config, show-config | `convention_over_configuration__*.md` |
133
+ | serve.py, deployment, logging | `deployment_observability__*.md` |
134
+ | frontend.py, SPA serving | `frontend_serving__*.md` |
135
+ | stores.py, PrefixedStore, Mall | `user_data_persistence__*.md` |
136
+ | Design philosophy, coupling, independence | `design_principles__*.md` |
137
+
138
+ Read the relevant doc before modifying its subsystem.
139
+
140
+ ## Implementation Phases (from spec)
141
+
142
+ **Phase 1 (done):** Core discovery, composition, CLI, serve
143
+ **Phase 2:** frontend.py (SPAStaticFiles, launcher), auth.py (PlatformAuthMiddleware,
144
+ shared-password login, signed cookies), inject.py (HTML injection), hash-password
145
+ and create-session-secret CLI commands
146
+ **Phase 3:** stores.py (PrefixedStore, Mall, filesystem/SQLite backends),
147
+ StoreInjectionMiddleware, per-user sessions, CSRF, WebSocket origin validation
148
+ **Phase 4:** deploy.py (Caddyfile/systemd generation), structlog middleware,
149
+ analytics injection, `generate` CLI namespace
150
+
151
+ ## Testing Pattern
152
+
153
+ ```bash
154
+ pytest enlace/tests/ # Unit tests
155
+ pytest tests/ # Integration tests
156
+ ```
157
+
158
+ Tests use `tmp_path` fixtures to create temporary app directories. Composition
159
+ tests use `starlette.testclient.TestClient` against the built app. Always test
160
+ both the happy path and error cases (import errors, conflicts, missing files).
161
+
162
+ ## Adding a New Module
163
+
164
+ 1. Create `enlace/{module}.py`
165
+ 2. Add exports to `enlace/__init__.py`
166
+ 3. Create `enlace/tests/test_{module}.py`
167
+ 4. If it adds CLI commands, register them in `enlace/__main__.py` via
168
+ `argh.dispatch_commands`
169
+ 5. Run `enlace check` before and after to verify nothing breaks
@@ -0,0 +1,17 @@
1
+ {
2
+ "skill_name": "enlace-dev",
3
+ "evals": [
4
+ {
5
+ "id": 1,
6
+ "prompt": "I need to add structured request logging to enlace as a middleware that logs every request with timestamp, method, path, status, duration, and the app name extracted from the route prefix. This is part of Phase 4 in the spec. Add it to compose.py's middleware stack.",
7
+ "expected_output": "The agent creates a pure ASGI middleware (NOT BaseHTTPMiddleware), extracts app_id from path prefix, uses time.perf_counter_ns for duration, wraps the send callable to capture status code. Adds it to the middleware stack in build_backend(). Reads the deployment_observability research doc for the structlog pattern.",
8
+ "files": []
9
+ },
10
+ {
11
+ "id": 2,
12
+ "prompt": "The show-config command currently works but it re-does the full discovery+import every time. I want to add a new enlace init command that creates a platform.toml and apps/ directory in the current directory. Keep it simple.",
13
+ "expected_output": "The agent adds an init() function, creates a default platform.toml and apps/ directory, registers the command in __main__.py's dispatch list, and exports from __init__.py. Follows the argh pattern used by existing commands.",
14
+ "files": []
15
+ }
16
+ ]
17
+ }