Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .opencode/plugins/synthorg-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* PreToolUse (Bash): scripts/check_bash_no_write.sh
* PreToolUse (Bash): scripts/check_git_c_cwd.sh
* PreToolUse (Bash): scripts/check_no_pr_create.sh
* PreToolUse (Bash): scripts/check_no_git_no_verify.sh
* PreToolUse (Bash): scripts/check_no_cd_prefix.sh
* PreToolUse (Bash): scripts/check_no_local_coverage.sh
* PreToolUse (Bash): scripts/check_enforce_parallel_tests.sh
Expand Down Expand Up @@ -342,6 +343,7 @@ export const SynthOrgHooks: Plugin = async ({ client, $, app }) => {
// -n=8 --dist=loadfile).
for (const script of [
"scripts/check_no_pr_create.sh",
"scripts/check_no_git_no_verify.sh",
"scripts/check_no_cd_prefix.sh",
"scripts/check_no_local_coverage.sh",
"scripts/check_enforce_parallel_tests.sh",
Expand Down
13 changes: 7 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -570,14 +570,15 @@ repos:
pass_filenames: false
stages: [pre-push]

- id: dto-forbid-extra
name: DTO extra="forbid" gate (src/synthorg/api)
entry: uv run python scripts/check_dto_forbid_extra.py
- id: frozen-extra-forbid
name: Frozen model extra="forbid" gate (src/synthorg)
entry: uv run python scripts/check_frozen_model_extra_forbid.py
language: system
# Trigger on any change to the scanned tree, the checker
# Project-wide successor to the old api-only dto-forbid-extra
# gate. Trigger on any change to the scanned tree, the checker
# itself, or this config so a PR that weakens the gate cannot
# bypass the check by not touching api Python files.
files: ^(src/synthorg/api/.*\.py|scripts/check_dto_forbid_extra\.py|\.pre-commit-config\.yaml)$
# bypass it by not touching scanned Python files.
files: ^(src/synthorg/.*\.py|scripts/check_frozen_model_extra_forbid\.py|\.pre-commit-config\.yaml)$
pass_filenames: false
stages: [pre-push]

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ PYTHONPATH=. uv run zensical build # docs
- No `from __future__ import annotations` (3.14 has PEP 649). PEP 758 except: `except A, B:` no parens unless binding.
- Type hints on public functions; mypy strict. Google-style docstrings. Line length 88; functions <50 lines; files <800 lines.
- Errors: `<Domain><Condition>Error` from `DomainError`; never inherit `Exception`/`RuntimeError`/etc directly. Enforced by `check_domain_error_hierarchy.py`.
- Pydantic v2 frozen + `extra="forbid"` on API DTOs (Request/Response/Snapshot/Result/Envelope/Status/Info/Summary suffixes); `@computed_field` for derived; `NotBlankStr` for identifiers.
- Pydantic v2 frozen + `extra="forbid"` on every frozen model project-wide (gate `check_frozen_model_extra_forbid.py`; `@computed_field` auto-exempt, per-line `# lint-allow: frozen-extra-forbid -- <reason>` for `extra="allow"`/`"ignore"` boundaries); `@computed_field` for derived; `NotBlankStr` for identifiers.
- Args models at every system boundary; `parse_typed()` for every external dict ingestion. Enforced by `check_boundary_typed.py`.
- Immutability: `model_copy(update=...)` or `copy.deepcopy()`; deepcopy at system boundaries.
- Async: `asyncio.TaskGroup` for fan-out/fan-in; helpers catch `Exception` (re-raise `MemoryError`/`RecursionError`).
Expand Down
14 changes: 7 additions & 7 deletions data/runtime_stats.yaml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
schema_version: 1
last_generated_utc: '2026-05-17T01:30:00Z'
generator_revision: 949abda43
last_generated_utc: '2026-05-17T13:33:50Z'
generator_revision: e0a5b2a55
stats:
tests:
raw: 31136
rounded: 31000
display: 31,000+
raw: 30950
rounded: 30000
display: 30,000+
mem0_stars:
raw: 55881
raw: 55932
rounded: 55000
display: 55k+
providers_curated:
raw: 20
display: '20'
providers_via_litellm:
raw: 2708
raw: 2717
display: 2700+
subagents:
raw: 26
Expand Down
4 changes: 2 additions & 2 deletions docs/design/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ from synthorg.core.types import NotBlankStr
class Skill(BaseModel):
"""Structured capability description, A2A AgentSkill-aligned."""

model_config = ConfigDict(frozen=True, allow_inf_nan=False)
model_config = ConfigDict(frozen=True, allow_inf_nan=False, extra="forbid")

id: NotBlankStr # e.g. "code-review"
name: NotBlankStr # e.g. "Code Review"
Expand All @@ -88,7 +88,7 @@ class Skill(BaseModel):
class SkillSet(BaseModel):
"""Agent skill inventory, split into primary and secondary."""

model_config = ConfigDict(frozen=True, allow_inf_nan=False)
model_config = ConfigDict(frozen=True, allow_inf_nan=False, extra="forbid")

primary: tuple[Skill, ...] = ()
secondary: tuple[Skill, ...] = ()
Expand Down
2 changes: 1 addition & 1 deletion docs/design/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ for non-Docker deployments where torch is installed directly.

```python
class EmbeddingFineTuneConfig(BaseModel):
model_config = ConfigDict(frozen=True, allow_inf_nan=False)
model_config = ConfigDict(frozen=True, allow_inf_nan=False, extra="forbid")

enabled: bool = False
checkpoint_path: NotBlankStr | None = None
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/audit-category-gate-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The four resolution paths are:
| Typed boundary (`parse_typed` at every external dict ingestion) | Standing gate | `scripts/check_boundary_typed.py` |
| Vendor-name leakage (Anthropic / OpenAI / Claude / GPT) | Standing gate | `scripts/check_forbidden_literals.py` |
| Regional-default hardcoding (currency / locale / timezone / language) | Standing gate | `scripts/check_backend_regional_defaults.py` + `scripts/check_web_design_system.py` |
| API DTO `extra="forbid"` (request / response / snapshot / result / envelope / status / info / summary suffixes) | Standing gate | `scripts/check_dto_forbid_extra.py` |
| Frozen model `extra="forbid"` (project-wide: every frozen `ConfigDict` model under `src/synthorg/`, `@computed_field` auto-exempt) | Standing gate | `scripts/check_frozen_model_extra_forbid.py` |
| Em-dashes (U+2014) in source | Standing gate | `scripts/check_no_em_dashes.py` |
| Redundant per-test `pytest.mark.timeout(30)` | Standing gate | `scripts/check_no_redundant_timeout.py` |
| Bulk edits without explicit user approval | Standing gate | `scripts/check_no_bulk_edit.py` |
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/convention-gates.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ All under `scripts/`. The list is generated by `ls scripts/check_*.py`; if an en
- `check_doc_drift_counts.py`
- `check_doc_numeric_macros.py`
- `check_domain_error_hierarchy.py`
- `check_dto_forbid_extra.py`
- `check_dto_types_ts_in_sync.py`
- `check_dual_backend_test_parity.py`
- `check_error_codes_ts_in_sync.py`
- `check_forbidden_literals.py`
- `check_frozen_model_extra_forbid.py`
- `check_image_signatures.py`
- `check_list_pagination.py`
- `check_logger_exception_str_exc.py`
Expand Down
44 changes: 29 additions & 15 deletions docs/reference/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,23 +183,36 @@ inline with the consumer. Examples:
## 8. Frozen `ConfigDict` pattern

Every Pydantic model declares
`model_config = ConfigDict(frozen=True, allow_inf_nan=False)`. The
project standard is to add `extra="forbid"` on every model that does
not need to round-trip through `model_dump()` -- which is most of
them. Around 489 ConfigDicts across `src/synthorg/` carry the strict
form today; the carve-out is the ~46 classes that declare a
`@computed_field`, where Pydantic v2 includes the computed value in
`model_dump()` output and a strict-extra reconstruction would reject
that key on the round trip. Request DTOs are always strict because
the caller-side reject-unknown-keys property is what `extra="forbid"`
exists for.

`model_config = ConfigDict(frozen=True, allow_inf_nan=False)` with
`extra="forbid"`. This is enforced project-wide (not API-DTO-only)
by `scripts/check_frozen_model_extra_forbid.py`: every class under
`src/synthorg/` whose own `model_config` is a `ConfigDict` (or dict
literal) with `frozen=True` MUST also set `extra="forbid"`.

Two carve-outs:

* **`@computed_field` (automatic).** Classes declaring a
`@computed_field` are exempt without annotation: Pydantic v2
includes the computed value in `model_dump()` output and a
strict-extra reconstruction would reject that key on the round
trip. The gate detects the decorator via AST so the ~68 such
classes carry no per-line noise.
* **Per-line opt-out.** Genuine exceptions (an `extra="allow"`
envelope that must accept arbitrary provider keys, a
validator-gated boundary using `extra="ignore"` for
forward-compat) declare
`# lint-allow: frozen-extra-forbid -- <reason>` on the class
definition line. Bare opt-outs without a reason are violations.

Request DTOs are always strict because the caller-side
reject-unknown-keys property is what `extra="forbid"` exists for.
Combined with the framework's `frozen` guarantee this gives us the
"create new objects, never mutate existing ones" property the
immutability covenant relies on.

References: 489+ occurrences across `src/synthorg/`. Canonical example:
`src/synthorg/approval/models.py:28`.
Canonical example: `src/synthorg/approval/models.py:28`. Gate:
`scripts/check_frozen_model_extra_forbid.py` (pre-push +
`.pre-commit-config.yaml` `frozen-extra-forbid`).

## 9. Typed args models at system boundaries (#1611)

Expand Down Expand Up @@ -754,8 +767,9 @@ API boundary. The naming suffix encodes its role:
* `*Info`: derived metadata (e.g. `ProviderInfo`).
* `*Summary`: aggregate / rollup view (e.g. `BudgetSummary`).

The `dto-forbid-extra` gate scans for any DTO carrying one of these
suffixes and verifies it sets `extra="forbid"`.
The project-wide `frozen-extra-forbid` gate (section 8) covers every
DTO carrying one of these suffixes along with every other frozen
model, verifying each sets `extra="forbid"`.

## 30. Import order

Expand Down
19 changes: 18 additions & 1 deletion docs/reference/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ Clients should dispatch on `error_code` (most specific) and fall back to `error_
| 2002 | `ARTIFACT_TOO_LARGE` | Upload exceeds `artifact.max_bytes` |
| 2003 | `TOOL_PARAMETER_ERROR` | Tool parameters failed schema validation |
| 2004 | `PROVIDER_TIER_COVERAGE_INSUFFICIENT` | Setup wizard cannot apply a template because no configured provider exposes any models |
| 2005 | `IMMUTABLE_FIELD_MISMATCH` | A restore/rollback would change an immutable field (e.g. agent id/name/department) |
| 2006 | `CHECKPOINT_ROLLBACK_UNAVAILABLE` | Fine-tune checkpoint rollback target is missing or unusable |
| 2007 | `CHECKPOINT_ROLLBACK_CORRUPT` | Fine-tune checkpoint rollback backup data is corrupt |

## Not Found (3xxx)

Expand All @@ -70,8 +73,12 @@ The NotFound hierarchy is driven by a single `NotFoundError` class with domain-s
| 3010 | `CONNECTION_NOT_FOUND` | Integration connection |
| 3011 | `MODEL_NOT_FOUND` | Provider model |
| 3012 | `ESCALATION_NOT_FOUND` | Escalation queue entry |
| 3013 | `WORKFLOW_DEFINITION_NOT_FOUND` | Workflow definition record |
| 3014 | `AB_TEST_NOT_FOUND` | A/B test record for a proposal |
| 3015 | `BACKUP_NOT_FOUND` | Backup archive |
| 3016 | `MEMORY_ENTRY_NOT_FOUND` | Agent memory entry |

All 13 share the same `type` URI; the numeric code is the discriminator.
All share the same `type` URI; the numeric code is the discriminator.

## Conflict (4xxx)

Expand All @@ -86,6 +93,11 @@ All 13 share the same `type` URI; the numeric code is the discriminator.
| 4006 | `ESCALATION_ALREADY_DECIDED` | Late decision on a closed escalation |
| 4007 | `MIXED_CURRENCY_AGGREGATION` | Cross-currency aggregation attempted |
| 4008 | `WORKFLOW_EXECUTION_ALREADY_TERMINAL` | Cancel hit an execution already in a terminal status (no retry will succeed) |
| 4009 | `BACKUP_IN_PROGRESS` | A backup/restore operation is already running |
| 4010 | `CHECKPOINT_OPERATION_CONFLICT` | Checkpoint deploy/delete rejected (e.g. active checkpoint) |
| 4011 | `FINE_TUNE_RUN_ACTIVE` | A fine-tune run is already active (start/resume blocked) |
| 4012 | `TRAINING_PLAN_NOT_MODIFIABLE` | Training plan cannot be modified after execution or failure |
| 4013 | `BACKUP_UNRESTARTABLE` | Backup service stopped in an unrestartable state |

## Rate Limit (5xxx)

Expand Down Expand Up @@ -135,6 +147,11 @@ All 13 share the same `type` URI; the numeric code is the discriminator.
| 8008 | `TOOL_EXECUTION_ERROR` | Tool runtime failure (subclass of `TOOL_ERROR`) |
| 8009 | `FEATURE_NOT_IMPLEMENTED` | Active backend or deployment fundamentally does not implement the requested operation (501) |
| 8010 | `ARTIFACT_NO_STORAGE_BACKEND` | Artifact service was constructed without a storage backend; controller-helper misconfiguration |
| 8011 | `AGENT_IDENTITY_ROLLBACK_FAILED` | Unexpected server failure during agent-identity rollback |
| 8012 | `BACKUP_RESTORE_FAILED` | Restore operation failed (non-recoverable backend error) |
| 8013 | `BACKUP_MANIFEST_ERROR` | Backup manifest could not be parsed or validated |
| 8014 | `SETTINGS_ENCRYPTION_ERROR` | Internal error processing a sensitive (encrypted) setting |
| 8015 | `SINK_CONFIG_VALIDATION_ERROR` | Internal error validating an observability sink configuration |

## Content negotiation

Expand Down
Loading
Loading