Skip to content

Commit 82a12ae

Browse files
dmealingclaude
andcommitted
fix(showcase): the prompt-drift demo renamed a payload member and called it a domain rename
metaobjects.dev/articles/prompts-are-code.html says prompt drift "is almost never a prompt-only event — the field got renamed IN THE DOMAIN MODEL, the prompt was just the surface nobody checked", and then shows a payload that was a standalone `object.value` with two bare `field.string`s and no connection to `Subscriber` at all. The drift fixture renamed a member of THAT value object. So the page's own demonstration proved something narrower than the sentence above it: a prompt caught a change to the prompt. The payload members now bind to Subscriber's fields with `extends`, which is what "a projection of the same entities" was always supposed to mean, and the fixture's rename moves onto the entity where the article says it happens. The binding is not decoration — it is what makes the chain forced rather than narrated: 1. `Subscriber.name` is renamed. `extends: Subscriber.name` on the payload stops resolving and the load fails with ERR_UNRESOLVED_SUPER (measured against the fixture, not assumed), so the payload CANNOT be left behind. 2. Propagating it there is therefore compulsory — and that leaves the mustache, still writing `{{name}}`, as the one surface nothing forced anyone to open. 3. `verify --templates` is what opens it: ERR_VAR_NOT_ON_PAYLOAD: name. It also makes the generated output argue the case by itself. `status` now inherits the enum's values and its requiredness, so the payload VO is typed `active | paused | cancelled` instead of `string?` in TypeScript, C#, Python and Kotlin — a projection you can see in the emitted code rather than take on faith. (Kotlin gains a `SubscriberBlurbPayloadStatus` enum file; the JVM run emits Java DTOs only and has no payload VO either way.) Two things fixed in passing: - The drift fixture's header claimed "exactly one change" while carrying two: it had also dropped `column: created_at`. Restored, so the claim is true and a reader diffing the two directories sees only the rename. - `python/subscriber_router.py` and `csharp/SubscriberRoutes.g.cs` were ALREADY stale on main — verified by stashing this work and running the check against a clean HEAD, which reports both. The current generators emit a sort-default map those files predate. `regen-showcase --check` runs in the release preflight rather than per-push, so it went unseen; this commit carries the refresh. The website will keep publishing the old snippet until the next release: the deploy pins to the newest npm-line tag (v1.0.0 today), not main. The article's prose has been corrected separately in wording that is true of both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
1 parent d6f81ab commit 82a12ae

15 files changed

Lines changed: 107 additions & 44 deletions

