Skip to content

Commit dfdb60f

Browse files
authored
Merge pull request #296 from metaobjectsdev/chore/release-0.23.1
2 parents ec6804e + 8c3a8ee commit dfdb60f

36 files changed

Lines changed: 1556 additions & 48 deletions

File tree

CHANGELOG.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,99 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
## [Unreleased]
99

10+
### Fixed — `{{#hasField}}` rendered as absent on a populated payload, in every port (npm/PyPI/NuGet/Maven)
11+
12+
A prompt's conditional section — *"include the abilities block only when there ARE
13+
abilities"* — is expressed as `{{#hasAbilities}}`, a **derived** boolean accessor over the
14+
declared field `abilities`. The JVM has emitted `has<Field>()` onto every generated payload
15+
record since 7.7.7 and accepts the section in its static drift check, sharing one naming
16+
rule so the two "can never drift apart".
17+
18+
**No render engine implemented the other half.** Given the same payload *data* — a map, which
19+
is what the runtime and the conformance corpus actually pass — all five ports rendered the
20+
section as absent:
21+
22+
```
23+
payload {"abilities":[{"name":"Fireball"}]}
24+
template "Abilities:{{#hasAbilities}} {{#abilities}}[{{name}}]{{/abilities}}{{/hasAbilities}}"
25+
before "Abilities:" ← content silently dropped, no error
26+
after "Abilities: [Fireball]"
27+
```
28+
29+
Silent wrong output, not a failure: the prompt shipped without its block. The JVM looked
30+
correct only because a *generated record* answers `hasFoo()` by its own method — so the same
31+
payload rendered differently depending on whether it arrived as a record or as a map.
32+
33+
`PayloadAccessors` now exists in all five ports carrying one shared rule (`"has" +
34+
capitalize`, and presence semantics mirroring the JVM emitter exactly: string → non-blank,
35+
collection → non-empty, reference → non-null, **number/boolean → no accessor at all**, since
36+
`{{#hasCount}}` over an int is drift rather than a conditional). Render derives them
37+
non-mutatingly, recursing into nested objects and collection elements so a section sees the
38+
element it is iterating; an **authored** `hasFoo` always wins. `verify` accepts exactly what
39+
render resolves, mirroring the JVM's deliberate permissiveness (acceptance keys off the
40+
field existing, not its type), and still reports drift inside a has-section body.
41+
42+
Found by an adopter with a JVM-authored prompt estate whose Node gate reported **157**
43+
`ERR_VAR_NOT_ON_PAYLOAD`, all `has`-prefixed, while its JVM gate reported none. Now 0 on
44+
both. Gated by the shared `render-derived-has-accessor` conformance case — **the corpus had
45+
no fixture using a derived accessor at all**, which is precisely why a divergence in the
46+
pillar that promises byte-identical rendering survived this long.
47+
48+
### Fixed — a requirement could not claim a prompt template (npm)
49+
50+
`@implementedBy` is documented as naming "the model nodes realising this requirement", and
51+
it resolved through the OBJECT resolver only. So a requirement could claim an entity, a
52+
value or a projection — and naming a `template.prompt` produced
53+
`ERR_REQUIREMENT_DANGLING_REF` ("the model moved and the requirement is stale") for a
54+
template sitting in the loaded tree.
55+
56+
That excluded the estate with the **most** to gain from a status. A retired entity leaves a
57+
table behind; a retired prompt leaves nothing, which is exactly the invisibility
58+
`@status: abandoned` exists to fix. A project whose prompts are a first-class pillar could
59+
describe every table it owns and not one of its prompts.
60+
61+
**L4 now means "a declared top-level model node"** — an `object.*` or a `template.*` — and
62+
L5 a member of one. Bare references bind package-locally and ambiguous ones bind nothing,
63+
the same fail-closed rule objects use. Requirements themselves are excluded: hierarchy is
64+
nesting, and a requirement claiming a requirement would be a second, contradictory parent
65+
mechanism. Object coverage is deliberately untouched and stays entity-grain — claiming a
66+
template must not silence the unclaimed-entity warning.
67+
68+
Also verified rather than assumed, since the same report asked about them: **fields, views,
69+
validators and identities were already claimable at L5** and needed no change. They are now
70+
pinned by tests so that stays true. Gated by `cli/test/requirement-template-refs.test.ts`.
71+
72+
### Fixed — `@verifiedBy` decided what a test file is, and was wrong about a mainstream convention (npm)
73+
74+
`@verifiedBy`'s scan carried one closed list of test-file patterns for the five ported
75+
ecosystems, with no way to extend it. **That list is a guess about someone else's repository,
76+
and it was wrong on a mainstream case from the day it shipped:** Maven Failsafe names
77+
integration tests `FooIT.java` / `FooIT.kt`, which matched nothing. Because the scan only fails
78+
OPEN at *zero* test files, a JVM project with unit tests (matched) and integration tests
79+
(unmatched) got a confident `ERR_REQUIREMENT_TEST_MISSING`*"the claim was never true"* — for
80+
a test sitting in the repo. An adopter hit exactly this: every repository test in the project is
81+
an `*IT`, so `@verifiedBy` was unusable there and the honest workaround was to stop using the
82+
attribute.
83+
84+
Three changes, of which only the first is a patch to the guess:
85+
86+
- **Failsafe's own defaults are now built in** (`*IT`, `*ITCase`, `IT*` for `.java`; `*IT` /
87+
`*ITCase` for `.kt`).
88+
- **`verify.testFiles` in `metaobjects.config.ts`** lets a project declare its own conventions
89+
as globs, added to the built-ins. What counts as a test file is project-specific; a list
90+
shipped by this repo cannot be authoritative about a convention it has never seen.
91+
- **An unrecognised convention is no longer reported as a broken claim.** When a name is absent
92+
from the corpus, `verify` now searches the unclassified source files before deciding. If the
93+
name is there, it emits `WARN_REQUIREMENT_TEST_UNCLASSIFIED` naming the file and pointing at
94+
`verify.testFiles`; `ERR_REQUIREMENT_TEST_MISSING` is reserved for a name that appears
95+
**nowhere**. The second pass runs only on the miss path, so the cost is per broken claim
96+
rather than per run.
97+
98+
The reusable lesson is the failure mode, not the regex: a gate that hardcodes another
99+
ecosystem's conventions will eventually tell a correct project that it is broken, and the
100+
default posture when the tool cannot classify something must be to say so rather than to
101+
convict. Gated by `cli/test/verified-by-corpus.test.ts`.
102+
10103
### Fixed — `verify` gates the committed schema snapshot, which nothing checked (npm) — [#292](https://github.com/metaobjectsdev/metaobjects/issues/292)
11104

