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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ generator catalog and the Atlas evaluation are 1.2 work.

### Added

- **Agent skills: a cost test for owned generators.** The `metaobjects-codegen` skill now says when not to write a generator: only when generator code plus
its generated output and added metadata is fewer lines than the hand-written code it
replaces, more instances of the shape are expected, or drift protection is the
point; a general-shape generator belongs in the library. The `metaobjects-audit` skill applies
the same test: a one-off hand-written layer that fails it is reported as acceptable, and any
generated-share figure must carry the net hand-written line change (a figure to report, not
the test) and is never a target.
Documentation only; no code or generator changed.
- **TypeScript: the `cube-model` reference generator (FR-044 Plan 4).** `cubeModel()` writes the
reporting vocabulary as Cube data-model files, `model/cubes/<Cube>.yml`: a cube for each
table-backed entity that declares dimensions, measures or segments, a join for each to-one
Expand Down
19 changes: 16 additions & 3 deletions agent-context/skills/metaobjects-audit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ description: Use when assessing how well a project has adopted MetaObjects — g
**Thesis.** Typed metadata is the durable spine; generated code is the disposable
artifact. Hand-writing a layer the metadata could own creates a second source of
truth for one fact — it will drift. This audit hunts those second sources of truth
and proposes folding them into the spine.
and proposes folding them into the spine — where the generator code plus its generated output
and added metadata is fewer lines than the hand-written code it replaces, more of that shape
are coming, or silent drift is the risk. A one-off layer
that fails that cost test is acceptable, not a defect.

**Boundary — read-only.** Deliverables: `.metaobjects/adoption-audit.json` (machine-readable
findings) + a rendered Markdown report. The audit **never edits code, never authors
Expand Down Expand Up @@ -47,7 +50,11 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid

**Census:** generated output line/file counts + metadata/owned-generator lines.
Compute **leverage ratio** = `generated_lines / (metadata_lines + generator_lines)`;
healthy = multi-× (example: ~4.7k spine → ~15.7k generated ≈ 3.3×).
healthy = multi-× (example: ~4.7k spine → ~15.7k generated ≈ 3.3×). **Never report a
generated-share or leverage figure alone, and never as a target:** pair it with the **net
hand-written line change**, a figure to report and not the cost test (hand-written lines
removed minus generator and metadata lines added). A share can rise while total hand-written
code grows.

