Skip to content

Commit 7b67aba

Browse files
authored
fix: close adopter defects for 1.2.0 across Python routers, filter query strings, and docs-site assets (#507)
* fix: Python router mypy --strict, withCount client flag, docs-site assetBase, and verify/entity doc corrections Python generated routers pass mypy --strict (Protocol list alias, Response-typed delete, typed M:N rows); buildFilterQs sends withCount=1; metaobjects agent-docs help and flags match its redirect; docsSiteFile takes assetBase; per-port verify tables, the README capability matrix and the C#/Python entity samples are corrected. * no-mistakes(review): Keep hidden ignored agent-docs flags; fix skill docs * docs: say in the @formExclude registry description that generated write routes ignore the field Since #484 the generated create, PATCH and PUT routes drop a value sent for a @formExclude field and reads are unchanged; the description said only that the field is omitted from generated forms. Same sentence in the spec, the TypeScript, Python and C# registry sources, expected-registry.json, the metamodel-docs fixture and the reference site (regenerated). * no-mistakes(review): Regenerate showcase Python router * no-mistakes(review): Correct per-port bare verify docs for C# * no-mistakes(document): Docs current; fixed C#/Python nested-object wording in field-types.md
1 parent 2433746 commit 7b67aba

48 files changed

Lines changed: 618 additions & 234 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@ here.**
1212

1313
### Docs
1414

15+
- **The `@formExclude` description says what the attribute does since #484.** The registry text
16+
said only "omitted from generated forms"; it now also says the generated create, PATCH and PUT
17+
routes ignore a value sent for the field and that reads of it are unchanged. The sentence is the
18+
same in `spec/metamodel/ui.json`, the TypeScript, Python and C# registry sources (Java reads the
19+
spec), `expected-registry.json`, the metamodel-docs fixture and the reference site. No
20+
behavior changes.
21+
22+
- **Verify tables, the capability matrix and the entity samples now match what the ports emit.**
23+
The per-port drift tables in `migrations-and-drift.md` and `templates-and-payloads.md` give
24+
each port's real bare command (`meta verify`, `mvn metaobjects:verify`, `dotnet meta verify`,
25+
`metaobjects verify`), what a bare run checks, the config each check needs and how to run one
26+
alone, and where a bare run skips a check versus fails (a bare `dotnet meta verify` with no
27+
`prompts/` or `templates/` directory exits 2); `python -m metaobjects.render.verify` (a module with no entry point) and the Node `meta`
28+
as the C# binary are gone from every copy. The README capability matrix splits "DB-drift
29+
verify" (TypeScript-only, the other four ports go through the TypeScript CLI) from
30+
"Codegen-drift verify". The C# entity is a mutable `class`, not a `record`, and the Python
31+
entity is a Pydantic `BaseModel`, not a `@dataclass`: the entity, field-type, source-kind and
32+
relationship pages now show output generated from the example models. The Python
33+
relationship section no longer calls reverse-finder and M:N codegen pending.
34+
1535
- **Writing your own generator is documented as the normal path.** `llms.txt`, `llms-full.txt`
1636
and the README now say a team should expect to write generators, and that documentation is a
1737
generator. New recipe, [a typed client for the reference routes](docs/recipes/typed-client-for-the-reference-routes.md),
@@ -25,6 +45,13 @@ here.**
2545

2646
### Added
2747

