diff --git a/CHANGELOG.md b/CHANGELOG.md index 4899002eb..6757b8de1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,73 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +## [0.21.0] — npm `0.21.0` · PyPI `0.21.0` · NuGet `0.21.0` · Maven `7.21.0` + +> ### ⚠️ BREAKING FOR METADATA AUTHORS — three changes make previously-valid metadata fail to load +> +> This is the pre-1.0 breaking slot (MINOR), not a patch, **specifically so it is not +> auto-adopted**: on a caret range `^0.20.x` resolves `<0.21.0`, so you pick this up only by +> deliberately bumping your range. Read +> **[the migration guide](docs/features/migrations/value-assembly-origins-and-source-role-shrink.md)** +> before upgrading — it carries the exact loader errors and a rewrite rule for each change. +> +> 1. **Assembly origins are illegal on an `object.value`.** `origin.aggregate`, `origin.computed`, +> `origin.collection` and `origin.first` on a value-hosted field now fail with +> `ERR_SUBTYPE_RULE_VIOLATION`. **`origin.passthrough` is unaffected** and stays legal on a value. +> 2. **`source.rdb @role` accepts only `primary | replica`.** `index`, `cache`, `publish` and +> `mirror` are retired to reserved-not-registered; a legacy use fails with `ERR_BAD_ATTR_VALUE`. +> 3. **A payload's nested `field.object @objectRef` must target an `object.value`.** Previously +> TypeScript, C# and Python accepted a non-value target *and emitted code from it*; it now fails +> at load in all four loaders. + +### Changed — assembly origins live on projections, not on values (#210) — BREAKING + +**Generated-output change — regenerate to pick it up; three-way merge preserves hand edits.** + +The durable rule this encodes: **"passthrough on a value is lineage; assembly origins live on +projections."** An `object.value` is pure shape — constructed by a caller or by embedding, never +populated from a store. Deriving a field by rolling up, computing over, or collecting from a backing +store is what an `object.projection` is *for*, and letting a value do it blurred the one distinction +the taxonomy exists to draw (ADR-0028). + +`origin.passthrough` deliberately **stays legal** on a value. There it is FR-015 *parameter lineage* +— it is how a stored-proc argument's type is bound to its source column — and the loaders already +drew exactly that line via the FR-024 B5 value-host exemption. Retiring it would have silently +dropped the `ERR_PASSTHROUGH_TYPE_MISMATCH` check on proc arguments. + +**The migration path is additive:** `@payloadRef` and `@responseRef` now accept a **sourceless +`object.projection`** as well as an `object.value`. A payload that was assembling values re-hosts as +a projection and keeps its origins. See the migration guide for the rewrite, including the caveat +that adding an `extends` anchor can flip a field's optionality (`@required` inherits through it). + +Implemented as one named subtype set (`ASSEMBLY_ORIGIN_SUBTYPES`) hoisted above the origin dispatch +in every loader, so cross-port coverage is a property of the constant rather than of four separate +branches. No new vocabulary, no new error codes; `object.value`'s registry `rules` and `description` +strings change to drop the retired "by assembly" construction mode. + +### Changed — `source.rdb @role` shrinks to `primary | replica` (#212) — BREAKING + +`index`, `cache`, `publish` and `mirror` are **reserved-not-registered** — documented on the axis, +absent from the registry (the ADR-0040 treatment). The re-entry bar is recorded in ADR-0007 +Amendment 2: *a role member enters the registry only when a shipping consumer dispatches on it.* + +The justification is that **no port ever built the dispatch these members anticipated.** Across all +five, every read of `@role` is an equality test against `primary`; Java's OMDB has zero role usage, +and Kotlin's and Python's write-through read paths are explicitly role-agnostic, finding the replica +by read-only `@kind`. The consumed information content was one bit, which makes the four unused +members indistinguishable from `replica` to every consumer. An adopter scan across this repo, the +public reference app and downstream consumer models found zero uses. + +Pruning now is the reversible direction: removing a registered member post-1.0 would be a 2.0 event, +whereas re-adding a reserved one is additive. + +### Fixed — a raw `NUL` byte made a TypeScript source file invisible to search tooling + +`constraint-merge.ts` used a NUL as a composite-key join delimiter — sound technique, but written as +a literal `0x00` byte rather than an escape, which made the whole file test as *binary*. `file(1)` +reported it as `data` and binary-skipping search tools silently ignored it. Runtime-identical fix; +the companion instance in the Java port shipped in `0.20.16`. These were the last two in the repo. + ## [0.20.16] — npm `0.20.16` · PyPI `0.20.16` · NuGet `0.20.16` · Maven `7.20.16` **Coordinated across all four registries.** The fix below is Kotlin, Python and Java, so diff --git a/CLAUDE.md b/CLAUDE.md index c0812d512..01bea028c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -438,9 +438,9 @@ Preserve the following contracts exactly across all language ports: **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`. - Filter operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `like`, `isNull` -- 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 `Dto`) are now shipped cross-port; the remaining FR-024 work is the declared-API surface — tracked in #10.) +- 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 `Dto`) are now shipped cross-port; the remaining FR-024 work is the declared-API surface — tracked in #10.) - 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`. -- Origin subtypes: `passthrough`, `aggregate` +- 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). - 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`. - 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). - Layout subtypes: `dataGrid` diff --git a/agent-context/skills/metaobjects-audit/SKILL.md b/agent-context/skills/metaobjects-audit/SKILL.md index 8120a6030..38ed38c3e 100644 --- a/agent-context/skills/metaobjects-audit/SKILL.md +++ b/agent-context/skills/metaobjects-audit/SKILL.md @@ -330,8 +330,12 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend ## Prompt anti-patterns (hunt per site; classify: fully-modeled / partial / fully-inline) - Inline prompt strings (triple-quoted / template-literal constants in service code). -- Untyped payloads (`str.format(**dict)` / f-strings / ad-hoc dicts) — payload should be an - `object.value` with `origin.*` (`passthrough` / `aggregate` / `collection`) fields. +- Untyped payloads (`str.format(**dict)` / f-strings / ad-hoc dicts) — payload should be a declared + shape: an `object.value` (caller-supplied fields; `origin.passthrough` only — FR-015 parameter + lineage) or, when fields derive by `aggregate` / `collection` / `computed` / `first`, a + **sourceless `object.projection`** carrying those origins (#210 — assembly origins on an + `object.value` fail load with `ERR_SUBTYPE_RULE_VIOLATION`; `@payloadRef` accepts the sourceless + projection). - Silent-degradation hack (`try/except KeyError` or `?? ''` around formatting) — flag every instance. - Hand-rolled output parsing (regex / XML / ad-hoc JSON) vs declared `template.output` + generated `parse*` / `safeParse*` / `extract*` parser — **generated in all five ports** diff --git a/agent-context/skills/metaobjects-prompts/SKILL.md b/agent-context/skills/metaobjects-prompts/SKILL.md index 514735dc1..99abcd3de 100644 --- a/agent-context/skills/metaobjects-prompts/SKILL.md +++ b/agent-context/skills/metaobjects-prompts/SKILL.md @@ -28,7 +28,7 @@ Both carry the generic attrs: | Attr | Required | Purpose | |---|---|---| -| `@payloadRef` | yes | the `object.value` declaring the payload shape | +| `@payloadRef` | yes | the `object.value` — or sourceless `object.projection` (#210) — declaring the payload shape | | `@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 | | `@format` | no | `text` (default) / `html` / `xml` / `csv` / `json` / `markdown` / `spreadsheet` — drives the escaper | | `@maxChars` | no | build-time size budget | @@ -41,7 +41,8 @@ selects how the output-format prompt fragment presents the payload shape to an L (see "the output-format prompt fragment" below); `@requiredTags` names output tags the rendered text must contain (`verify` checks it) on both subtypes. `template.prompt` additionally carries `@responseRef` — naming the response -value-object the prompt expects, for typed LLM-call trace derivation. +shape (an `object.value` or sourceless `object.projection`, #210) the prompt +expects, for typed LLM-call trace derivation. A third, structurally different subtype is also registered core vocabulary: **`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 `@format` attrs above). The vocabulary exists today; MCP exposure of declared prompts/tools is roadmap, not shipped — don't promise it. -## The payload is an `object.value` you declare +## The payload is a shape you declare — an `object.value`, or a sourceless `object.projection` -The payload is **not** an entity — it's an `object.value` whose DECLARED fields ARE -the prompt's typed shape. Every port's payload codegen is +The payload is **not** an entity — it's a declared shape whose fields ARE the +prompt's typed surface: an `object.value` (caller-supplied fields; +`origin.passthrough` only — FR-015 parameter lineage), or a **sourceless +`object.projection`** (#210 — no `source.*` child, own or inherited) when fields +derive by assembly. Every port's payload codegen is **declared-type-authoritative (#270)**: a field's generated type comes only from its declared `field.` + `isArray` + `@objectRef`, and a nested payload is a declared `field.object @objectRef` to another `object.value` (`isArray: true` for a -list). The caller supplies the field values at render time. An `origin.*` child on a -payload field is IGNORED for typing — never author assembly origins (`aggregate` / -`collection` / `computed` / `first`) on a payload VO. Derivation belongs to -**projection** read models (`object.projection` over an entity), covered by the -`metaobjects-authoring` skill and `docs/features/source-kinds.md`, not here. +list — nested targets stay value-only, loader-enforced). The caller supplies the +field values at render time. An `origin.*` child on a payload field is IGNORED for +typing — and the assembly origins (`aggregate` / `collection` / `computed` / +`first`) are ILLEGAL on an `object.value` host (`ERR_SUBTYPE_RULE_VIOLATION`, #210): +an origin-derived payload lives on the sourceless projection, which `@payloadRef` +accepts. Projections generally are covered by the `metaobjects-authoring` skill and +`docs/features/source-kinds.md`, not here. Declaring the payload shape is what makes payload bloat visible: adding a field to -the prompt is a diff on the `object.value`, and `verify` catches template/payload +the prompt is a diff on the declared shape, and `verify` catches template/payload drift at build time instead of letting a prompt silently degrade. ```json diff --git a/docs/CONFORMANCE.md b/docs/CONFORMANCE.md index 8e5e01e94..1a1b14b0c 100644 --- a/docs/CONFORMANCE.md +++ b/docs/CONFORMANCE.md @@ -25,7 +25,7 @@ regenerate with `ls -d fixtures//*/ | wc -l`. | Corpus | Fixtures | TS | Java | Kotlin | C# | Python | |---|---|---|---|---|---|---| -| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 262 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ | +| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 270 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ | | [`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 | | [`fixtures/verify-conformance/`](../fixtures/verify-conformance/) | 31 | ✓ | ✓ | inherits via Java | ✓ | ✓ | | [`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. ## Fixture-to-doc mapping -### `fixtures/conformance/` — metamodel loader + canonical serializer (262) +### `fixtures/conformance/` — metamodel loader + canonical serializer (270) | Fixture prefix | Feature doc | |---|---| diff --git a/docs/features/migrations/0.x-to-1.0.md b/docs/features/migrations/0.x-to-1.0.md index 8119e1610..be148519e 100644 --- a/docs/features/migrations/0.x-to-1.0.md +++ b/docs/features/migrations/0.x-to-1.0.md @@ -98,7 +98,26 @@ covering projections, entities, values, and parameter refs alike. `origin.computed`'s job (`@expr`, shipped in #195); #159 adds further node kinds to that expression grammar. -## 7. Deprecated `codegen-ts/generators` export removed (at the 1.0 cut) +## 7. Assembly origins leave `object.value`; nested payload targets are value-only; `@role` shrinks (shipped 0.21.0 / Maven 7.21.0) + +Three coordinated breaking changes in one cut (#210 / #212): + +- A field hosted on an `object.value` may no longer carry an **assembly origin** + (`origin.aggregate` / `origin.computed` / `origin.collection` / `origin.first`) + — `ERR_SUBTYPE_RULE_VIOLATION`. Re-host the payload as a **sourceless + `object.projection`**; `@payloadRef`/`@responseRef` now accept one. + `origin.passthrough` on a value (FR-015 parameter lineage) stays legal. +- A payload field's nested `field.object @objectRef` must resolve to an + `object.value` (previously unconstrained by the loader; TS/C#/Python accepted + and emitted from a non-value target) — `ERR_SUBTYPE_RULE_VIOLATION`. +- `source.rdb @role` is exactly `primary | replica`; the former `index` / + `cache` / `publish` / `mirror` members are retired (reserved-not-registered) + — `ERR_BAD_ATTR_VALUE`. + +- **Full mechanical rewrites for all three:** + [value-assembly-origins-and-source-role-shrink.md](value-assembly-origins-and-source-role-shrink.md). + +## 8. Deprecated `codegen-ts/generators` export removed (at the 1.0 cut) Importing the built-in generators from `@metaobjectsdev/codegen-ts/generators` (`entityFile` / `queriesFile` / `routesFile` / `barrel`) is **removed** at 1.0. diff --git a/docs/features/migrations/value-assembly-origins-and-source-role-shrink.md b/docs/features/migrations/value-assembly-origins-and-source-role-shrink.md new file mode 100644 index 000000000..34979527f --- /dev/null +++ b/docs/features/migrations/value-assembly-origins-and-source-role-shrink.md @@ -0,0 +1,235 @@ +# Migrating value-hosted assembly origins (#210) and retired `@role` members (#212) + +This release line carries **three** coordinated breaking metamodel changes. All +three fail at **load time** with a clear error — nothing changes silently — and +each has a mechanical rewrite. + +1. **Assembly origins leave `object.value` (#210).** A field hosted on an + `object.value` may no longer carry `origin.aggregate`, `origin.computed`, + `origin.collection` or `origin.first`. Re-host the payload as a + **sourceless `object.projection`** — `@payloadRef`/`@responseRef` now accept + one. +2. **Nested payload targets are value-only, loader-enforced (#210).** A payload + field's `field.object @objectRef` must resolve to an `object.value`. + TypeScript, C# and Python previously accepted a non-value target here and + emitted a nested shape from it — that metadata now fails load. +3. **`source.rdb @role` shrinks to `primary | replica` (#212).** The + `index` / `cache` / `publish` / `mirror` members are retired + (reserved-not-registered, the ADR-0040 treatment). + +--- + +## 1. Assembly origins leave `object.value` (#210) + +### What changed + +The *assembly* origins — `origin.aggregate`, `origin.computed`, +`origin.collection`, `origin.first` — are now illegal on a field hosted by an +`object.value`, in all four loaders. **`origin.passthrough` stays legal on a +value**: there it declares FR-015 *parameter lineage* (e.g. a stored-proc +argument tracing back to an entity column), not an assembly path, and it keeps +its `ERR_PASSTHROUGH_TYPE_MISMATCH` type-preservation check. + +In exchange, the template-level payload references widen: +`template.* @payloadRef` and `@responseRef` now accept a **sourceless +`object.projection`** (no `source.*` child, own or inherited) in addition to an +`object.value`. A *sourced* projection as a payload target remains illegal. +**Nested** payload targets — a payload field's `field.object @objectRef` — stay +value-only. + +### Why + +The durable rule (ADR-0028, amended): **passthrough on a value is lineage; +assembly origins live on projections.** A value is a pure shape — constructed by +a caller or by embedding, never populated from a backing store. Rolling up, +computing, or collecting from related rows is *derivation*, and derivation is +what `object.projection` exists for. A payload that declares its fields' +derivations is a *read model assembled on the wire* — which is exactly a +sourceless projection. + +### The error you'll see + +``` +ERR_SUBTYPE_RULE_VIOLATION: value object 'acme::ai::AuthorReport' field 'postCount' +hosts origin.aggregate — assembly origins (aggregate, computed, collection, first) +live on object.projection; a value is constructed by a caller or by embedding, +never assembled from a backing store. Re-host this field on a sourceless +object.projection; origin.passthrough (FR-015 parameter lineage) remains legal +on a value (#210, ADR-0028) +``` + +### Rewrite rule + +Change the payload's host subtype from `object.value` to `object.projection`. +Fields, origins, and the template's `@payloadRef` all stay as they are. One +addition may be needed: an origin **without an explicit `@via`** derives its +base entity from the projection's extends anchors, so `extends`-bind at least +one field (or declare an extended identity) to name the base. + +**Before:** + +```jsonc +{ "object.value": { + "name": "AuthorReport", + "children": [ + { "field.string": { "name": "name", "children": [ + { "origin.passthrough": { "@from": "Author.name" } } ] } }, + { "field.long": { "name": "postCount", "children": [ + { "origin.aggregate": { "@agg": "count", "@of": "Post.id", "@via": "Author.posts" } } ] } } + ] +}}, +{ "template.prompt": { "name": "AuthorBio", "@payloadRef": "AuthorReport", "@textRef": "ai/bio" } } +``` + +**After:** + +```jsonc +{ "object.projection": { + "name": "AuthorReport", + "children": [ + { "field.string": { "name": "name", "extends": "Author.name", "children": [ + { "origin.passthrough": { "@from": "Author.name" } } ] } }, + { "field.long": { "name": "postCount", "children": [ + { "origin.aggregate": { "@agg": "count", "@of": "Post.id", "@via": "Author.posts" } } ] } } + ] +}}, +{ "template.prompt": { "name": "AuthorBio", "@payloadRef": "AuthorReport", "@textRef": "ai/bio" } } +``` + +(The `extends: "Author.name"` anchor is what lets the no-`@via` passthrough +derive `Author` as the base entity; every explicit-`@via` origin needs no +anchor.) + +A value that carries **only** `origin.passthrough` (an FR-015 parameter VO) +needs no change. The host subtype change **alone** does not affect payload +typing — every port's payload emitter is declared-type-authoritative (#270) — +but **adding an `extends` anchor is not typing-neutral: the field then inherits +the anchored field's properties, which can flip its optionality** (e.g. a +`title` anchored to a `@required` entity field becomes required in the +generated record). Regenerate and review the payload diff rather than assuming +byte-identity; this repo's own canonical example changed +`title?: string` → `title: string` under exactly this rewrite. + +### What does NOT change + +- `origin.passthrough` on `object.value` (parameter lineage). +- Assembly origins on `object.projection` and on `object.entity` read-views. +- Nested payload shapes already targeting an `object.value`. +- Physical schema: a sourceless projection has no DDL, so `meta migrate` emits + nothing for it. + +--- + +## 2. Nested payload targets are value-only (#210) + +### What changed + +Every `field.object @objectRef` reachable from a template-level payload target +(the `@payloadRef`/`@responseRef` closure) must resolve to an `object.value` — +now enforced by the loader in all four ports. Before this release the loader +did not constrain the target's subtype: Kotlin and Java codegen filtered +non-value targets out, but **TypeScript, C# and Python accepted the shape and +emitted a nested interface/record from the entity** — so a payload field +pointing at an `object.entity` (or a projection) previously loaded and +generated code, and now fails. + +### The error you'll see + +``` +ERR_SUBTYPE_RULE_VIOLATION: payload 'acme::ai::ReviewRequest' field 'author' +@objectRef 'acme::Author' resolves to object.entity — a nested payload target +must be an object.value (template-level refs may also target a sourceless +object.projection, nested refs may not) (#210, ADR-0028, ADR-0044) +``` + +### Rewrite rule + +Declare an `object.value` mirroring the subset of the entity the payload +actually needs — optionally `extends`-binding the entity's fields to reuse +their shape — and repoint the `@objectRef` at it. Embedding a full entity in a +payload was always payload bloat (every entity column shipped to the LLM); the +curated value makes the exposure an explicit, reviewable list. + +**Before:** + +```jsonc +{ "object.value": { + "name": "ReviewRequest", + "children": [ + { "field.string": { "name": "instructions" } }, + { "field.object": { "name": "author", "@objectRef": "Author" } } + ] +}} +``` + +**After:** + +```jsonc +{ "object.value": { + "name": "AuthorBrief", + "children": [ + { "field.string": { "name": "name", "extends": "Author.name" } } + ] +}}, +{ "object.value": { + "name": "ReviewRequest", + "children": [ + { "field.string": { "name": "instructions" } }, + { "field.object": { "name": "author", "@objectRef": "AuthorBrief" } } + ] +}} +``` + +(The widen does NOT extend here: a nested `@objectRef` at a sourceless +projection is also rejected — only the **template-level** +`@payloadRef`/`@responseRef` accept a projection.) + +--- + +## 3. `source.rdb @role` shrinks to `primary | replica` (#212) + +### What changed + +The `@role` enum on `source.rdb` is now exactly `primary | replica`. The four +retired members — `index`, `cache`, `publish`, `mirror` — are **reserved, not +registered** (the ADR-0040 treatment): documented for future re-entry, rejected +by every loader today. A role member re-enters the registry only when a +shipping consumer dispatches on it (ADR-0007 amendment). + +### The error you'll see + +``` +ERR_BAD_ATTR_VALUE: source.rdb attribute '@role' has value 'publish' which is +not one of the allowed values: primary, replica +``` + +(Wording varies by port — Java, for example, emits `… is not a valid value; +allowed: primary, replica` — but the code and the allowed set are identical in +all four loaders.) + +A single-source object with a non-`primary` role also reports +`ERR_SOURCE_NO_PRIMARY`. That error is not new — a single source declaring +`@role: index` failed the one-primary rule before the shrink too; what changes +is that you now see **both** errors, since the retired member additionally +fails the `allowedValues` check. + +### Rewrite rule + +Every read of `@role` in every port is an equality test against `primary`, so +the four retired members were always indistinguishable from `replica` to every +consumer. The rewrite is mechanical: replace the retired member with `replica`. + +**Before:** + +```jsonc +{ "source.rdb": { "@kind": "view", "@view": "v_orders_search", "@role": "index" } } +``` + +**After:** + +```jsonc +{ "source.rdb": { "@kind": "view", "@view": "v_orders_search", "@role": "replica" } } +``` + +`@role: primary` (or omitting `@role` — `primary` is the default) is unchanged. +No migration SQL is emitted for either change: `@role` never affected DDL. diff --git a/docs/features/source-kinds.md b/docs/features/source-kinds.md index 482ede5c1..a86c16102 100644 --- a/docs/features/source-kinds.md +++ b/docs/features/source-kinds.md @@ -225,10 +225,18 @@ time. ## Multi-source via `@role` -An entity may have multiple `source.rdb` children, one per role. Exactly one must -carry `@role: "primary"`; the others identify additional sources (typically -read-replicas or split-domain projections). The TS persistence layer and the Java -OMDB engine route writes to `primary` and reads to the role you select. +An entity may have multiple `source.rdb` children. `@role` is exactly +`primary | replica` (#212 — the former `index` / `cache` / `publish` / `mirror` +members are retired, reserved-not-registered; a role member re-enters the +registry only when a shipping consumer dispatches on it). Exactly one source +must carry `@role: "primary"` (`primary` is also the default when `@role` is +omitted); every additional source is `@role: "replica"` — typically a +read-replica or the read-only view of an entity read-view (#214). `@role` is a +**designation**, not a routing mechanism: consumers test "is this the primary?" +— writes go to the primary source and reads to a read-only-kind replica — and +nothing ever dispatched on the retired members (ADR-0007 Amendment 2). +Migrating a retired member: +[value-assembly-origins-and-source-role-shrink.md](migrations/value-assembly-origins-and-source-role-shrink.md). ```json { diff --git a/docs/features/templates-and-payloads.md b/docs/features/templates-and-payloads.md index f956da972..f6c64260d 100644 --- a/docs/features/templates-and-payloads.md +++ b/docs/features/templates-and-payloads.md @@ -19,9 +19,10 @@ This buys four guarantees: 4. **Cross-language conformance** — a Python eval renders exactly what the Java production server sends. -The vocabulary is `template.*` (the renderable unit) over a declared -`object.value` payload shape. Mustache is the chosen template engine — it has -the only published cross-language spec + conformance suite. +The vocabulary is `template.*` (the renderable unit) over a declared payload +shape — an `object.value`, or (since #210) a **sourceless** `object.projection`. +Mustache is the chosen template engine — it has the only published +cross-language spec + conformance suite. ## Two template subtypes @@ -34,7 +35,7 @@ Both carry the same generic attributes: | Attr | Required | Purpose | |---|---|---| -| `@payloadRef` | yes | The `object.value` view-object declaring the payload shape | +| `@payloadRef` | yes | The `object.value` — or sourceless `object.projection` (#210) — declaring the payload shape | | `@textRef` | yes | The 2-layer logical reference `group/source` resolved by a provider | | `@format` | no | `text` / `html` / `xml` / `csv` / `json` / `markdown` / `spreadsheet` — drives the escaper. Default: `text`. | | `@maxChars` | no | Build-time size budget | @@ -43,9 +44,10 @@ Both carry the same generic attributes: ## Payload fields are declared -A payload is an `object.value` view-object whose fields DECLARE the payload's -shape — a prompt's payload is a typed projection you author, so payload bloat -shows up as a diff. Every port's payload codegen is **declared-type-authoritative +A payload is an `object.value` — or a **sourceless** `object.projection` +(#210: no `source.*` child, own or inherited) — whose fields DECLARE the +payload's shape; a prompt's payload is a typed projection you author, so +payload bloat shows up as a diff. Every port's payload codegen is **declared-type-authoritative (#270)**: a field's generated type comes only from its declared `field.` + `isArray` + `@objectRef`, and a nested payload is a declared `field.object @objectRef` to another `object.value` (`isArray: true` for a list). An `origin.*` @@ -53,11 +55,17 @@ child on a payload field is **ignored for typing** — it never changes the generated type, nullability, or the nested-payload set. The caller supplies the field values at render time. -Derivation and assembly belong to **projection** read models (`object.projection` -over an entity), which carry the origin vocabulary — `origin.passthrough`, -`origin.aggregate` (incl. the `any` / `all` quantifiers and the `collect` array -rollup), `origin.computed` (a closed `@expr` grammar) and `origin.first` (#195) — -see [source-kinds.md](source-kinds.md). +Derivation and assembly belong to **projections** (`object.projection`), which +carry the origin vocabulary — `origin.passthrough`, `origin.aggregate` (incl. +the `any` / `all` quantifiers and the `collect` array rollup), `origin.computed` +(a closed `@expr` grammar) and `origin.first` (#195) — see +[source-kinds.md](source-kinds.md). #210 draws the host line hard: a +value-hosted field may carry **only `origin.passthrough`** (FR-015 parameter +lineage); the assembly origins (`aggregate` / `computed` / `collection` / +`first`) on an `object.value` fail load with `ERR_SUBTYPE_RULE_VIOLATION`. An +origin-declared payload lives on a **sourceless projection** instead, and +`@payloadRef` / `@responseRef` accept it at the template level. Nested payload +targets (a payload field's `field.object @objectRef`) stay value-only. ## Authoring @@ -184,7 +192,8 @@ For the `lobby/welcome` template: ### TypeScript `@metaobjectsdev/render` ships the render engine + verify. Payload-VO codegen is -shared with the projection codegen path (the payload IS an `object.value`). +shared with the projection codegen path (the payload is an `object.value` or a +sourceless `object.projection`, #210). ```ts import { render } from "@metaobjectsdev/render"; @@ -531,8 +540,9 @@ for the per-port pass/skip ledger. ## See also -- [entities.md](entities.md) — `object.value` is the payload's host type +- [entities.md](entities.md) — `object.value` / sourceless `object.projection` are the payload host types (#210) - [field-types.md](field-types.md) — fields in payload VOs - [source-kinds.md](source-kinds.md) — `source.rdb` `@kind: "view"` for materialized payloads (FR-003) - [migrations-and-drift.md](migrations-and-drift.md) — the verify pillar +- [migrations/value-assembly-origins-and-source-role-shrink.md](migrations/value-assembly-origins-and-source-role-shrink.md) — migrating a pre-#210 payload (assembly origins on a value; nested non-value targets) - FR-004 spec: [2026-05-22-fr-004-cross-language-prompt-construction-design.md](../superpowers/specs/2026-05-22-fr-004-cross-language-prompt-construction-design.md) diff --git a/docs/superpowers/specs/2026-08-02-multi-persistence-architecture-research.md b/docs/superpowers/specs/2026-08-02-multi-persistence-architecture-research.md index 3f35bce67..d8ced0a8b 100644 --- a/docs/superpowers/specs/2026-08-02-multi-persistence-architecture-research.md +++ b/docs/superpowers/specs/2026-08-02-multi-persistence-architecture-research.md @@ -2,6 +2,7 @@ **Status:** Design research (no code changed) **Date:** 2026-08-02 +**Amended 2026-08-06** ([#212](https://github.com/metaobjectsdev/metaobjects/issues/212), ADR-0007 Amendment 2): `@role`'s registered vocabulary has since shrunk to `primary | replica` — `index`/`cache`/`publish`/`mirror` are now **reserved-not-registered** (`SOURCE_ROLES` in every port carries only the two survivors), so §1's "already ships the full role vocabulary" claim and the worked `@role: index` / `@role: cache` examples describe the pre-shrink state and are not currently loadable. **Question:** #248 made persistability derive from "declares a writable `source.*` child" — but that check, the single `dialect`, and the migrate/codegen rails all assume ONE relational store. How should MetaObjects model an object persisted to multiple, heterogeneous backends at once (two RDBs, RDB + search index, document store, KV cache), and how do rails scope to a specific store? --- diff --git a/examples/advanced-modeling/README.md b/examples/advanced-modeling/README.md index bcfd8865a..94fb94561 100644 --- a/examples/advanced-modeling/README.md +++ b/examples/advanced-modeling/README.md @@ -34,7 +34,7 @@ through the real CLI path. It does **not** re-prove the patterns' *behavior* | 1 | **Projections** (`object.projection`, `origin.*`) | `metaobjects/meta.catalog.yaml:127-165` — `ProgramSummary`: `origin.passthrough` (a join to `Author`, `:138`), `origin.aggregate @agg:count` (`:143-146`), `origin.aggregate @agg:sum` + `filter` (`:153-157`), `origin.computed @expr` (`:163-164`) | `src/generated/ProgramSummary.ts` (the `pgView` declaration + read schema), `src/generated/ProgramSummary.routes.ts` (read-only routes — no POST/PATCH/DELETE) | | 2 | **Entity views** (`view.*` control family) | `metaobjects/meta.catalog.yaml:25-93` — `Program`: `field.enum` (`:31-33`, → `