**Coverage matrix:** per entity/projection/value — query helpers? view? routes? UI?
The gap between "modeled + query helpers" and "has view + route + UI" is the headline
Expand Down Expand Up @@ -314,6 +321,7 @@ with full force). Full guidance: `docs/features/downstream-metadata-decisions.md
| **CODEGEN CANDIDATE (high)** | Standard CRUD/list/form over a modeled or modelable entity. | Author the view + generate; parity-gate. |
| **CODEGEN CANDIDATE (partial)** | Generatable data layer, bespoke presentation. | Generate data layer; keep viz hand-written. |
| **DYNAMIC-RUNTIME CANDIDATE** | Behavior that could be metadata-driven at runtime. | Assess runtime-metadata feasibility. |
| **ACCEPTABLE (one-off)** | A hand-written layer the metadata could own, but an owned generator for it would cost more lines than it replaces, no more instances are expected, and nothing drifts silently (it reads typed generated properties). | Report as acceptable. Not a defect, not a recommendation to generate. Re-class if it recurs or drift appears. |
| **BESPOKE (keep)** | Genuine custom: irreducible SQL (recursive CTEs, window functions, set ops — NOT plain count/sum/avg rollups, which are `origin.aggregate` on a projection), graph, SSE, auth, search, viz. | Leave hand-written — still import generated types. |
| **VOCAB CANDIDATE (advisory)** | A recurring, closed hand-coded pattern (parallel integration modules / an ad-hoc string discriminator) that project-registered vocabulary — a custom subtype/attr via a provider — plus a small owned generator would own. | Propose the provider + `metadata_sketch`; apply the ADR-0037 ordered test; advisory only. |

Expand Down Expand Up @@ -385,7 +393,12 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
· new shape → **author a declarative template-spec / custom generator from the metadata**
· genuinely un-modelable → hand-write (still import the generated types).
- **Generator-gap check:** missing generators that block the biggest wins? Recommend per gap:
own + customize / author a template-spec / fix upstream / stopgap.
own + customize / author a template-spec / fix upstream / stopgap. **Cost-test every
recommendation to generate** (`metaobjects-codegen` → `references/owning-generators.md`): an
owned generator is worth it only if generator + output is smaller than the code it replaces,
more instances are expected, or drift protection is the point. A layer that fails is
ACCEPTABLE, and an owned generator larger than the code it replaced is itself a finding.
A general-shape generator belongs in the library, not in each app.
- **Verify the DB artifact, not just the types** — computed view columns may appear in the
contract but be dropped from the view DDL; the contract may lie.
- **An *undeclared* hand-authored DB view is invisible to `meta verify --db`.** A view that is
Expand Down
4 changes: 3 additions & 1 deletion agent-context/skills/metaobjects-codegen/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ exact config, generator names and commands for this project; read it. The other
or none emits it the way this application needs it. OpenAPI, JSON Schema, Zod schemas,
DTOs or a client for a framework nothing here ships, a service layer in the house style,
a data dictionary, a search-index mapping, test fixtures. That is a generator, and you
write it now, in this change. Do not hand-write the output (it drifts the day the model
write it now, in this change — once it passes the cost test in `references/owning-generators.md`
(smaller than the code it replaces, generator counted; more instances expected; or drift
protection is the point). Do not hand-write the output (it drifts the day the model
changes and nothing notices), do not wait for MetaObjects to ship it, and do not ask
permission: a generator in this repo is ordinary source you own.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ same target rules, and is drift-gated the same way. The runner adds no header to
on TypeScript, C# and Python none is needed, because the hash manifest records what was
written.

**Is it worth a generator? Run the cost test first.** Write an adopter-owned generator only
when at least one holds:

1. **It is smaller.** The generator code plus its generated output, plus the metadata it
adds, is fewer lines than the hand-written code it replaces.
2. **More instances are coming.** The app will keep adding entities, routes or events of this
shape, so the generator's cost is paid back as the model grows.
3. **Drift protection is the point.** A change to the model could pass the compiler and go
wrong silently (a runtime lookup by name, a string a consumer compares), and regeneration
plus `verify` is what catches it.

Otherwise hand-write it: a one-off layer that reads typed generated properties cannot drift
and is a fine place to stop. A generator with a general shape (usable by any app on this
model) belongs in the MetaObjects library, not copied into each app; that is an upstream
request, not a reason to grow your own.

Write a new generator when the *shape* needs to change. When a reference's shape is right
and only its *target* is wrong — a different framework than the reference emits for — eject
that generator and retarget it instead; see "Your framework isn't the default" below.
Expand Down Expand Up @@ -125,10 +141,10 @@ convention, not the method name.
a normal source file. Copy it and customize the copy (three-way merge preserves your
edits on regen), or customize the template a built-in renders from. Reach for a
custom generator when you want the change applied **consistently across every
entity** (the scale win); a one-off edit when it's genuinely one file.
entity** and it passes the cost test above; a one-off edit when it's genuinely one file.

**The decision ladder:** an output the model describes that no reference emits → write
a generator (the top of the skill) · a reference is close → eject it and customize the
a generator if it passes the cost test (the top of the skill) · a reference is close → eject it and customize the
copy · a reference fits → use it · only the genuinely un-modelable (business algorithms,
external calls) is hand-written outside codegen — and it still imports the generated
types.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ description: Use when assessing how well a project has adopted MetaObjects — g
**Thesis.** Typed metadata is the durable spine; generated code is the disposable
artifact. Hand-writing a layer the metadata could own creates a second source of
truth for one fact — it will drift. This audit hunts those second sources of truth
and proposes folding them into the spine.
and proposes folding them into the spine — where the generator code plus its generated output
and added metadata is fewer lines than the hand-written code it replaces, more of that shape
are coming, or silent drift is the risk. A one-off layer
that fails that cost test is acceptable, not a defect.

**Boundary — read-only.** Deliverables: `.metaobjects/adoption-audit.json` (machine-readable
findings) + a rendered Markdown report. The audit **never edits code, never authors
Expand Down Expand Up @@ -47,7 +50,11 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid

**Census:** generated output line/file counts + metadata/owned-generator lines.
Compute **leverage ratio** = `generated_lines / (metadata_lines + generator_lines)`;
healthy = multi-× (example: ~4.7k spine → ~15.7k generated ≈ 3.3×).
healthy = multi-× (example: ~4.7k spine → ~15.7k generated ≈ 3.3×). **Never report a
generated-share or leverage figure alone, and never as a target:** pair it with the **net
hand-written line change**, a figure to report and not the cost test (hand-written lines
removed minus generator and metadata lines added). A share can rise while total hand-written
code grows.

**Coverage matrix:** per entity/projection/value — query helpers? view? routes? UI?
The gap between "modeled + query helpers" and "has view + route + UI" is the headline
Expand Down Expand Up @@ -314,6 +321,7 @@ with full force). Full guidance: `docs/features/downstream-metadata-decisions.md
| **CODEGEN CANDIDATE (high)** | Standard CRUD/list/form over a modeled or modelable entity. | Author the view + generate; parity-gate. |
| **CODEGEN CANDIDATE (partial)** | Generatable data layer, bespoke presentation. | Generate data layer; keep viz hand-written. |
| **DYNAMIC-RUNTIME CANDIDATE** | Behavior that could be metadata-driven at runtime. | Assess runtime-metadata feasibility. |
| **ACCEPTABLE (one-off)** | A hand-written layer the metadata could own, but an owned generator for it would cost more lines than it replaces, no more instances are expected, and nothing drifts silently (it reads typed generated properties). | Report as acceptable. Not a defect, not a recommendation to generate. Re-class if it recurs or drift appears. |
| **BESPOKE (keep)** | Genuine custom: irreducible SQL (recursive CTEs, window functions, set ops — NOT plain count/sum/avg rollups, which are `origin.aggregate` on a projection), graph, SSE, auth, search, viz. | Leave hand-written — still import generated types. |
| **VOCAB CANDIDATE (advisory)** | A recurring, closed hand-coded pattern (parallel integration modules / an ad-hoc string discriminator) that project-registered vocabulary — a custom subtype/attr via a provider — plus a small owned generator would own. | Propose the provider + `metadata_sketch`; apply the ADR-0037 ordered test; advisory only. |

