feat(standards): fix F11-F16 and rebuild bundle (43 components) + trial findings

- F11 PopulateRule field_name/collection_name corrections
- F12 service adaptor update handles PopulatingRepository
- F13 nested-references section; F14 authless /users/me guidance
- F15 template ruff-clean; F16 stop vendoring .ruff_cache, ship .gitignore
- document --template-style in build_recipe/spec_first_routing; refresh collected
  openapi-first + standards bundle (12 modules, 43 components)
- add trial.md and trial_findings.md (F1-F16, re-trial A/B evidence)
This commit is contained in:
2026-10-13 19:35:12 +05:30
parent fced40af59
commit 6c0d859a1e
56 changed files with 521 additions and 165 deletions

View File

@@ -21,18 +21,17 @@ Example:
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Dict, List, Optional
class KVStore(ABC):
"""Abstract key-value store port."""
@abstractmethod
async def get(self, key: str) -> Optional[str]:
async def get(self, key: str) -> str | None:
"""Return the value at ``key``, or ``None``."""
@abstractmethod
async def set(self, key: str, value: str, ttl: Optional[int] = None) -> None:
async def set(self, key: str, value: str, ttl: int | None = None) -> None:
"""Store ``value`` at ``key`` with an optional TTL in seconds."""
@abstractmethod
@@ -40,7 +39,7 @@ class KVStore(ABC):
"""Delete ``key`` if present."""
@abstractmethod
async def scan(self, pattern: str) -> List[str]:
async def scan(self, pattern: str) -> list[str]:
"""Return keys matching a glob ``pattern``."""
async def clear(self, pattern: str = "*") -> None:
@@ -67,8 +66,8 @@ class InMemoryKV(KVStore):
def __init__(self) -> None:
"""Initialize an empty store with no imports or connections."""
self._data: Dict[str, str] = {}
self._expiry: Dict[str, float] = {}
self._data: dict[str, str] = {}
self._expiry: dict[str, float] = {}
def _expired(self, key: str) -> bool:
import time
@@ -80,13 +79,13 @@ class InMemoryKV(KVStore):
return True
return False
async def get(self, key: str) -> Optional[str]:
async def get(self, key: str) -> str | None:
"""Return the value at ``key``, or ``None`` when absent/expired."""
if self._expired(key):
return None
return self._data.get(key)
async def set(self, key: str, value: str, ttl: Optional[int] = None) -> None:
async def set(self, key: str, value: str, ttl: int | None = None) -> None:
"""Store ``value`` at ``key`` with an optional TTL in seconds."""
import time
@@ -98,7 +97,7 @@ class InMemoryKV(KVStore):
self._data.pop(key, None)
self._expiry.pop(key, None)
async def scan(self, pattern: str) -> List[str]:
async def scan(self, pattern: str) -> list[str]:
"""Return live keys matching a glob ``pattern``."""
import fnmatch
@@ -126,11 +125,11 @@ class RedisKV(KVStore):
self._redis = Redis.from_url(url, decode_responses=decode_responses)
async def get(self, key: str) -> Optional[str]:
async def get(self, key: str) -> str | None:
"""Return the value at ``key``, or ``None``."""
return await self._redis.get(key)
async def set(self, key: str, value: str, ttl: Optional[int] = None) -> None:
async def set(self, key: str, value: str, ttl: int | None = None) -> None:
"""Store ``value`` at ``key`` with an optional TTL in seconds."""
await self._redis.set(key, value, ex=ttl)
@@ -138,7 +137,7 @@ class RedisKV(KVStore):
"""Delete ``key`` if present."""
await self._redis.delete(key)
async def scan(self, pattern: str) -> List[str]:
async def scan(self, pattern: str) -> list[str]:
"""Return keys matching a glob ``pattern``."""
return [key async for key in self._redis.scan_iter(match=pattern)]

View File

