Repository navigation
Commit 07fcf77
FR-037 R1. `@readOnly` (boolean) is retired across 18 registry entries and
replaced by `@mutability: readWrite | writeOnce | readOnly` on field.base,
registered by the CORE field provider. A legacy `@readOnly` fails a strict load
with ERR_UNKNOWN_ATTR; there is no deprecation shim under ADR-0023.
WHY AN ENUM, not a second boolean: readOnly and writeOnce are mutually
exclusive modes of ONE axis — who may write, and when. One enum makes the
illegal pair unrepresentable and gives inheritance a total order
(readWrite < writeOnce < readOnly), so "may only tighten" is an INDEX
comparison over the declaration order rather than a lookup table. A test pins
that ordering, because reordering the array would silently invert the rule.
Five rules, all five ports, each with a cross-port fixture or per-port unit test:
ERR_MUTABILITY_AUTOSET_CONFLICT (new) — @autoset with writeOnce or readOnly.
@autoset already says the SERVER supplies the value; @mutability says who
may write it. The boolean era left readOnly x @autoset REPRESENTABLE but
unvalidated — the enum closes both arms with one rule. Proven live in all
four ports by sabotaging the fixture and watching each go red.
ERR_MUTABILITY_DOWNGRADE (new) — replaces ERR_READONLY_DOWNGRADE, renamed
because the rule now spans three modes and a READONLY name would misdescribe
a writeOnce -> readWrite loosening.
ERR_READONLY_ASSIGNED_PRIMARY — KEEPS its name. The condition really is
readOnly-specific, and the asymmetry is the enum's whole justification:
`writeOnce` on an assigned primary key is LEGAL and is in fact the natural
declaration for one — a thing the boolean vocabulary could not say at all.
WARN_MUTABILITY_VALUE_OBJECT (new) — replaces WARN_READONLY_VALUE_OBJECT.
WARN_MUTABILITY_READONLY_HOST (new) — writeOnce on a projection / read-only
@kind. Benign: inert, not wrong, and a projection may inherit it.
writeOnce leaves the UPDATE shape and ONLY the update shape, through each
port's existing excluded-settable-set seam (the one @autoset:onCreate already
uses): TS updateSchemaFields + the combined zod walk, Python _emit_patch_lines,
Java SpringDtoGenerator.settableFields (vanilla AND the TPH overload — stated
twice deliberately, per the 0.19.4 lesson that TPH is a separate code path),
Kotlin patchSettableFields, C# SetAfterSaveBehavior(Ignore).
D1 VERIFIED, not assumed: a presented writeOnce key on PATCH is STRIPPED, not
400'd. The emitted <Entity>UpdateSchema is a plain z.object() (never .strict()),
Zod drops unknown keys, and mountUpdateRoute safeParses that same schema — the
TPH discriminator is additionally hand-stripped after parse, the same
convention. Rejecting would break our own shipped client: the generated edit
form submits EVERY registered field (0.19.2 switched the resolver to
UpdateSchema on edit rather than diff-and-omit), so 400-on-present would fail
every save on every generated edit form for an entity carrying one.
That verification exposed a PRE-EXISTING doc defect, now corrected:
docs/features/generated-mutations.md PATCH-3 has claimed "at the HTTP tier an
unknown or non-settable key is a 400", which is false today for the
non-settable case and has been.
And a real CROSS-PORT DIVERGENCE, now closed: Python's <Entity>Patch INCLUDED
@readonly fields — `_emit_patch_lines` skipped only the PK and derived fields,
while TS excluded readOnly from UpdateSchema. Nobody hit it because @readonly
has zero adopter uses; it would have bitten the first one.
C# splits the two modes exactly where they differ. Both get
SetAfterSaveBehavior(Ignore) (they share the UPDATE half); only `readOnly` gets
a private setter. `writeOnce` keeps a PUBLIC setter deliberately — the caller
must supply it on insert, and a private setter there would make the mode
useless. So neither signal alone distinguishes the modes; only the pair does,
and the test asserts the pair.
The accessor lives on the field node in every port (MetaField.getMutability /
.Mutability, fieldMutability, field_mutability) and is RESOLVING per ADR-0039,
so a mode inherited through `extends` is honoured — an own-only read would
report readWrite for a field whose abstract parent declared readOnly and emit a
setter for a column nothing may write. Python's naming inversion is called out
at the call site (`attr()` is own-only there; `get_meta_attr()` resolves).
Fixtures: six field-readonly-* renamed to field-mutability-* and migrated;
three ADDED for writeOnce, which had no corpus coverage at all — and coverage
here is the whole story, because a peer scan of three adopter estates (826/8/75
metadata files, 14,860 field nodes) found ZERO uses of @readonly by two
independent methods with controls firing. So the corpus is the only thing that
exercises these modes, and ERR_MUTABILITY_AUTOSET_CONFLICT has no existing
instance anywhere to migrate — it is purely forward-looking. The coverage
ratchet fired on the rename and was regenerated only after confirming the diff
is exactly 13x readOnly->mutability plus U2's -1 subtype: no coverage lost.
Output equivalence pinned in TS and C#: an explicit @mutability: "readWrite"
emits byte-identically to declaring nothing.
Green: TS build + typecheck clean, 5939 tests 0 fail · C# four Passed! lines,
0 error CS, conformance 955 · Python 1723 · Java metadata 1461 /
codegen-spring 292 / codegen-kotlin 202 / render 316, BUILD SUCCESS,
ConformanceTest 576 · check-metamodel-version.mjs exit 0 (0.10 covers it) ·
ci-local.sh --only gates PASSED.
Still to land for FR-037 R1: the api-contract-conformance writeOnce scenario
(both lanes, every port, vanilla AND TPH).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
1 parent b99d5c3 commit 07fcf77
87 files changed
Lines changed: 2290 additions & 1017 deletions
File tree
- agent-context/skills/metaobjects-audit/references
- docs/features
- fixtures
- agent-context-conformance
- java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/references
- java-react/expected/.claude/skills/metaobjects-audit/references
- python/expected/.claude/skills/metaobjects-audit/references
- ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references
- ts-requirements/expected/.claude/skills/metaobjects-audit/references
- conformance
- error-field-mutability-autoset-conflict
- input
- error-field-mutability-downgrade
- input
- error-field-mutability-readonly-assigned-primary
- input
- field-mutability-inherited
- input
- field-mutability-on-view-projection
- input
- field-mutability-readonly-basic
- input
- field-mutability-tighten-on-subtype
- input
- field-mutability-write-once-assigned-primary
- input
- field-mutability-write-once
- input
- metamodel-docs/expected
- types
- registry-conformance
- server
- csharp
- MetaObjects.Codegen.Tests
- MetaObjects.Codegen/Generators
- MetaObjects
- Core/Field
- Loader
- Meta
- SpecMetamodel
- java
- codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin
- codegen-spring/src/main/java/com/metaobjects/generator/spring
- metadata/src/main/java/com/metaobjects
- field
- loader
- util
- python
- src/metaobjects
- codegen/generators
- loader
- meta/core/field
- spec_metamodel
- tests/codegen/golden
- extends/expected
- nested-array/expected
- scalars/expected
- vanilla/expected
- typescript/packages
- codegen-ts
- src
- generators
- templates
- test/templates
- metadata
- src
- core/field
- loader
- test
- spec/metamodel
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | | - | |
54 | | - | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
55 | 58 | | |
56 | 59 | | |
57 | 60 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
36 | | - | |
37 | | - | |
38 | | - | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
39 | 50 | | |
40 | 51 | | |
41 | 52 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | | - | |
54 | | - | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
55 | 58 | | |
56 | 59 | | |
57 | 60 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | | - | |
54 | | - | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
55 | 58 | | |
56 | 59 | | |
57 | 60 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | | - | |
54 | | - | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
55 | 58 | | |
56 | 59 | | |
57 | 60 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | | - | |
54 | | - | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
55 | 58 | | |
56 | 59 | | |
57 | 60 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | | - | |
54 | | - | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
55 | 58 | | |
56 | 59 | | |
57 | 60 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | | - | |
61 | | - | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
62 | 63 | | |
63 | 64 | | |
64 | 65 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
0 commit comments