Expand Down Expand Up @@ -385,7 +393,12 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
· new shape → **author a declarative template-spec / custom generator from the metadata**
· genuinely un-modelable → hand-write (still import the generated types).
- **Generator-gap check:** missing generators that block the biggest wins? Recommend per gap:
own + customize / author a template-spec / fix upstream / stopgap.
own + customize / author a template-spec / fix upstream / stopgap. **Cost-test every
recommendation to generate** (`metaobjects-codegen` → `references/owning-generators.md`): an
owned generator is worth it only if generator + output is smaller than the code it replaces,
more instances are expected, or drift protection is the point. A layer that fails is
ACCEPTABLE, and an owned generator larger than the code it replaced is itself a finding.
A general-shape generator belongs in the library, not in each app.
- **Verify the DB artifact, not just the types** — computed view columns may appear in the
contract but be dropped from the view DDL; the contract may lie.
- **An *undeclared* hand-authored DB view is invisible to `meta verify --db`.** A view that is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ exact config, generator names and commands for this project; read it. The other
or none emits it the way this application needs it. OpenAPI, JSON Schema, Zod schemas,
DTOs or a client for a framework nothing here ships, a service layer in the house style,
a data dictionary, a search-index mapping, test fixtures. That is a generator, and you
write it now, in this change. Do not hand-write the output (it drifts the day the model
write it now, in this change — once it passes the cost test in `references/owning-generators.md`
(smaller than the code it replaces, generator counted; more instances expected; or drift
protection is the point). Do not hand-write the output (it drifts the day the model
changes and nothing notices), do not wait for MetaObjects to ship it, and do not ask
permission: a generator in this repo is ordinary source you own.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ same target rules, and is drift-gated the same way. The runner adds no header to
on TypeScript, C# and Python none is needed, because the hash manifest records what was
written.

**Is it worth a generator? Run the cost test first.** Write an adopter-owned generator only
when at least one holds:

1. **It is smaller.** The generator code plus its generated output, plus the metadata it
adds, is fewer lines than the hand-written code it replaces.
2. **More instances are coming.** The app will keep adding entities, routes or events of this
shape, so the generator's cost is paid back as the model grows.
3. **Drift protection is the point.** A change to the model could pass the compiler and go
wrong silently (a runtime lookup by name, a string a consumer compares), and regeneration
plus `verify` is what catches it.

Otherwise hand-write it: a one-off layer that reads typed generated properties cannot drift
and is a fine place to stop. A generator with a general shape (usable by any app on this
model) belongs in the MetaObjects library, not copied into each app; that is an upstream
request, not a reason to grow your own.

Write a new generator when the *shape* needs to change. When a reference's shape is right
and only its *target* is wrong — a different framework than the reference emits for — eject
that generator and retarget it instead; see "Your framework isn't the default" below.
Expand Down Expand Up @@ -125,10 +141,10 @@ convention, not the method name.
a normal source file. Copy it and customize the copy (three-way merge preserves your
edits on regen), or customize the template a built-in renders from. Reach for a
custom generator when you want the change applied **consistently across every
entity** (the scale win); a one-off edit when it's genuinely one file.
entity** and it passes the cost test above; a one-off edit when it's genuinely one file.

**The decision ladder:** an output the model describes that no reference emits → write
a generator (the top of the skill) · a reference is close → eject it and customize the
a generator if it passes the cost test (the top of the skill) · a reference is close → eject it and customize the
copy · a reference fits → use it · only the genuinely un-modelable (business algorithms,
external calls) is hand-written outside codegen — and it still imports the generated
types.
Loading
Loading