48+
- **`docsSiteFile({ assetBase })` loads the docs site's CSS and JS from a base you choose.** The
49+
page chrome loads daisyUI, Tailwind's browser build, Mermaid and svg-pan-zoom from jsDelivr, so a
50+
site opened without network access rendered unstyled with dead diagrams. `assetBase` (also an
51+
option of `generateSite`) replaces `https://cdn.jsdelivr.net/npm` in those five tags; the default
52+
is unchanged and the default output is byte-identical. The `@metaobjectsdev/docs-site` README
53+
lists the five files and how to serve them. Copying them into the output is not done for you.
54+
2855
- **Jinja templates in Python and TypeScript, held to the same payload check (ADR-0061, closes #483).**
2956
A `template.prompt` or `template.output` body may be a Jinja file —
3057
`<group>/<source>.jinja`, `.j2` or `.jinja2` — instead of a `.mustache` one. Nothing in the
@@ -240,6 +267,25 @@ here.**
240267

241268
### Fixed
242269

270+
- **Generated Python routers pass `mypy --strict`.** The repository `Protocol` declares a method
271+
named `list`, so every later `list[...]` annotation in its body resolved to that method
272+
(`valid-type`, 81 errors on the fitness corpus); the annotations now use an
273+
`from builtins import list as _list` alias, and the method keeps its name. The delete handlers
274+
returned a `JSONResponse` from `-> None` and the M:N traversal handlers returned `Any` from
275+
`-> list[Any]` (`return-value`, `no-any-return`); delete is now `-> Response` and returns the same
276+
204, and the traversal handlers bind the rows to a typed local. HTTP behavior is unchanged
277+
(the generated api-contract lane passes). A new test runs `mypy --strict` over the routers
278+
generated from the fitness corpus. `mypy` joins the Python dev group.
279+
280+
- **`buildFilterQs({ withCount: true })` sends `withCount=1`.** It sent `withCount=true`, which
281+
every server's list route ignores (the cross-port contract is `withCount=1`), so the caller got
282+
a bare array instead of `{ rows, total }`. `false` and `0` now send nothing.
283+
284+
- **`metaobjects agent-docs` says what it does.** Its help claimed to scaffold the agent context;
285+
the command only points at `npx meta agent-docs --server python` and exits 1. The help now says
286+
so. The three flags it never read (`--server`, `--client`, `--out`) are still accepted but
287+
ignored and hidden from help, so the old invocation still reaches the pointer.
288+
243289
- **`@default: "now"` is the current time on Postgres, not the table's creation time.** The
244290
expected schema stored it as the literal `'now'`, and Postgres converts that literal to a
245291
timestamp once, when it creates the column's `DEFAULT`, so every row got the same value. It

‎README.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,10 @@ first-week wedge plan — and `meta init` picks up from there.
179179
| Templates + render (FR-004) | Yes | Yes | Yes (wraps Java) | Yes | Yes |
180180
| Payload-VO codegen (the value object's own type, ADR-0056) | Yes (`entityFile()`) | Yes (`SpringValueObjectGenerator`) | Yes (`KotlinEntityGenerator`) | Yes (`EntityGenerator`) | Yes (`entity`) |
181181
| Migration emission | `meta migrate` (Postgres / SQLite / D1) | Via TS toolchain (`@metaobjectsdev/cli migrate`) | Via TS toolchain (`@metaobjectsdev/cli migrate`) | Via TS toolchain (ADR-0015) | Via TS toolchain (ADR-0015) |
182-
| DB-drift verify | `meta verify --db <url>` | Template-drift: `Verify.check`; schema-drift is TS-owned (ADR-0015) | Template-drift: `Verify.check`; startup: `MetadataStartupValidator` | `dotnet meta verify` (codegen-drift) | Schema-drift is TS-owned (ADR-0015) |
182+
| DB-drift verify | `meta verify --db <url>` | Through the TypeScript CLI (ADR-0015) | Through the TypeScript CLI (ADR-0015) | Through the TypeScript CLI (ADR-0015) | Through the TypeScript CLI (ADR-0015) |
183+
| Codegen-drift verify | `meta verify` | `mvn metaobjects:verify` | `mvn metaobjects:verify` (via Java) | `dotnet meta verify` | `metaobjects verify` |
183184
| Prisma schema (`orm: "prisma"`, #479) | `prismaSchemaFile()` writes `schema.prisma` (Postgres / MySQL / SQLite); `verify --codegen` and `verify --db` gate it | No | No | No | No |
184-
| Template-drift verify | Yes | Yes (`Verify.check`) | Yes (via Java) | Yes (`dotnet meta verify`) | Yes (`metaobjects.render.verify`) |
185+
| Template-drift verify | Yes | Yes (`Verify.check`) | Yes (via Java) | Yes (`dotnet meta verify`) | Yes (`metaobjects verify`) |
185186
| YAML authoring (sigil-free → JSON) | Yes | Yes | Yes (via Java) | Yes | Yes |
186187
| Capability requirements (`requirement.*`) | `meta verify` gate + authoring lint; `requirementTests()` stubs | `mvn metaobjects:verify` gate; JUnit Jupiter `requirement-tests` | Gate via the Java Maven goal; JUnit Jupiter `requirement-tests` | `dotnet meta verify` gate; xUnit `requirement-tests` | `metaobjects verify` gate; pytest `requirement-tests` |
187188
| Reporting vocabulary (`dimension` / `measure` / `segment` / `object.report`, FR-044) | Registered; a view-backed report becomes a SQL view in `meta migrate`, is read by `ObjectManager`, and gets generated read-only list routes and a TanStack list hook (no grid or form); the `cube-model` generator writes the vocabulary as Cube data-model files | Registered; OMDB reads a view-backed report; generates a read-only Spring controller, DTO, repository and filter allowlist | Registered (via Java); generates an Exposed table object, a row data class, a filter allowlist and a read-only Spring controller per view-backed report | Registered; generates a keyless EF Core row type, read-only routes and a filter allowlist per view-backed report | Registered; `ObjectManager` reads a view-backed report; generates a Pydantic row model, a read-only FastAPI router and a filter allowlist |
@@ -241,7 +242,7 @@ sixth ships two libraries at their own stability labels:
241242
[Write your own generator](docs/recipes/write-your-own-generator.md) has every port's
242243
20-line shape plus JSON Schema and OpenAPI examples to copy. The reference generators
243244
(Drizzle/Zod + Fastify for TS, Spring REST + DTO + repository for Java, `data class` +
244-
Exposed for Kotlin, EF Core record + ASP.NET routes for C#, Pydantic + FastAPI for
245+
Exposed for Kotlin, EF Core entity class + ASP.NET routes for C#, Pydantic + FastAPI for
245246
Python) are starting points: run one as shipped when it already emits what you want, or
246247
eject the one that is close, change it, and regenerate.
247248
The engine that runs generators is core; their output is yours.

‎agent-context/skills/metaobjects-codegen/references/python.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,6 @@ rather than editing `render_wire(..., _WIRE)` in a router. `metaobjects verify`
270270

271271
## Re-scaffold this context
272272

273-
`metaobjects agent-docs --server python [--out <dir>]` (re)scaffolds the slim always-on
274-
Markdown + these `metaobjects-*` skills into the project — the Python tool bundles the
275-
agent-context tree, so a Python consumer needs no Node `meta`.
273+
`metaobjects agent-docs` does not scaffold anything: it prints a pointer and exits 1.
274+
Run `npx meta agent-docs --server python` (the Node `meta` CLI) to (re)scaffold the slim
275+
always-on Markdown + these `metaobjects-*` skills into the project.

‎agent-context/skills/metaobjects-prompts/references/csharp.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ For every RESPONDING `template.prompt` — one declaring `@responseRef` —
55
a model's reply against that shape. ADR-0052: the tier binds `@responseRef`, never
66
`@payloadRef` (which types the request the prompt renders outbound), and a
77
`template.output` gets no parser at all. This is the receive side only — codegen emits
8-
**no** provider/LLM-call layer; you compose the call yourself. C# names records after the
9-
resolved VALUE OBJECT, so the response record simply IS that VO's record — no second
8+
**no** provider/LLM-call layer; you compose the call yourself. C# names its types after the
9+
resolved VALUE OBJECT, so the response type simply IS that VO's class — no second
1010
naming convention, and the parser and the record can't silently drift.
1111

1212
## Contents

‎agent-context/skills/metaobjects-verify/references/requirements.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ text (a shared corpus pins them). Run the one for the project's server language:
2626
| TypeScript | `meta verify` |
2727
| Python | `metaobjects verify` |
2828
| Java, Kotlin | `mvn metaobjects:verify` (one goal for both) |
29-
| C# | `dotnet meta verify <metadataDir> --templates <dir>` (or `--codegen --out <dir>`) |
29+
| C# | `dotnet meta verify` |
3030

3131
The gate is core: it is not a generator and cannot be ejected or replaced. What differs
3232
by port is the **authoring lint** further down this page, which only the Node `meta verify`

‎client/web/packages/runtime-web/src/filter-qs.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import qs from "qs";
1818
* correctly for the server's parser.
1919
*
2020
* `withCount` is the opt-in flag for the `{ rows, total }` list-response
21-
* envelope (see runtime-ts mountListRoute). Pass `1` or `true` to enable.
21+
* envelope (see runtime-ts mountListRoute). Pass `1` or `true` to enable; the wire form is always
22+
* `withCount=1`, the cross-port contract every server's list route reads. `false`, `0` and
23+
* `undefined` emit nothing.
2224
*
2325
* `search` is a free-text term that the server ORs as `like('%term%')` across
2426
* all @filterable string fields, AND-combined with any explicit filter WHERE.
@@ -29,7 +31,7 @@ export function buildFilterQs(filter: Record<string, unknown>): string {
2931
if (limit !== undefined) top.limit = limit;
3032
if (offset !== undefined) top.offset = offset;
3133
if (sort !== undefined) top.sort = sort;
32-
if (withCount !== undefined) top.withCount = withCount;
34+
if (withCount === true || withCount === 1 || withCount === "1" || withCount === "true") top.withCount = 1;
3335
if (search !== undefined) top.search = search;
3436
const filterObj: Record<string, unknown> = { ...fields };
3537
if (or !== undefined) filterObj.or = or;

‎client/web/packages/runtime-web/test/filter-qs.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ describe("buildFilterQs", () => {
6262
expect(decoded(result)).not.toContain("filter[withCount]");
6363
});
6464

65+
test("withCount: true is sent as withCount=1, the value the server's list routes read", () => {
66+
const result = buildFilterQs({ limit: 10, withCount: true });
67+
expect(result).toContain("withCount=1");
68+
expect(result).not.toContain("withCount=true");
69+
});
70+
71+
test("withCount: false or 0 emits no withCount key", () => {
72+
expect(buildFilterQs({ limit: 10, withCount: false })).not.toMatch(/withCount/);
73+
expect(buildFilterQs({ limit: 10, withCount: 0 })).not.toMatch(/withCount/);
74+
});
75+
6576
test("omits withCount when not present (no key, not 'withCount=undefined')", () => {
6677
const result = buildFilterQs({ limit: 10 });
6778
expect(result).not.toMatch(/(^|&)withCount(=|&|$)/);

‎docs/features/entities.md‎

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
An **entity** is a typed, named record shape declared in metadata. It is the unit of
44
codegen, the unit of persistence, and the unit of API exposure. Every port turns the
55
same `object.entity` declaration into idiomatic native code — a TS interface + Zod
6-
schema, a Java POJO, a Kotlin `data class`, a C# `record`, a Python `@dataclass`.
6+
schema, a Java POJO, a Kotlin `data class`, a C# entity class, a Python Pydantic model.
77

88
The entity is metadata-first: you don't write the class, the loader reads the
99
declaration, the codegen emits the class, and the runtime layer reads the same
@@ -233,19 +233,38 @@ object AuthorTable : Table("authors") {
233233

234234
### C#
235235

236-
`MetaObjects.Codegen` (via `dotnet meta gen`) emits an EF Core entity record, a
237-
`DbSet` on the generated `AppDbContext`, and a `CREATE TABLE` in the migration
238-
emitted by the Node `meta migrate` (schema is Node-owned, ADR-0015).
236+
`MetaObjects.Codegen` (via `dotnet meta gen`) emits an EF Core entity class (a mutable
237+
`class` with data-annotation attributes, not a `record`), a `DbSet` on the generated
238+
`AppDbContext`, and a `CREATE TABLE` in the migration emitted by the Node `meta migrate`
239+
(schema is Node-owned, ADR-0015). The sample below is the output of `EntityGenerator` with
240+
`--namespace Acme.Blog`.
239241

240242
```csharp
241-
// generated/Acme/Blog/Author.cs
243+
// generated/Author.g.cs
244+
// <auto-generated/>
245+
// Generated by MetaObjects entity-generator. Do not edit by hand.
246+
#nullable enable
247+
using System;
248+
using System.Collections.Generic;
249+
using System.ComponentModel.DataAnnotations;
250+
using System.ComponentModel.DataAnnotations.Schema;
251+
242252
namespace Acme.Blog;
243253
244-
public record Author
254+
[Table("authors")]
255+
public class Author
245256
{
246-
public long Id { get; set; }
247-
public string Name { get; set; } = string.Empty; // required, maxLength=200
248-
public string? Bio { get; set; } // optional, maxLength=2000
257+
[Key]
258+
[Column("id")]
259+
public long Id { get; set; }
260+
[Column("name")]
261+
[Required(AllowEmptyStrings = true)]
262+
[MaxLength(200)]
263+
[MinLength(1)]
264+
public string Name { get; set; } = default!;
265+
[Column("bio")]
266+
[MaxLength(2000)]
267+
public string? Bio { get; set; }
249268
}
250269
```
251270

@@ -259,21 +278,37 @@ public partial class AppDbContext : DbContext
259278

260279
### Python
261280

262-
`metaobjects.codegen` emits a Python `@dataclass` per entity, plus a shipped
281+
`metaobjects.codegen` emits one Pydantic `BaseModel` per entity (not a `@dataclass`), with a
282+
`<Name>Create` and a `<Name>Patch` input model beside it, plus a shipped
263283
FastAPI `APIRouter` (`router_generator`, FR-008 §2.3) and a shipped
264284
`ObjectManager` runtime (`metaobjects.runtime.object_manager`) for CRUD
265285
against the same metadata.
266286

267287
```python
268-
# generated/acme/blog/author.py
269-
from dataclasses import dataclass
270-
from typing import Optional
271-
272-
@dataclass
273-
class Author:
274-
id: int
275-
name: str # required, max_length=200
276-
bio: Optional[str] = None
288+
# generated/Author.py
289+
from __future__ import annotations
290+
291+
from pydantic import BaseModel, Field
292+
293+
294+
class Author(BaseModel):
295+
id: int | None = None
296+
name: str = Field(max_length=200)
297+
bio: str | None = Field(default=None, max_length=2000)
298+
299+
300+
class AuthorCreate(BaseModel):
301+
"""GENERATED — CREATE input: auto-gen PK / @mutability readOnly omitted (writeOnce is settable here, once); @default/@autoSet optional; present values validated (FR-036)."""
302+
303+
name: str = Field(min_length=1, max_length=200)
304+
bio: str | None = Field(default=None, max_length=2000)
305+
306+
307+
class AuthorPatch(BaseModel):
308+
"""GENERATED — PATCH input: all fields optional (PK, @mutability readOnly and writeOnce excluded); present values validated (FR-036)."""
309+
310+
name: str | None = Field(default=None, min_length=1, max_length=200)
311+
bio: str | None = Field(default=None, max_length=2000)
277312
```
278313

279314
```python

‎docs/features/field-types.md‎

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ across ports — a `field.currency` is integer minor units everywhere; a
2525
| `field.uri` | `string` | `java.net.URI` | `java.net.URI` | `Uri` | `AnyUrl` | `text` |
2626
| `field.inet` | `string` | `InetAddress` | `InetAddress` | `IPAddress` | `IPvAnyAddress` | `inet` |
2727
| `field.enum` | union + `z.enum` | `Enum` | `enum class` | `enum` | `Enum` | `varchar` + `CHECK` |
28-
| `field.object` | nested type | nested class | nested data class | nested record | nested dataclass | per `@storage` |
28+
| `field.object` | nested type | nested class | nested data class | nested class | nested Pydantic model | per `@storage` |
2929
| `field.map` | `Record<string, V>` | `Map<String, V>` | `Map<String, V>` | `Dictionary<string, V>` | `dict[str, V]` | `jsonb` |
3030

3131
That is the whole registered vocabulary — 17 concrete subtypes. `field.base` is an
@@ -357,18 +357,31 @@ object AuthorTable : Table("authors") {
357357
### C#
358358

359359
```csharp
360-
// generated/Acme/Blog/Author.cs
361-
public record Author
360+
// generated/Author.g.cs (usings and header elided)
361+
namespace Acme.Blog;
362+
363+
[Table("authors")]
364+
public class Author
362365
{
363-
public long Id { get; set; }
364-
public string Name { get; set; } = string.Empty;
365-
public string? Bio { get; set; }
366-
public long PriceCents { get; set; } // currency: minor units
367-
public AuthorStatus Status { get; set; }
368-
public DateTimeOffset CreatedAt { get; set; }
366+
public enum AuthorStatus { DRAFT, PUBLISHED, ARCHIVED }
367+
[Key]
368+
[Column("id")]
369+
public long Id { get; set; }
370+
[Column("name")]
371+
[Required(AllowEmptyStrings = true)]
372+
[MaxLength(200)]
373+
[MinLength(1)]
374+
public string Name { get; set; } = default!;
375+
[Column("bio")]
376+
[MaxLength(2000)]
377+
public string? Bio { get; set; }
378+
[Column("price_cents")]
379+
public long PriceCents { get; set; } // currency: minor units
380+
[Column("status")]
381+
public AuthorStatus Status { get; set; }
382+
[Column("created_at")]
383+
public DateTimeOffset CreatedAt { get; set; }
369384
}
370-
371-
public enum AuthorStatus { Draft, Published, Archived }
372385
```
373386

374387
EF Core enum-as-string is wired in `AppDbContext`:
@@ -382,25 +395,22 @@ modelBuilder.Entity<Author>()
382395
### Python
383396

384397
```python
385-
# generated/acme/blog/author.py
386-
from dataclasses import dataclass
387-
from datetime import datetime
388-
from enum import Enum
389-
from typing import Optional
390-
391-
class AuthorStatus(str, Enum):
392-
DRAFT = "DRAFT"
393-
PUBLISHED = "PUBLISHED"
394-
ARCHIVED = "ARCHIVED"
395-
396-
@dataclass
397-
class Author:
398-
id: int
399-
name: str
400-
bio: Optional[str] = None
401-
price_cents: int = 0 # currency: minor units
402-
status: AuthorStatus = AuthorStatus.DRAFT
403-
created_at: Optional[datetime] = None
398+
# generated/Author.py (the Create and Patch input models are elided)
399+
from __future__ import annotations
400+
401+
import datetime
402+
from typing import Literal
403+
404+
from pydantic import BaseModel, Field
405+
406+
407+
class Author(BaseModel):
408+
id: int | None = None
409+
name: str = Field(max_length=200)
410+
bio: str | None = Field(default=None, max_length=2000)
411+
priceCents: int # currency: minor units
412+
status: Literal["DRAFT", "PUBLISHED", "ARCHIVED"]
413+
createdAt: datetime.datetime
404414
```
405415

406416
## Verified by

0 commit comments

Comments
 (0)