@@ -5,14 +5,14 @@ Generic CRUD service adaptor over a Mongo repository.
Bridge pydantic models and mongo-ops documents, exposing create, read, update,
patch, delete, and search operations for every domain that needs them.
"""
from typing import Generic, Type, TypeVar
from typing import Generic, TypeVar
from mongo_ops import BaseDocument, PopulatingRepository
from mongo_ops.models import PyObjectId
from pydantic import BaseModel
from infra.db.repos import mongo
from pydantic import BaseModel
from mongo_ops import BaseDocument
from mongo_ops.models import PyObjectId
Model = TypeVar("Model", bound=BaseModel)
Schema = TypeVar("Schema", bound=BaseDocument)
@@ -27,8 +27,8 @@ class CrudsServiceAdaptor(Generic[Model, Schema]):
"""
repo: mongo.BaseRepository
model: Type[Model]
schema: Type[Schema]
model: type[Model]
schema: type[Schema]
def __init__(self, *, repo=None):
"""Initialize the adaptor with a backing repository.
@@ -83,7 +83,17 @@ class CrudsServiceAdaptor(Generic[Model, Schema]):
Returns:
Schema | None: Updated document, or ``None`` when absent.
Notes:
A populating repository expects a document *model* (so FK fields can
be depopulated), while a base repository expects a raw mapping. This
method normalizes both inputs to whatever the repo requires.
"""
if isinstance(self.repo, PopulatingRepository):
if not isinstance(data, self.schema):
fields = data if isinstance(data, dict) else self.model_to_dict(data)
data = self.schema(**fields)
return await self.repo.update(id, data)
return await self.repo.update(
id,
data if isinstance(data, dict) else self.model_to_dict(data),

View File

@@ -7,7 +7,7 @@ SSEInMemoryBus — in-memory event bus for SSE progress events.
Buffer published progress events so they can be replayed (and streamed) per
correlation key, such as a job, upload, or fetch-request id.
"""
from typing import Generic, List, TypeVar
from typing import Generic, TypeVar
T = TypeVar("T")
@@ -27,7 +27,7 @@ class SSEInMemoryBus(Generic[T]):
key_attr (str): Attribute name on each event holding the stream
correlation key (for example ``"job_id"``).
"""
self._events: List[T] = []
self._events: list[T] = []
self._key_attr = key_attr
async def publish(self, event: T) -> None:
@@ -38,7 +38,7 @@ class SSEInMemoryBus(Generic[T]):
"""
self._events.append(event)
def get_events(self, key: str) -> List[T]:
def get_events(self, key: str) -> list[T]:
"""Return buffered events for a correlation key.
Args:

View File

@@ -5,21 +5,20 @@ Generic MongoDB CRUD repo — single implementation for all domains.
Select a concrete mongo-ops repository (base, cached, or populating) from the
options supplied to the factory.
"""
from typing import List, Optional, Type
from mongo_ops import BaseRepository, CachedBaseRepository, PopulatingRepository
from mongo_ops.cache import CacheBackend, CacheConfig
from mongo_ops.models import BaseDocument
from mongo_ops.populate import PopulationEngine, PopulateRule
from mongo_ops.populate import PopulateRule, PopulationEngine
def _repo_factory(
collection: str,
db_model: Type[BaseDocument],
cache_backend: Optional[CacheBackend] = None,
cache_config: Optional[CacheConfig] = None,
engine: Optional[PopulationEngine] = None,
populate_rules: Optional[List[PopulateRule]] = None,
db_model: type[BaseDocument],
cache_backend: CacheBackend | None = None,
cache_config: CacheConfig | None = None,
engine: PopulationEngine | None = None,
populate_rules: list[PopulateRule] | None = None,
) -> BaseRepository:
"""Build the appropriate mongo-ops repository.

View File

@@ -23,8 +23,9 @@ Notes:
"""
import hashlib
import json
from collections.abc import Callable
from functools import wraps
from typing import Any, Callable, Optional
from typing import Any
from fastapi import Request, Response
from fastapi.encoders import jsonable_encoder
@@ -55,7 +56,7 @@ class HttpCacheStore:
self._redis = Redis.from_url(url, decode_responses=decode_responses)
async def get(self, key: str) -> Optional[str]:
async def get(self, key: str) -> str | None:
"""Read the value stored at ``key``.
Args:
@@ -173,7 +174,7 @@ def _cache_key(request: Request, generation: str = "0") -> str:
return hashlib.sha256("|".join(parts).encode()).hexdigest()
def cache_response(ttl: Optional[int] = None) -> Callable:
def cache_response(ttl: int | None = None) -> Callable:
"""Decorate a GET-style route handler to cache its JSON response.
Args:
@@ -203,7 +204,7 @@ def cache_response(ttl: Optional[int] = None) -> Callable:
return await func(*args, **kwargs)
db_resources = getattr(request.app.state, "db_resources", None) or {}
store: Optional[HttpCacheStore] = db_resources.get("http_cache_store")
store: HttpCacheStore | None = db_resources.get("http_cache_store")
if store is None:
return await func(*args, **kwargs)