File tree

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
{
22
"AppDbContext.g.cs": "314c6078030ba151687430b37e6b6ea936238bb5df02df238e4acce2f95a430d",
33
"Subscriber.g.cs": "3ca160941378fbb8b6ad6acf080ff85573f463931e21b8756873a4476e5dd163",
4-
"Subscriber.py": "8afba41c6c2f73bd66caee5da827dd21ef3de81e8afc77a039fb24fd6d135ab1",
5-
"SubscriberBlurbPayload.payload.cs": "5e09867b8b40f9cfb464d2082c813043cb8f1f5dada9e8a58cbbaacd8a6caf4e",
6-
"SubscriberBlurbPayload.py": "73017383c2e8708209f3f6ec4f06a691a8a552f7285f1bcec208ae024a91d2a0",
4+
"SubscriberBlurbPayload.payload.cs": "2a02b73a14b39d3bd39b924ef017498c6ea4320b8143e893099562868a825044",
75
"SubscriberFilterAllowlist.g.cs": "7428d47f6ccb64bb315266ee0564fa3803736623a34ab491435c042848c5d472",
86
"SubscriberNames.g.cs": "91f6eba78657b31fceef7021c6ca6bd799524dcb5120acbc8fcbe18786787174",
9-
"SubscriberRoutes.g.cs": "d08fe489063431017ba8220df57f15a201453a182088d6f82ae73499b4d2fbac",
10-
"__init__.py": "daa04cec586768866d0996c779f62073e6773fe63750ff668327991fac2fd0f5",
7+
"SubscriberRoutes.g.cs": "93896ab53e74872435b2527525b82564fdafd15e8033d7823c1675d6196fee3e",
8+
"generated/python/Subscriber.py": "8afba41c6c2f73bd66caee5da827dd21ef3de81e8afc77a039fb24fd6d135ab1",
9+
"generated/python/SubscriberBlurbPayload.py": "92ac93b66dc2a94ae623f27d85559dfc9997b1d053fad0832bd77233c27f81f5",
10+
"generated/python/__init__.py": "daa04cec586768866d0996c779f62073e6773fe63750ff668327991fac2fd0f5",
11+
"generated/python/subscriber_blurb_payload.py": "158acb455397e82b05cdbf799788ff0080c3a5fef5237a67a31cfac090a78fea",
12+
"generated/python/subscriber_filter_allowlist.py": "73f9fbd1f29d3a1899bdb3362254e103566ff5e534370faa9b92d18bd8861af5",
13+
"generated/python/subscriber_names.py": "fedc996dcfd0344c73529e4d16d0b2cef9ccfd30cde0f4725b963123eac5ef4b",
14+
"generated/python/subscriber_router.py": "14063dde33a04a0e6f07bbb00a63a2149f61616fbf0c6306c4f6c360c4bd8d11",
1115
"generated/ts/Subscriber.queries.ts": "843e21d8ed84ecead64870b980dfa3156dd2f360ade1f1d2693a5f04bde0fb3c",
1216
"generated/ts/Subscriber.routes.ts": "05d57884708ed78bdad9d69744b1685bc7a310a988aec2edd2830b53b68b3315",
1317
"generated/ts/Subscriber.ts": "c3c1729c5c83cb8773cae76178343932d04080aec87c3a5d27916d4b412c2d33",
14-
"generated/ts/SubscriberBlurbPayload.ts": "613a80a6c0ce7e967f84f7af3c587a3c6f04434a67cea838344217ab6d132fb9",
18+
"generated/ts/SubscriberBlurbPayload.ts": "3513f04b4fa8480e519d63973e3b2b0d2a88d71e64af2d8986f19f9a7ea8eb78",
1519
"generated/ts/index.ts": "b9f04c67786b5eec1aa493958d9843b86a58406228fcfc85634559fc4b2cbfa6",
16-
"generated/ts/prompts.ts": "9cd69070c5b9c1f4b1200422858ef34488c2e8aa8ea5339714629a8de72c81cb",
17-
"generated/ts/requirements/subscriberCanBePausedWithoutErasingHistory.field.enum.test.ts": "a5d2494ce5340673da43f2ee3ef13d7505ee349724e8d85f377a84eafeea50ae",
18-
"subscriber_blurb_payload.py": "79fe8a6e989ee4e567d4bee122dd11fc29b38211c37c7390e65811634fd0b558",
19-
"subscriber_filter_allowlist.py": "73f9fbd1f29d3a1899bdb3362254e103566ff5e534370faa9b92d18bd8861af5",
20-
"subscriber_names.py": "fedc996dcfd0344c73529e4d16d0b2cef9ccfd30cde0f4725b963123eac5ef4b",
21-
"subscriber_router.py": "acd0cbf39d14bd0b11e49f31375cf38446caebb3a3039d5f0b9f386638403aae"
20+
"generated/ts/prompts.ts": "232380afd261d1533f5bbb0c21b31a4528f375337660b9c7ac53f80863c44a98",
21+
"generated/ts/requirements/subscriberCanBePausedWithoutErasingHistory.field.enum.test.ts": "a5d2494ce5340673da43f2ee3ef13d7505ee349724e8d85f377a84eafeea50ae"
2222
}

