Skip to content

Commit 18fb4bb

Browse files
authored
Merge pull request #276 from metaobjectsdev/feat/role-shrink-and-210
feat(metamodel): consolidate projection/payload vocabulary (#210, #212)
2 parents 726f9de + 03a7eed commit 18fb4bb

106 files changed

Lines changed: 2124 additions & 358 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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,73 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
## [Unreleased]
99

10+
## [0.21.0] — npm `0.21.0` · PyPI `0.21.0` · NuGet `0.21.0` · Maven `7.21.0`
11+
12+
> ### ⚠️ BREAKING FOR METADATA AUTHORS — three changes make previously-valid metadata fail to load
13+
>
14+
> This is the pre-1.0 breaking slot (MINOR), not a patch, **specifically so it is not
15+
> auto-adopted**: on a caret range `^0.20.x` resolves `<0.21.0`, so you pick this up only by
16+
> deliberately bumping your range. Read
17+
> **[the migration guide](docs/features/migrations/value-assembly-origins-and-source-role-shrink.md)**
18+
> before upgrading — it carries the exact loader errors and a rewrite rule for each change.
19+
>
20+
> 1. **Assembly origins are illegal on an `object.value`.** `origin.aggregate`, `origin.computed`,
21+
> `origin.collection` and `origin.first` on a value-hosted field now fail with
22+
> `ERR_SUBTYPE_RULE_VIOLATION`. **`origin.passthrough` is unaffected** and stays legal on a value.
23+
> 2. **`source.rdb @role` accepts only `primary | replica`.** `index`, `cache`, `publish` and
24+
> `mirror` are retired to reserved-not-registered; a legacy use fails with `ERR_BAD_ATTR_VALUE`.
25+
> 3. **A payload's nested `field.object @objectRef` must target an `object.value`.** Previously
26+
> TypeScript, C# and Python accepted a non-value target *and emitted code from it*; it now fails
27+
> at load in all four loaders.
28+
29+
### Changed — assembly origins live on projections, not on values (#210) — BREAKING
30+
31+
**Generated-output change — regenerate to pick it up; three-way merge preserves hand edits.**
32+
33+
The durable rule this encodes: **"passthrough on a value is lineage; assembly origins live on
34+
projections."** An `object.value` is pure shape — constructed by a caller or by embedding, never
35+
populated from a store. Deriving a field by rolling up, computing over, or collecting from a backing
36+
store is what an `object.projection` is *for*, and letting a value do it blurred the one distinction
37+
the taxonomy exists to draw (ADR-0028).
38+
39+
`origin.passthrough` deliberately **stays legal** on a value. There it is FR-015 *parameter lineage*
40+
— it is how a stored-proc argument's type is bound to its source column — and the loaders already
41+
drew exactly that line via the FR-024 B5 value-host exemption. Retiring it would have silently
42+
dropped the `ERR_PASSTHROUGH_TYPE_MISMATCH` check on proc arguments.
43+
44+
**The migration path is additive:** `@payloadRef` and `@responseRef` now accept a **sourceless
45+
`object.projection`** as well as an `object.value`. A payload that was assembling values re-hosts as
46+
a projection and keeps its origins. See the migration guide for the rewrite, including the caveat
47+
that adding an `extends` anchor can flip a field's optionality (`@required` inherits through it).
48+
49+
Implemented as one named subtype set (`ASSEMBLY_ORIGIN_SUBTYPES`) hoisted above the origin dispatch
50+
in every loader, so cross-port coverage is a property of the constant rather than of four separate
51+
branches. No new vocabulary, no new error codes; `object.value`'s registry `rules` and `description`
52+
strings change to drop the retired "by assembly" construction mode.
53+
54+
### Changed — `source.rdb @role` shrinks to `primary | replica` (#212) — BREAKING
55+
56+
`index`, `cache`, `publish` and `mirror` are **reserved-not-registered** — documented on the axis,
57+
absent from the registry (the ADR-0040 treatment). The re-entry bar is recorded in ADR-0007
58+
Amendment 2: *a role member enters the registry only when a shipping consumer dispatches on it.*
59+
60+
The justification is that **no port ever built the dispatch these members anticipated.** Across all
61+
five, every read of `@role` is an equality test against `primary`; Java's OMDB has zero role usage,
62+
and Kotlin's and Python's write-through read paths are explicitly role-agnostic, finding the replica
63+
by read-only `@kind`. The consumed information content was one bit, which makes the four unused
64+
members indistinguishable from `replica` to every consumer. An adopter scan across this repo, the
65+
public reference app and downstream consumer models found zero uses.
66+
67+
Pruning now is the reversible direction: removing a registered member post-1.0 would be a 2.0 event,
68+
whereas re-adding a reserved one is additive.
69+
70+
### Fixed — a raw `NUL` byte made a TypeScript source file invisible to search tooling
71+
72+
`constraint-merge.ts` used a NUL as a composite-key join delimiter — sound technique, but written as
73+
a literal `0x00` byte rather than an escape, which made the whole file test as *binary*. `file(1)`
74+
reported it as `data` and binary-skipping search tools silently ignored it. Runtime-identical fix;
75+
the companion instance in the Java port shipped in `0.20.16`. These were the last two in the repo.
76+
1077
## [0.20.16] — npm `0.20.16` · PyPI `0.20.16` · NuGet `0.20.16` · Maven `7.20.16`
1178

1279
**Coordinated across all four registries.** The fix below is Kotlin, Python and Java, so

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,9 +438,9 @@ Preserve the following contracts exactly across all language ports:
438438

439439
**Metamodel subtype vocabularies (must be identical across languages):** the `registry-conformance` gate (`fixtures/registry-conformance/`) is the structural enforcer of this rule — each port emits its registry as a canonical manifest byte-matched to `expected-registry.json`. **All five ports (TS / C# / Java / Kotlin / Python) are live + green** (SP-G Java/Kotlin reconciliation complete; the JVM runners compose from the defined metamodel provider set so codegen-base/om classpath SPI does not pollute the measured vocabulary). See `fixtures/registry-conformance/README.md`.
440440
- Filter operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `like`, `isNull`
441-
- Object subtypes: `entity` (owns data: own identity, writable sources, lifecycle), `value` (pure shape: NO identity, NO source, ever; constructed — by caller/assembly/embedding — never populated; may `extends` entity fields for shape), `projection` (derived read-only representation: fields `extends`-bound / origin-derived / self-declared-under-external-assembly, all read-only at subtype level; identity optional and MUST extend an entity identity; sources restricted to read-only `@kind`s; the declared field set IS the exposure — inclusive list, fail-closed). A field carrying `origin.*` is derived ⇒ read-only wherever it lives (incl. on entities). An entity's primary source must be a writable `@kind` (read-only kinds only in read role). See [ADR-0028](spec/decisions/ADR-0028-object-taxonomy-projection-value-purity.md). (FR-024 Phase E — `object.projection`/`value` are registered in `expected-registry.json` and the projection/value validation passes [identity pass-through, value-purity, projection-licensing, `@via` inference/cardinality, extends/origin agreement, derived-field providability] are enforced cross-port in all 5 ports. The **B4b** entity-primary-source-readonly cutover [the "writable `@kind`" clause above — `ERR_ENTITY_PRIMARY_SOURCE_READONLY`] + the projection codegen fan-out (read-only DTOs for view-kind projections; FR-015 proc-callables for proc-kind projections; api-docs label `object.projection` units as `projection` and document their generated `<Name>Dto`) are now shipped cross-port; the remaining FR-024 work is the declared-API surface — tracked in #10.)
441+
- Object subtypes: `entity` (owns data: own identity, writable sources, lifecycle), `value` (pure shape: NO identity, NO source, ever; constructed — by caller/embedding — never populated; may `extends` entity fields for shape; a value-hosted field may carry `origin.passthrough` but never an assembly origin), `projection` (derived read-only representation: fields `extends`-bound / origin-derived / self-declared-under-external-assembly, all read-only at subtype level; identity optional and MUST extend an entity identity; sources restricted to read-only `@kind`s; the declared field set IS the exposure — inclusive list, fail-closed). A field carrying `origin.*` is derived ⇒ read-only wherever it lives (incl. on entities). An entity's primary source must be a writable `@kind` (read-only kinds only in read role). See [ADR-0028](spec/decisions/ADR-0028-object-taxonomy-projection-value-purity.md). (FR-024 Phase E — `object.projection`/`value` are registered in `expected-registry.json` and the projection/value validation passes [identity pass-through, value-purity, projection-licensing, `@via` inference/cardinality, extends/origin agreement, derived-field providability] are enforced cross-port in all 5 ports. The **B4b** entity-primary-source-readonly cutover [the "writable `@kind`" clause above — `ERR_ENTITY_PRIMARY_SOURCE_READONLY`] + the projection codegen fan-out (read-only DTOs for view-kind projections; FR-015 proc-callables for proc-kind projections; api-docs label `object.projection` units as `projection` and document their generated `<Name>Dto`) are now shipped cross-port; the remaining FR-024 work is the declared-API surface — tracked in #10.)
442442
- Source subtypes: `rdb` (paradigm; ADR-0007). The pre-v2 `dbTable`/`dbView` subtypes are RETIRED — `source.rdb` + `@kind: table|view|materializedView|storedProc|tableFunction` is the form, with read-only-ness derived from `@kind`. Multi-source via `@role` (exactly one `primary` per object). Source physical name = `@table` (NOT `@name`); field physical name = `@column` (renamed from `@dbColumn`). Referential actions on relationships: `@onDelete` / `@onUpdate`.
443-
- Origin subtypes: `passthrough`, `aggregate`
443+
- Origin subtypes: `passthrough`, `aggregate`, `collection`, `computed`, `first` (concrete; `base` is the abstract root). `passthrough` is legal on an `object.value`-hosted field (FR-015 parameter lineage); the four assembly origins (`aggregate`/`computed`/`collection`/`first`) live on `object.projection` only — a value-hosted assembly origin is `ERR_SUBTYPE_RULE_VIOLATION` (#210).
444444
- Relationship subtypes: `association`, `aggregation`, `composition`. Cardinality via `@cardinality: one|many`; target via `@objectRef`. **M:N (FR-018) slim vocabulary:** `@cardinality: "many"` + `@objectRef` (target) + `@through` (the junction/through entity — a third entity that MUST declare two `identity.reference` children, one per FK side). The relationship's FK fields are **derived** from those references (the `identity.reference` SSOT for FK direction), never restated. `@sourceRefField` (optional) disambiguates a *directed* self-join by naming the source-side FK field on the junction (the other reference is the target side). `@symmetric` (optional boolean) marks an *undirected* self-join (union-on-read) — valid only when `@objectRef` == the declaring entity, and mutually exclusive with `@sourceRefField`. The pre-FR-018 `@joinEntity`/`@joinFields` attrs are REMOVED. Validation errors: symmetric-on-hetero / symmetric+sourceRefField → `ERR_BAD_ATTR_VALUE`; junction-missing-two-references / sourceRefField-not-matching / M:N-attr-on-1:N → `ERR_INVALID_RELATIONSHIP`.
445445
- Index subtypes: `index.lookup` (non-unique retrieval index; uniqueness is encoded in the **type**: `identity.secondary` = unique alternate key, `index.lookup` = non-unique; `@unique` is REMOVED from `identity.secondary``ERR_UNKNOWN_ATTR` on any legacy `@unique`). RDB-physical escapes `@using`/`@expr`/`@where`/`@orders` are registered by the db provider on **both** `identity.secondary` and `index.lookup`. `index.fulltext` / `index.vector` / `index.spatial` are reserved on the subtype axis — documented, NOT registered (YAGNI + 1.0 vocab freeze). See [ADR-0040](spec/decisions/ADR-0040-index-type-and-secondary-key-purity.md).
446446
- Layout subtypes: `dataGrid`

agent-context/skills/metaobjects-audit/SKILL.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,12 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
330330
## Prompt anti-patterns (hunt per site; classify: fully-modeled / partial / fully-inline)
331331

332332
- Inline prompt strings (triple-quoted / template-literal constants in service code).
333-
- Untyped payloads (`str.format(**dict)` / f-strings / ad-hoc dicts) — payload should be an
334-
`object.value` with `origin.*` (`passthrough` / `aggregate` / `collection`) fields.
333+
- Untyped payloads (`str.format(**dict)` / f-strings / ad-hoc dicts) — payload should be a declared
334+
shape: an `object.value` (caller-supplied fields; `origin.passthrough` only — FR-015 parameter
335+
lineage) or, when fields derive by `aggregate` / `collection` / `computed` / `first`, a
336+
**sourceless `object.projection`** carrying those origins (#210 — assembly origins on an
337+
`object.value` fail load with `ERR_SUBTYPE_RULE_VIOLATION`; `@payloadRef` accepts the sourceless
338+
projection).
335339
- Silent-degradation hack (`try/except KeyError` or `?? ''` around formatting) — flag every instance.
336340
- Hand-rolled output parsing (regex / XML / ad-hoc JSON) vs declared `template.output` +
337341
generated `parse*` / `safeParse*` / `extract*` parser — **generated in all five ports**

agent-context/skills/metaobjects-prompts/SKILL.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Both carry the generic attrs:
2828

2929
| Attr | Required | Purpose |
3030
|---|---|---|
31-
| `@payloadRef` | yes | the `object.value` declaring the payload shape |
31+
| `@payloadRef` | yes | the `object.value` — or sourceless `object.projection` (#210) — declaring the payload shape |
3232
| `@textRef` | yes for `template.prompt` and a `template.output @kind: document` (the default) — a `template.output @kind: email` carries **no** `@textRef`; it uses `@subjectRef` + `@htmlBodyRef` (+ optional `@textBodyRef`) instead | the 2-layer logical text reference `group/source`, resolved by a provider |
3333
| `@format` | no | `text` (default) / `html` / `xml` / `csv` / `json` / `markdown` / `spreadsheet` — drives the escaper |
3434
| `@maxChars` | no | build-time size budget |
@@ -41,7 +41,8 @@ selects how the output-format prompt fragment presents the payload shape to an L
4141
(see "the output-format prompt fragment" below); `@requiredTags` names output tags
4242
the rendered text must contain (`verify` checks it) on both subtypes.
4343
`template.prompt` additionally carries `@responseRef` — naming the response
44-
value-object the prompt expects, for typed LLM-call trace derivation.
44+
shape (an `object.value` or sourceless `object.projection`, #210) the prompt
45+
expects, for typed LLM-call trace derivation.
4546

4647
A third, structurally different subtype is also registered core vocabulary:
4748
**`template.toolcall`** (`@toolName` + `@payloadRef`, ADR-0011) — a vendor-agnostic
@@ -50,21 +51,26 @@ LLM tool-call envelope with no renderable text body (the body IS the
5051
`@format` attrs above). The vocabulary exists today; MCP exposure of declared
5152
prompts/tools is roadmap, not shipped — don't promise it.
5253

53-
## The payload is an `object.value` you declare
54+
## The payload is a shape you declare — an `object.value`, or a sourceless `object.projection`
5455

55-
The payload is **not** an entity — it's an `object.value` whose DECLARED fields ARE
56-
the prompt's typed shape. Every port's payload codegen is
56+
The payload is **not** an entity — it's a declared shape whose fields ARE the
57+
prompt's typed surface: an `object.value` (caller-supplied fields;
58+
`origin.passthrough` only — FR-015 parameter lineage), or a **sourceless
59+
`object.projection`** (#210 — no `source.*` child, own or inherited) when fields
60+
derive by assembly. Every port's payload codegen is
5761
**declared-type-authoritative (#270)**: a field's generated type comes only from its
5862
declared `field.<subType>` + `isArray` + `@objectRef`, and a nested payload is a
5963
declared `field.object @objectRef` to another `object.value` (`isArray: true` for a
60-
list). The caller supplies the field values at render time. An `origin.*` child on a
61-
payload field is IGNORED for typing — never author assembly origins (`aggregate` /
62-
`collection` / `computed` / `first`) on a payload VO. Derivation belongs to
63-
**projection** read models (`object.projection` over an entity), covered by the
64-
`metaobjects-authoring` skill and `docs/features/source-kinds.md`, not here.
64+
list — nested targets stay value-only, loader-enforced). The caller supplies the
65+
field values at render time. An `origin.*` child on a payload field is IGNORED for
66+
typing — and the assembly origins (`aggregate` / `collection` / `computed` /
67+
`first`) are ILLEGAL on an `object.value` host (`ERR_SUBTYPE_RULE_VIOLATION`, #210):
68+
an origin-derived payload lives on the sourceless projection, which `@payloadRef`
69+
accepts. Projections generally are covered by the `metaobjects-authoring` skill and
70+
`docs/features/source-kinds.md`, not here.
6571

6672
Declaring the payload shape is what makes payload bloat visible: adding a field to
67-
the prompt is a diff on the `object.value`, and `verify` catches template/payload
73+
the prompt is a diff on the declared shape, and `verify` catches template/payload
6874
drift at build time instead of letting a prompt silently degrade.
6975

7076
```json

docs/CONFORMANCE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ regenerate with `ls -d fixtures/<corpus>/*/ | wc -l`.
2525

2626
| Corpus | Fixtures | TS | Java | Kotlin | C# | Python |
2727
|---|---|---|---|---|---|---|
28-
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 262 ||| inherits via `metadata-ktx` |||
28+
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 270 ||| inherits via `metadata-ktx` |||
2929
| [`fixtures/yaml-conformance/`](../fixtures/yaml-conformance/) | 15 | 15 / 15 | 14 / 15 (1 ledgered: `yaml-quoted-leading-zero` — Java pipeline strips quotes off `"007"`) | inherits via Java | 14 / 15 (1 ledgered: `error-yaml-coerced-hex-in-string` — YamlDotNet doesn't coerce `0xFF`) | 15 / 15 |
3030
| [`fixtures/verify-conformance/`](../fixtures/verify-conformance/) | 31 ||| inherits via Java |||
3131
| [`fixtures/verify-strict-conformance/`](../fixtures/verify-strict-conformance/) | 1 ||||||
@@ -69,7 +69,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker.
6969

7070
## Fixture-to-doc mapping
7171

72-
### `fixtures/conformance/` — metamodel loader + canonical serializer (262)
72+
### `fixtures/conformance/` — metamodel loader + canonical serializer (270)
7373

7474
| Fixture prefix | Feature doc |
7575
|---|---|

docs/features/migrations/0.x-to-1.0.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,26 @@ covering projections, entities, values, and parameter refs alike.
9898
`origin.computed`'s job (`@expr`, shipped in #195); #159 adds further node kinds to that
9999
expression grammar.
100100

101-
## 7. Deprecated `codegen-ts/generators` export removed (at the 1.0 cut)
101+
## 7. Assembly origins leave `object.value`; nested payload targets are value-only; `@role` shrinks (shipped 0.21.0 / Maven 7.21.0)
102+
103+
Three coordinated breaking changes in one cut (#210 / #212):
104+
105+
- A field hosted on an `object.value` may no longer carry an **assembly origin**
106+
(`origin.aggregate` / `origin.computed` / `origin.collection` / `origin.first`)
107+
`ERR_SUBTYPE_RULE_VIOLATION`. Re-host the payload as a **sourceless
108+
`object.projection`**; `@payloadRef`/`@responseRef` now accept one.
109+
`origin.passthrough` on a value (FR-015 parameter lineage) stays legal.
110+
- A payload field's nested `field.object @objectRef` must resolve to an
111+
`object.value` (previously unconstrained by the loader; TS/C#/Python accepted
112+
and emitted from a non-value target) — `ERR_SUBTYPE_RULE_VIOLATION`.
113+
- `source.rdb @role` is exactly `primary | replica`; the former `index` /
114+
`cache` / `publish` / `mirror` members are retired (reserved-not-registered)
115+
`ERR_BAD_ATTR_VALUE`.
116+
117+
- **Full mechanical rewrites for all three:**
118+
[value-assembly-origins-and-source-role-shrink.md](value-assembly-origins-and-source-role-shrink.md).
119+
120+
## 8. Deprecated `codegen-ts/generators` export removed (at the 1.0 cut)
102121

103122
Importing the built-in generators from `@metaobjectsdev/codegen-ts/generators`
104123
(`entityFile` / `queriesFile` / `routesFile` / `barrel`) is **removed** at 1.0.

0 commit comments

Comments
 (0)