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
18 changes: 13 additions & 5 deletions agent-context/skills/metaobjects-audit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid
| `references/calibration.md` | Before raising any finding: it lists the port gaps and non-defects that are never the adopter's fault. |
| `references/surface-review-axes.md` | Working any axis (A–I) of Phase 1b. |
| `references/capability-checklist.md` | On every axis: the exhaustive list of what the metamodel can express, one hunt per capability. |
| `references/drift-signatures.md` | Hunting hand-written second sources of truth (validators, mappers, comments, views, JSON bags). |
| `references/drift-signatures.md` | Hunting hand-written second sources of truth (validators, mappers, comments, views, JSON bags, physical-name literals, enum-member and event-name literals). |
| `references/prompt-anti-patterns.md` | Axis D: the project builds text sent to an LLM or parses a reply. |
| `references/vocabulary-hygiene.md` | Axis I: retired or deprecated authoring forms, custom vocabulary, vocabulary opportunities. |
| `references/requirements.md` | Axis E2: the project declares `requirement.*` nodes. |
Expand Down Expand Up @@ -65,7 +65,13 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid
not removed or reordered ones: confirm those from the file's history
(`git log -p -- codegen/generators/<name>.ts` shows no edit after the eject) first, and
report a copy you cannot confirm as "possibly customized: check its history", not as a
copy to drop. For `entity`,
copy to drop. **A copy with lines of its own is not proof of a customization either.**
`meta eject --list` (and each port's listing) prints `DIFFERS` for BOTH a deliberate
customization and a copy gone stale by version, and its `M of your own` count includes
every line the reference has since removed or rewritten, which are older upstream lines.
Run the history check on every `DIFFERS` row, `M` above 0 included, and report the two
figures separately: lines behind the reference, and lines of the project's own that the
history confirms. A copy stale by version is a finding (`owned-generator-stale`). For `entity`,
`routes` and `routes-hono` that holds only when every `codegen/runtime/` file the copy
reaches is also marked identical: an edited adapter is a customization, and such a copy
is not a finding. Those runtime rows appear only with `--format text` (or as the
Expand Down Expand Up @@ -134,7 +140,7 @@ Axes (the full hunt for each is in `references/surface-review-axes.md` — open
- **C.** Drift hotspot — validators, mappers, runtime models · **D.** Prompt pillar
- **E.** Owned generators and scaffold-and-own · **E2.** Capability ledger (`requirement.*`, opt-in)
- **F.** Drift-gate adoption — does it block? · **F2.** Over-generation and generated-but-bypassed (codegen waste)
- **G.** Runtime-contract anti-patterns · **G2.** `own*()` accessor discipline (a correctness defect, not advisory)
- **G.** Runtime-contract anti-patterns · **G2.** `own*()` accessor discipline (a correctness defect, not advisory) · **G3.** Guarded status transitions (every write to a status-like enum column is conditional on the current value; the fix goes through the port's ORM with generated constants, never raw SQL)
- **H.** Authoring correctness / ADR conformance · **H2.** Wrong native type (`field.string` + `@dbColumnType: uuid`)
- **I.** Vocabulary hygiene / modernization (advisory, never a failing finding)

Expand Down Expand Up @@ -173,7 +179,9 @@ hidden candidate. **Stub trap:** demo-data routes have nothing to replace — cl
the Phase 0 owned-generators check: recommend the package import. Importing any other
generator from that subpath is correct and is not a finding.
- Audit owned generators: (a) regenerate clean? (b) drifted from reference templates —
intentional (good) vs stale/accidental (missed upstream fix)? (c) hand-rolling a walk
intentional (good) vs stale/accidental (missed upstream fix)? Decide it from the copy's
history, not from the listing's `own` count: `DIFFERS` reads the same for both, and lines
the reference has since removed count as "own" (Phase 0, owned-generators check). (c) hand-rolling a walk
that a declarative `scope` + `outputPattern` could replace? (d) bespoke shape better as
a `templateGenerator` than a forked generator?
- **Authoring ladder:** built-in fits → import it as shipped · close → **eject it and
Expand Down Expand Up @@ -274,7 +282,7 @@ Each roadmap item: LOC retired, prerequisite, **parity-gate before deleting hand

| Field | Content |
|---|---|
| `id` | stable kebab id (e.g. `handwritten-crud-route`, `manual-zod-validator`) |
| `id` | stable kebab id (e.g. `handwritten-crud-route`, `manual-zod-validator`, `enum-literal-spelling`, `unguarded-status-transition`, `owned-generator-stale`) |
| `title` | "you hand-wrote X that metadata can generate / model" |
| `pillar` | `codegen` / `runtime` / `drift` / `prompt` |
| `surface` | `entity` / `route` / `validator` / `repository` / `dto` / `hooks` / `prompt` / `migration` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
- Planned, not shipped
- Cross-port version-number skew
- Stale upstream prose
- Include-only templates
- Owned generator copies with lines of their own
- Enum-member literals at a checked position

- **Filter-operator route codegen — the baseline grammar ships in all five ports.** The
`?filter[field][op]=value` grammar (all 9 operators `eq/ne/gt/gte/lt/lte/in/like/isNull`,
Expand All @@ -32,3 +35,6 @@
- **Planned, not shipped** — `api.*` / `operation.*` / `binding.*` (FR-024) and MCP exposure of declared prompts/tools are not yet in the registry; their absence is not an adopter defect.
- **Cross-port version-NUMBER skew is by design** — TS/C#/Python `0.x` vs Java/Kotlin `7.x` Maven is correct; never flag the *number lines* differing. But that is exactly why you can't eyeball cross-language drift: compare **`metamodelVersion`** (Phase 0 cross-language consistency item), not the package numbers. A `metamodelVersion` MISMATCH across ports *is* a finding; so is a port lagging its ecosystem's latest release. Also flag *intra-port* drift (mixed versions within one port, or a runtime package in `devDependencies`).
- **Stale upstream prose** — "hand-write the Spring controller" (Java/Kotlin) is out of date; trust `meta gen --list`, not stale prose.
- **Include-only templates are not duplicates.** A prompt template whose body is only `{{> partial }}` includes is already the shared-partial fix the template-duplication check recommends. Identical stubs are the shape to want, never a byte-identical cluster to report (`references/prompt-anti-patterns.md`, template-text duplication).
- **An owned generator copy with lines of its own is not thereby customized.** The listing prints `DIFFERS` for a deliberate customization and for a copy stale by version, and counts lines the reference later removed as "of your own". Check the copy's history before calling either (Phase 0, owned-generators check).
- **An enum-member literal at a position the type checker verifies is not drift.** A value narrowed by the generated union or enum type breaks the build when the member is renamed. Signature 13 scores only a literal in a position nothing checks: a string-typed parameter, raw SQL, a JSON payload, an event name (`references/drift-signatures.md`).
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ classify it (using the classification scheme in `SKILL.md`) and route the cutove
- **`field.date` / `field.time` / `field.timestamp`** (`@autoSet`) — hunt hand-stamped
`createdAt` / `updatedAt` assignments and ad-hoc temporal parsing; `@autoSet` stamps them.
- **`field.enum`** (`@values`) — hunt hand-written TS unions, language `enum`s, or DB
`CHECK ... IN (...)` lists that restate a modeled enum's members.
`CHECK ... IN (...)` lists that restate a modeled enum's members. Two more hunts on the same
capability: a member or event name typed as a hand string beside the generated constant
(`drift-signatures.md`, signature 13), and a status-like enum column written without a
condition on its current value (`surface-review-axes.md`, G3: every write conditional, through
the port's ORM with generated constants, never raw SQL).
- **`field.uuid`** — hunt UUIDs typed as bare strings / hand-validated.
- **`field.object`** (`@objectRef`, `@storage`) — hunt hand-flattened owned columns or
hand-rolled jsonb (de)serialization the `@storage` mode (`flattened`/`jsonb`/`subdocument`)
Expand Down
17 changes: 15 additions & 2 deletions agent-context/skills/metaobjects-audit/references/csharp.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ rejected (exit 2).
| `FromSqlInterpolated(` outside `.g.cs` | stored-proc call — candidate for the `callable` generator |
| `// keep in sync with` / `// mirrors the` | second-source-of-truth comment — always a finding |
| `HasPrecision(` hand-coded | `field.decimal` with `@precision`/`@scale` drives this from the `entity` generator |
| a member of a generated enum typed as a hand string (`"Error"` in raw SQL, a JSON body, a topic or event name, a `switch` over a `string`) beside the generated enum constant | **signature 13.** Value-first from each enum's `@values`. A value typed as the generated C# enum is resolved and exempt; a `string` position, SQL text or a payload is compared and scores |
| a status-like column written with a property assignment then `SaveChanges`, or `ExecuteSqlRaw("UPDATE ... SET Status")`, with no condition on the current value | **axis G3.** Guard it through EF Core: `Where(x => x.Id == id && x.Status == <from>)` then `ExecuteUpdateAsync`, with the generated enum constant, and check the affected-row count. Never raw SQL |
| a table/column string in raw-SQL EF calls, or `nameof(Entity.Prop)` standing in for a column | second spelling of a declared physical name — reference `<Entity>Names.g.cs` (`AuthorNames.SourcePrimaryTable` / `<Field>Column`, emitted when `codegen/Generators.cs` lists `names` — `Names.Name` is the OBJECT's name, not the table); an EF property inside LINQ is the typed handle — correct |

---
Expand All @@ -92,8 +94,19 @@ identical too, proves a copy unmodified; that row is a **finding**: point the
(`dotnet meta eject --undo` removes every identical copy and prints the edit). `DIFFERS`
with `0 of your own` rules out added lines, not removed ones: confirm from `git log -p` on
the copy that nothing changed after the eject, else report it as **possibly customized:
check its history**. A copy with lines of its own, or an edited runtime file, is a
customization; check only how far behind it is. Count lines for the Phase 0 total: lines in
check its history**.

**`DIFFERS` is printed for BOTH a deliberate customization and a copy gone stale by version**,
so the listing cannot tell them apart, and a copy with lines of its own is NOT thereby a
customization: `M of your own` counts every line absent from today's reference, so after the
reference moved on, a line it removed or rewrote reads as "yours" and is an older upstream
line. Run the history check on any `DIFFERS` row, `M` above 0 included: lines the copy's later
commits added are the project's own; lines already in the eject commit are upstream's and
count as behind. Report two figures, `<n> lines behind the reference` and `<m> lines of the
project's own (confirmed from history)`. A copy whose own lines history does not confirm is
**possibly customized or stale: check its history**; one that is stale by version is a finding
(`owned-generator-stale`): re-eject at the current version or return to the stock entry.
Count lines for the Phase 0 total: lines in
copies proven unmodified, over all lines under `codegen/generators/` and `codegen/runtime/`.

A generator the project WRITES is an `IGenerator` listed in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- 10. N declarations of one field across objects
- 11. A metadata-derived value spelled a second time by hand
- 12. An open JSON bag whose readers agree on a shape
- 13. An enum member or event name typed by hand beside the generated constant

Per finding: `file:line` → what → generated-equivalent exists? → recommendation.

Expand Down Expand Up @@ -55,3 +56,12 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
**Do NOT flag:** a bag whose readers genuinely pass it through (logged, echoed, stored, forwarded to a third party) without reading a key; a third-party or LLM raw response whose shape is not the adopter's to declare; an **array of scalars**, which is `isArray: true` on the base subtype and never an object; a bag the metadata's own comment justifies as open with a reason.

**Remedy — a ladder, chosen by the WRITER's declared type, and every rung gives the consumer a TYPED HANDLE rather than a string to get and set.** `list[X]` ⇒ the base subtype + `isArray: true`. `dict[str, X]` with a known `X` ⇒ **`field.map` + `@objectRef`** (a value object) or `@valueType` (a scalar) — registered vocabulary that emits `jsonb("col").$type<Record<string, X>>()` and `z.record(z.string(), X)`. **Check the CONSUMER before recommending this rung: `field.map` codegen now completes on all five ports** (Java types it `java.util.Map<String, V>`; C# emits the `Dictionary<string, V>` property and its EF jsonb storage mapping), **but no persistence- or api-contract-conformance fixture exercises `field.map` on ANY port, and the runtime persistence tier is uneven** — only Python's `ObjectManager` encodes a map, while `runtime-ts`, Java's OMDB and the Kotlin Exposed lane carry none. So where GENERATED CODE is the consumer, check the port before recommending this rung — nested map values are validated on every write path on every port as of 1.0.1 ([#362](https://github.com/metaobjectsdev/metaobjects/issues/362) — before that, Java skipped the cascade on TPH write paths and C# skipped it everywhere, silently accepting invalid values rather than erroring). **Check the adopter's pinned version before recommending this rung to a Java-TPH or C# consumer: on 1.0.0 the hole is open and they need their own boundary check.** Prefer a value object where a PORT RUNTIME must read the column back; a genuinely dynamic key set stays a bag. A serialized DTO / dataclass / `@Serializable` class ⇒ declare it as an `object.value` and point the field at it with `field.object` + `@objectRef` + `@storage: jsonb`. Only `dict[str, Any]` whose readers pin no key stays an open bag. **What the adopter GETS, per port** — this is the point of the conversion and worth stating in the finding: TypeScript emits the Drizzle column as `.$type<VO>()` plus the VO's own Zod schema and inferred type; Python annotates the field with the referenced VO's Pydantic model (and its `<Ref>Create` on the wire, so a nested violation still validates); Kotlin emits typed Exposed jsonb codecs backed by a shared Jackson `MetaJsonbMapper`. In none of them does the consumer touch a JSON string — that is what the open bag costs them today, since `unknown` forces every reader to cast. The column stays jsonb, so this is a codegen/contract change and not necessarily a migration; what changes is that `unknown` becomes a generated type, the casts delete, and the keys become metadata-owned — which also promotes their literals into drift signature 11's scope, where the `->>'key'` sites were previously exempt because the metadata owned nothing. **Verify the column type is genuinely unchanged before promising no migration**, and parity-gate each converted read against the raw-SQL one before deleting it. Convert one column at a time: the shape is a judgment per bag, and a wrong guess declares a contract the writers do not honor.

13. **An enum member or event name typed as a hand string beside the generated constant.** Signature 11 covers physical names; this is the same rule for a model's enum values and for the event or message names an enum member spells. A literal in non-generated code that EQUALS a member of a generated enum (or the name of a declared event) is a second spelling, and it is the commonest drift an estate that already uses the constants carries: the file imports `CouncilStatusEnum` and uses `CouncilStatusEnum.enum.complete` on one line, then writes `status: "error"` on the next. The two spellings agree today and nothing ties them together. **Hunt VALUE-first, from the declaration outward:** list every enum's members from the metadata (`@values` on each `field.enum`, plus the member names of any enum-backed event or message type), then grep the non-generated tree, config and fixtures included, for each member as a quoted literal. Distinctive members (`"edited_approved"`, `"awaiting_review"`) score on the hit; generic ones (`"error"`, `"done"`, `"active"`) only by reading the site, because the same word may belong to something the metadata does not own. **A file that already imports the enum's generated constant is the strongest tell**: its author knew the constant existed, so a bare literal of another member of that enum in the same file is almost never intentional. Count those first.

**Apply signature 11's discriminator, then say which side of it the site is on.** Rename the member in metadata and regenerate. Where the site is RESOLVED, the regeneration breaks it before anything runs and it is not a finding: a value narrowed by the generated union (`export type <Entity><Field> = "a" | "b"` on TypeScript, a `Literal[...]` annotation on Python, a generated `enum` type on Java, Kotlin and C#) at a position the compiler or type checker actually checks. Where the site is COMPARED, it survives the regeneration and fails later, on data: a literal in a string-typed position (a `Record<string, unknown>`, a `string` parameter, a loosely typed object spread into a `.set(...)`, a `dict`), in raw SQL (`WHERE status = 'error'`), in a JSON payload or fixture, as a broker topic or an SSE / websocket event name, in a `switch` over a `string` that the type checker cannot exhaust, or in a comparison against a column read back as `string`. **Verify the position before scoring it, never the shape of the literal:** the TypeScript case that earned the exemption (332 narrowed literals, 0 findings) and the case that earned this signature (`status: "error"` beside `CouncilStatusEnum.enum.complete`) look identical in a grep, and only the type at the site separates them. If you cannot tell which side a site is on, read the declared type of the target; report it as unverified rather than skipping it.

**Do not flag:** a literal narrowed by the generated union or enum type at a checked position; a value the metadata does not own (an application-level state machine's label that merely coincides with a member, a page route, copy); a literal inside a generated file; and a test asserting the wire value of a member on purpose (a test that pins `"complete"` is a contract test and says so). A literal inside a `source.rdb` `@sql` body is metadata and is exempt, as in signature 11.

**Remedy:** reference the generated constant, which is the one rule for every port: the Zod enum's member (for example `CouncilStatusEnum.enum.complete`) or the generated union type on TypeScript, the generated `Literal` alias or enum on Python, the generated enum constant on Java, Kotlin and C#. In raw SQL the value moves to a bound parameter fed from the constant (never a literal inside the SQL text), and the table and column in that statement are signature 11's names artifact. Report the site count as `<adopted>/<total> sites` exactly as signature 11 does, and carry a deferral forward with its count. This signature needs no names artifact; it needs the generated enum type. If the project generates none, the un-wired `entity` generator is the first finding.

Loading
Loading