‎examples/showcase/drift/metaobjects/meta.subscriber.yaml‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
# THIS FIXTURE EXISTS TO FAIL.
22
#
3-
# It is examples/showcase/ with exactly one change: the payload member
4-
# `subscriberName` is renamed to `displayName`, while the mustache still writes
5-
# {{subscriberName}}. `meta verify --templates` therefore reports
6-
# ERR_VAR_NOT_ON_PAYLOAD, and THAT OUTPUT IS PAGE CONTENT — the transcript block on
3+
# It is examples/showcase/ with one change, made where the article says it is made:
4+
# in the DOMAIN MODEL. `Subscriber.name` is renamed to `displayName`, and the blurb
5+
# payload — whose members bind to Subscriber's fields — follows it, because leaving
6+
# it behind would not compile: `extends: Subscriber.name` no longer resolves. The
7+
# mustache still writes {{name}}, and nothing forced anyone to look at it.
8+
#
9+
# That is the whole point, and the reason this fixture is shaped the way it is. It
10+
# used to rename a payload MEMBER, which made the page's claim — "the field got
11+
# renamed in the domain model, the prompt was just the surface nobody checked" —
12+
# a demo of something else. The rename now starts on the entity, propagates to the
13+
# payload because the binding makes it, and stops at the template because nothing
14+
# but `verify` was watching.
15+
#
16+
# `meta verify --templates` therefore reports ERR_VAR_NOT_ON_PAYLOAD, and THAT
17+
# OUTPUT IS PAGE CONTENT — the transcript block on
718
# metaobjects.dev/articles/prompts-are-code.html is captured from this directory,
819
# not typed by hand. If this fixture ever goes green, the page is showing a stale
920
# error and the payload build fails (see scripts/site/payload.ts).
@@ -30,12 +41,12 @@ metadata:
3041
- source.rdb: { table: subscribers }
3142
- field.long: { name: id }
3243
- field.string: { name: email, maxLength: 320, required: true }
33-
- field.string: { name: name }
44+
- field.string: { name: displayName }
3445
- field.enum:
3546
name: status
3647
values: [active, paused, cancelled]
3748
required: true
38-
- field.timestamp: { name: createdAt, autoSet: onCreate }
49+
- field.timestamp: { name: createdAt, column: created_at, autoSet: onCreate }
3950
- identity.primary: { name: primary, fields: [id], generation: increment }
4051
# <<<
4152

@@ -59,8 +70,8 @@ metadata:
5970
- object.value:
6071
name: SubscriberBlurbPayload
6172
children:
62-
- field.string: { name: displayName }
63-
- field.string: { name: status }
73+
- field.string: { name: displayName, extends: Subscriber.displayName }
74+
- field.enum: { name: status, extends: Subscriber.status }
6475

6576
- template.prompt:
6677
name: subscriberBlurb
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Write one sentence about {{subscriberName}}, whose account is {{status}}.
1+
Write one sentence about {{name}}, whose account is {{status}}.

‎examples/showcase/generated/csharp/SubscriberBlurbPayload.payload.cs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace Generated;
88

99
public sealed record SubscriberBlurbPayload
1010
{
11-
public string? subscriberName { get; init; }
12-
public string? status { get; init; }
11+
public enum Status { active, paused, cancelled }
12+
public string? name { get; init; }
13+
public required Status status { get; init; }
1314
}

‎examples/showcase/generated/csharp/SubscriberRoutes.g.cs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ public static class SubscriberRoutes
2020
"CreatedAt",
2121
};
2222