12105
`meta migrate` diffs metadata against `.metaobjects/migrations/.schema.<dialect>.json` by default

agent-context/skills/metaobjects-authoring/references/requirements.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,15 @@ line: *would this sentence have to change if the code changed but the model did
4747
is `notes`.
4848

4949
**Hierarchy is nesting, and links live at the bottom.** L1 solution, L2 segment, L3
50-
service — these never reference the model. **L4** binds an object, **L5** binds a field,
51-
view or identity. `implementedBy` above L4 is an error. Regrouping *moves* a node; it does
52-
not edit a parent string.
50+
service — these never reference the model. **L4** binds a declared top-level node — an
51+
`object.*` **or a `template.*`** — and **L5** binds a member of one: a field, view,
52+
validator, identity, or a template's child. `implementedBy` above L4 is an error.
53+
Regrouping *moves* a node; it does not edit a parent string.
54+
55+
Claim your prompts. A `template.prompt` is a model node realising a capability exactly as
56+
an entity is, and it is the node whose retirement is hardest to see later — a removed
57+
prompt leaves no table behind. A prompt estate with no requirement entries is the same
58+
blind spot this whole mechanism exists to close.
5359

5460
**L1–L3 are levels of abstraction and ownership in the problem domain** — whose need is
5561
this, and at what altitude — and are NEVER a directory, package, deployable or module.

agent-context/skills/metaobjects-verify/references/requirements.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,26 @@ mechanism exists to preserve.
3535
| `@implementedBy` above the L4 link floor | 1 |
3636
| live `requirement.architectural` claimed by nothing | 1 |
3737
| `@verifiedBy` naming a test that exists nowhere | 1 |
38+
| `@verifiedBy` naming a name found only in an **unrecognised** test file | 0 (warning) |
3839
| `@verifiedBy` naming a test that is **skipped** | 0 (warning) |
3940
| an entity no requirement claims | 0 (warning) |
4041

42+
## What counts as a test file is YOUR project's call
43+
44+
The scan ships patterns for jest/vitest/bun, JUnit, Maven Failsafe (`*IT`), xUnit/NUnit,
45+
pytest and Kotlin. Those are a convenience, **not an authority** — a built-in list is a guess
46+
about your repository, and a wrong guess reports a real test as a broken claim. Declare your
47+
conventions and they are added to the built-ins:
48+
49+
```ts
50+
// metaobjects.config.ts
51+
export default defineConfig({ verify: { testFiles: ["**/*IT.kt", "**/*.feature"] } });
52+
```
53+
54+
If a named test is missing from the corpus but present in some other source file, `verify`
55+
warns and names that file rather than failing — an unrecognised convention is the tool's
56+
ignorance, not your mistake.
57+
4158
## What a green run does NOT prove
4259

4360
It proves **referential integrity**: statuses parse, levels are in range, links sit at or

docs/features/requirements.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,21 @@ no `id` and no `parent`: regrouping moves a subtree.
5757
L4 object, L5 member. `@implementedBy` is legal at **L4 and L5 only** — L1–L3 are
5858
organisational and never reference the model.
5959

60+
**What L4 and L5 may name.** L4 names a declared top-level node: an `object.*` **or a
61+
`template.*`**. A declared prompt is a model node realising a capability in the same sense
62+
an entity is — and it is the one most in need of a status, because a retired prompt leaves
63+
no table behind to notice. L5 names a member of one: a field, a view, a validator, an
64+
identity, or a template's child.
65+
66+
```jsonc
67+
{ "requirement.functional": {
68+
"name": "sceneBrief", "@level": 4, "@status": "live",
69+
"@statement": "The game master is told what the party can currently see.",
70+
"@violation": "A scene narrated from world state the party has no way to know.",
71+
"@implementedBy": ["acme::play::sceneBrief"] // a template.prompt
72+
}}
73+
```
74+
6075
**L1–L3 are levels of abstraction and ownership in the problem domain** — whose need is this,
6176
and at what altitude — and are **never** a directory, package, deployable or module. Binding
6277
to technical constructs happens only at L4 and L5, which is the allocation step. The test to
@@ -149,6 +164,24 @@ entry.
149164
`@verifiedBy` names tests: `verify` checks each exists and is not skipped. It never runs
150165
them. `@trackedBy` names issues or tickets and is **not** resolved — `verify` has no network.
151166