23+
private static readonly System.Collections.Generic.Dictionary<string, bool> SortDefaultDesc = new(System.StringComparer.OrdinalIgnoreCase)
24+
{
25+
};
26+
2327
public static IEndpointRouteBuilder MapSubscriberRoutes(this IEndpointRouteBuilder app, string prefix = "/api")
2428
{
2529
app.MapGet(prefix + "/subscribers", async (HttpContext http, AppDbContext db) =>
@@ -38,9 +42,11 @@ public static IEndpointRouteBuilder MapSubscriberRoutes(this IEndpointRouteBuild
3842
{
3943
var parts = sortRaw.ToString().Split(':', 2);
4044
var field = parts[0];
41-
var desc = parts.Length > 1 && string.Equals(parts[1], "desc", System.StringComparison.OrdinalIgnoreCase);
4245
if (!SortAllowlist.TryGetValue(field, out var resolved))
4346
return Results.BadRequest(new { error = "invalid_sort" });
47+
var desc = parts.Length > 1
48+
? string.Equals(parts[1], "desc", System.StringComparison.OrdinalIgnoreCase)
49+
: SortDefaultDesc.TryGetValue(resolved, out var dd) && dd;
4450
q = ApplySortSubscriber(q, resolved, desc);
4551
}
4652

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package acme
22

3+
import jakarta.validation.constraints.NotNull
34
import kotlin.String
45

56
/**
67
* GENERATED — do not hand-edit. Regenerated from metadata.
78
*/
89
public data class SubscriberBlurbPayload(
9-
public val subscriberName: String? = null,
10-
public val status: String? = null,
10+
public val name: String? = null,
11+
@field:NotNull
12+
public val status: SubscriberBlurbPayloadStatus,
1113
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package acme
2+
3+
import kotlinx.serialization.Serializable
4+
5+
/**
6+
* GENERATED — do not hand-edit. Regenerated from metadata.
7+
*/
8+
@Serializable
9+
public enum class SubscriberBlurbPayloadStatus {
10+
active,
11+
paused,
12+
cancelled,
13+
}

‎examples/showcase/generated/python/SubscriberBlurbPayload.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@
44

55
from __future__ import annotations
66

7+
from typing import Literal
8+
79
from pydantic import BaseModel
810

911

1012
class SubscriberBlurbPayload(BaseModel):
11-
subscriberName: str | None = None
12-
status: str | None = None
13+
name: str | None = None
14+
status: Literal["active", "paused", "cancelled"]
1315

1416

1517
class SubscriberBlurbPayloadCreate(BaseModel):
1618
"""GENERATED — CREATE input: auto-gen PK / @mutability readOnly omitted (writeOnce is settable here, once); @default/@autoSet optional; present values validated (FR-036)."""
1719

18-
subscriberName: str | None = None
19-
status: str | None = None
20+
name: str | None = None
21+
status: Literal["active", "paused", "cancelled"]

‎examples/showcase/generated/python/subscriber_blurb_payload.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@
44

55
from __future__ import annotations
66

7+
from typing import Literal
8+
79
from pydantic import BaseModel, ConfigDict
810

11+
Status = Literal["active", "paused", "cancelled"]
12+
913

1014
class subscriberBlurbPayload(BaseModel):
1115
"""GENERATED payload for template ``subscriberBlurb``.
1216
1317
Field shape derived from the ``SubscriberBlurbPayload`` object.value."""
1418

1519
model_config = ConfigDict(extra="forbid")
16-
subscriberName: str | None = None
17-
status: str | None = None
20+
name: str | None = None
21+
status: Status
1822

1923

2024
__all__ = ["subscriberBlurbPayload"]

‎examples/showcase/generated/python/subscriber_router.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ class _SortClause(BaseModel):
4141
}
4242

4343

44+
_SORT_DEFAULT_ORDER: dict[str, str] = {}
45+
46+
4447
_REQUIRED_FIELDS: frozenset[str] = frozenset(
4548
{
4649
"email",
@@ -57,7 +60,11 @@ def _parse_sort(raw: str) -> _SortClause | None:
5760
parts = raw.split(":", 1)
5861
if not parts or parts[0] not in _SORT_ALLOWLIST:
5962
return None
60-
direction = parts[1].lower() if len(parts) == 2 else "asc"
63+
direction = (
64+
parts[1].lower()
65+
if len(parts) == 2
66+
else _SORT_DEFAULT_ORDER.get(parts[0], "asc")
67+
)
6168
if direction not in ("asc", "desc"):
6269
return None
6370
return _SortClause(field=parts[0], direction=direction)

0 commit comments

Comments
 (0)