167+
**What counts as a test file is your project's call.** The scan ships patterns for the
168+
conventions this repo ports to — jest/vitest/bun, JUnit, Maven Failsafe (`*IT`), xUnit/NUnit,
169+
pytest, Kotlin — and they are a *convenience, not an authority*: a built-in list is a guess
170+
about someone else's repository, and a wrong guess turns a real test into a "broken claim".
171+
Declare yours and they are added to the built-ins:
172+
173+
```ts
174+
// metaobjects.config.ts
175+
export default defineConfig({
176+
verify: { testFiles: ["**/*IT.kt", "**/*.feature"] },
177+
});
178+
```
179+
180+
If a named test cannot be found in the corpus but *does* appear in some other source file,
181+
`verify` says so (`WARN_REQUIREMENT_TEST_UNCLASSIFIED`, naming the file) instead of claiming
182+
the requirement is broken — an unrecognised convention is the tool's ignorance, not your
183+
mistake. `ERR_REQUIREMENT_TEST_MISSING` is reserved for a name that appears **nowhere**.
184+
152185
> **`@verifiedBy` is existence evidence, not proof — and the difference matters most to whoever
153186
> authored it.** The scan matches a name anywhere in the test corpus, as a whole word, in any
154187
> language; that generosity is deliberate (a "missing" verdict then means the name appears in no

fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/requirements.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,15 @@ line: *would this sentence have to change if the code changed but the model did
4747
is `notes`.
4848

4949
**Hierarchy is nesting, and links live at the bottom.** L1 solution, L2 segment, L3
50-
service — these never reference the model. **L4** binds an object, **L5** binds a field,
51-
view or identity. `implementedBy` above L4 is an error. Regrouping *moves* a node; it does
52-
not edit a parent string.
50+
service — these never reference the model. **L4** binds a declared top-level node — an
51+
`object.*` **or a `template.*`** — and **L5** binds a member of one: a field, view,
52+
validator, identity, or a template's child. `implementedBy` above L4 is an error.
53+
Regrouping *moves* a node; it does not edit a parent string.
54+
55+
Claim your prompts. A `template.prompt` is a model node realising a capability exactly as
56+
an entity is, and it is the node whose retirement is hardest to see later — a removed
57+
prompt leaves no table behind. A prompt estate with no requirement entries is the same
58+
blind spot this whole mechanism exists to close.
5359

5460
**L1–L3 are levels of abstraction and ownership in the problem domain** — whose need is
5561
this, and at what altitude — and are NEVER a directory, package, deployable or module.

fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-verify/references/requirements.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,26 @@ mechanism exists to preserve.
3535
| `@implementedBy` above the L4 link floor | 1 |
3636
| live `requirement.architectural` claimed by nothing | 1 |
3737
| `@verifiedBy` naming a test that exists nowhere | 1 |
38+
| `@verifiedBy` naming a name found only in an **unrecognised** test file | 0 (warning) |
3839
| `@verifiedBy` naming a test that is **skipped** | 0 (warning) |
3940
| an entity no requirement claims | 0 (warning) |
4041

42+
## What counts as a test file is YOUR project's call
43+
44+
The scan ships patterns for jest/vitest/bun, JUnit, Maven Failsafe (`*IT`), xUnit/NUnit,
45+
pytest and Kotlin. Those are a convenience, **not an authority** — a built-in list is a guess
46+
about your repository, and a wrong guess reports a real test as a broken claim. Declare your
47+
conventions and they are added to the built-ins:
48+
49+
```ts
50+
// metaobjects.config.ts
51+
export default defineConfig({ verify: { testFiles: ["**/*IT.kt", "**/*.feature"] } });
52+
```
53+
54+
If a named test is missing from the corpus but present in some other source file, `verify`
55+
warns and names that file rather than failing — an unrecognised convention is the tool's
56+
ignorance, not your mistake.
57+
4158
## What a green run does NOT prove
4259

4360
It proves **referential integrity**: statuses parse, levels are in range, links sit at or

fixtures/metamodel-docs/expected/types/requirement.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ How the system is built, applied uniformly across the model. Its check is UNIVER
2727
| `@status` | string | yes | | `planned`, `live`, `partial`, `abandoned`, `superseded` || As on requirement.functional. A live or partial architectural requirement claimed by NOTHING is an error: a policy declared and applied to nothing. A planned one is exempt from that check — it is not applied yet by definition. |
2828
| `@supersededBy` | string | no | | || The requirement that replaced this one. Expected on status=superseded. |
2929
| `@trackedBy` | string[] | no | | || As on requirement.functional. Issue or ticket references for outstanding work; free-form, not resolved. |
30-
| `@verifiedBy` | string[] | no | | || Names of the tests proving the policy holds. verify checks each exists and is not skipped; it never runs them. |
30+
| `@verifiedBy` | string[] | no | | || OPTIONAL — omit unless you have opened the test and read what it asserts. Names of tests that assert the policy holds. verify checks each name EXISTS and is not skipped; it never runs them, and it cannot tell whether the named test verifies this requirement — any occurrence in the test corpus satisfies it. |
3131
| `@violation` | string | yes | | || What breaking it looks like — the node that would contradict it. This is what makes universality checkable. |
3232

3333
**Allowed children**
@@ -51,7 +51,7 @@ What the product does for a user, stated as one violable claim. Its check is EXI
5151
| `@status` | string | yes | | `planned`, `live`, `partial`, `abandoned`, `superseded` || planned intended but not built yet; live implemented and in use; partial implemented with known gaps; abandoned built then deliberately retired; superseded replaced by a different mechanism. A dangling @implementedBy is an ERROR on live/partial (the model moved, the requirement is stale) and ALLOWED on planned/abandoned/superseded — on planned the nodes do not exist YET, on the other two they are meant to be gone, and that is the entry doing its job. A planned requirement also never contributes to object coverage: planning a capability must not silence the warning that nothing implements it. |
5252
| `@supersededBy` | string | no | | || The requirement that replaced this one. Expected on status=superseded. |
5353
| `@trackedBy` | string[] | no | | || Issue or ticket references for outstanding work — a URL, an owner/repo#123 shorthand, or a tracker key. Free-form and NOT resolved by verify, which does not reach the network; unlike @verifiedBy, nothing here is checked to exist. Its job is to stop a deferred gap becoming invisible, so verify warns when a deferred requirement names no ticket. Also the right place to link the ticket that a planned requirement will be built under. |
54-
| `@verifiedBy` | string[] | no | | || Names of the tests proving the behaviour. verify checks each exists and is not skipped; it never runs them. |
54+
| `@verifiedBy` | string[] | no | | || OPTIONAL — omit unless you have opened the test and read what it asserts. Names of tests that assert the behaviour. verify checks each name EXISTS and is not skipped; it never runs them, and it cannot tell whether the named test verifies this requirement — any occurrence in the test corpus satisfies it. |
5555
| `@violation` | string | yes | | || What breaking it looks like, in one sentence. A requirement MUST be violable: 'every entity has a uuid primary key' is (point at one with a composite string key); 'things are persisted' is not, and is a description rather than a requirement. |
5656

5757
**Allowed children**
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
title:Party
2+
bio: (none)
3+
sponsor: Guild
4+
companions: (none)
5+
abilities: Fireball[fire aoe ] Mend[untagged]
6+
details: present
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "format": "text", "note": "Derived has<Field> boolean accessors: present/absent/blank across scalar, collection and nested scope" }
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"title": "Party",
3+
"bio": " ",
4+
"abilities": [
5+
{
6+
"name": "Fireball",
7+
"tags": [
8+
"fire",
9+
"aoe"
10+
]
11+
},
12+
{
13+
"name": "Mend",
14+
"tags": []
15+
}
16+
],
17+
"companions": [],
18+
"sponsor": {
19+
"name": "Guild"
20+
},
21+
"emptyDetails": {}
22+
}

0 commit comments

Comments
 (0)