Skip to content

Commit b07178c

Browse files
dmealingclaude
andcommitted
Merge #234: @lenient opt-out on field.uri/field.inet + strict cross-port pin
Coordinated PATCH (npm 0.20.6 / PyPI 0.19.8 / NuGet 0.19.6 / Maven 7.11.6). - Strict field.uri/field.inet accept/reject pinned identical across all 5 ports (fixes C#/Java/Kotlin relative-URI acceptance + the JVM inet-DNS lookup bug). - @lenient:true degrades to a plain string (lenient field.inet → text column). - Fixes a latent Zod-4 field.inet codegen bug (z.string().ip() was removed). - Adversarial review caught + fixed 2 cross-port parser divergences (leading-zero IPv4 silent-rewrite; TS-rejected IPv4-mapped IPv6) the probe set had missed. validation-conformance 40 cases green on all 5 ports; full suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014xy8powhHYJ6gfFt9Ut8dL
2 parents eafc58a + fda728b commit b07178c

50 files changed

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

88
## [Unreleased]
99

10+
### Added — `@lenient` opt-out on `field.uri` / `field.inet`; strict well-formedness pinned cross-port (#234)
11+
12+
**Coordinated PATCH** (npm / PyPI / NuGet / Maven Central). Two halves, one release:
13+
14+
- **Strict accept/reject is now pinned and identical across all five ports** (on the shared
15+
`validation-conformance` probe set, which spans the URI/IP-literal edge cases — scheme-less,
16+
relative, garbage, empty-authority, leading-zero octet, IPv4-mapped IPv6, CIDR, padding).
17+
A `field.uri` must be an
18+
**absolute, scheme-bearing URI** (`https://a.com`, `mailto:a@b`, `urn:…`; a scheme-less
19+
`example.com` or a bare `/path` is rejected) and a `field.inet` must be an **IPv4/IPv6 literal**
20+
(no hostnames, no CIDR, no padding). Previously the ports diverged: C#/Java/Kotlin accepted
21+
relative URIs, and the JVM ports resolved **hostnames into `field.inet` via a live DNS lookup on
22+
the request path** (blocking I/O, a network-dependent verdict, and a silent value rewrite) — now
23+
fixed. Enforcement lives at each port's wire/bind layer via small generated, dependency-free
24+
support files (`MetaNetBindings.java` / `MetaNetJson.kt` / `MetaNetValidation.cs`; TS keeps its
25+
Zod check, Python its Pydantic type). **This is a wire-acceptance change for existing C#/Java/Kotlin
26+
deployments** that were accepting values now rejected — the escape hatch ships in the same release:
27+
- **`@lenient: true`** (a new optional boolean attribute on `field.uri` / `field.inet`, registered
28+
cross-port + gated by `registry-conformance`) opts a field **out** of strict enforcement: codegen
29+
binds a plain string (no URL/IP validator, no native `URI`/`InetAddress` type) so a
30+
not-necessarily-well-formed value — an LLM-emitted citation URL, a user-supplied host — round-trips
31+
unchanged. A `field.inet @lenient` additionally uses a plain `text` column instead of the native
32+
`inet` type, so **toggling `@lenient` on an existing `field.inet` is schema-affecting** (an `ALTER`
33+
on the next `meta migrate`). Strict remains the default (attr absent/false → byte-identical output).
34+
35+
Gated by the shared `validation-conformance` corpus (strict accept/reject + `@lenient` accept-garbage,
36+
identical boolean verdicts on all five ports) and a real-Postgres idempotence + value-semantics
37+
round-trip for the lenient-inet `text` column. Also fixes a **latent Zod-4 bug**: `field.inet` codegen
38+
emitted `z.string().ip()`, which was removed in Zod 4 (the workspace + generated consumers run Zod 4);
39+
it now emits a version-agnostic IPv4/IPv6 regex union valid on both Zod 3 and Zod 4.
40+
1041
## [0.20.5] — 2026-07-26
1142

1243
**Coordinated release** — npm `0.20.5` · PyPI `0.19.7` · Maven Central `7.11.5` · NuGet

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,8 @@ Canonical form for common field needs — reach for these before inventing anyth
313313
| Long / unbounded text | bare `field.string` | add `@maxLength` only when you want `varchar(N)` |
314314
| Nested structured value | `field.object` | `@objectRef` + `@storage` |
315315
| Open JSON bag (no fixed shape) | `field.string` + `@dbColumnType: jsonb` | logical type stays string; column is jsonb |
316-
| URL / URI | `field.uri` | native `URI`/`Uri`; `text` column; URL validation — a real native type + behavior, so a subtype (not a validated string) |
317-
| IP address | `field.inet` | native IP type; Postgres `inet` column |
316+
| URL / URI | `field.uri` | native `URI`/`Uri`; `text` column; strict absolute-URI validation (add `@lenient: true` to store any string) — a real native type + behavior, so a subtype (not a validated string) |
317+
| IP address | `field.inet` | native IP type; Postgres `inet` column; strict IPv4/IPv6-literal validation (add `@lenient: true` for a plain-string `text` column) |
318318
| Validated plain string (email / hostname) | `field.string` + `@stringFormat` | `@stringFormat: email` or `@stringFormat: hostname` — idiomatic per-port validation; don't hand-write the `validator.regex` |
319319

320320
**UUID columns are `field.uuid``field.string` + `@dbColumnType: uuid` is a forbidden smell.**
@@ -352,6 +352,24 @@ lives in `field.timestamp` (instant by default) + the `@localTime` naive opt-out
352352
{ "field.timestamp": { "name": "opensAt", "@localTime": true } }
353353
```
354354

355+
**URIs / IPs — strict by default, `@lenient` to store any string (#234).** `field.uri` is a
356+
strict **absolute, scheme-bearing URI** (`https://a.com`, `mailto:a@b`; a scheme-less
357+
`example.com` or a bare `/path` is rejected) and `field.inet` is a strict **IPv4/IPv6 literal**
358+
(no hostnames, no CIDR). When a field must hold a *not-necessarily-well-formed* value — an
359+
LLM-emitted citation URL, a user-supplied host that might be a name — add **`@lenient: true`**:
360+
codegen then binds a plain string (no URL/IP validator) and a `field.inet @lenient` uses a plain
361+
`text` column instead of the native `inet` type (so a value the native column would reject at
362+
INSERT round-trips unchanged). Strict is the default; `@lenient` is the deliberate opt-out (never
363+
default-true — the same shape as `@localTime`). **Toggling `@lenient` on an existing `field.inet`
364+
is schema-affecting** (`inet``text`), so it shows up as an `ALTER` on the next `meta migrate`.
365+
366+
```json
367+
{ "field.uri": { "name": "homepage" } } // strict — must be an absolute URL
368+
{ "field.uri": { "name": "citationUrl", "@lenient": true } } // any string (LLM-emitted, may be malformed)
369+
{ "field.inet": { "name": "sourceIp" } } // strict — IPv4/IPv6 literal only
370+
{ "field.inet": { "name": "reportedHost", "@lenient": true } } // any string; text column
371+
```
372+
355373
**`@autoSet` — let the runtime stamp created/updated times; never hand-set them.**
356374
`@autoSet` is registered on the temporal subtypes (`field.date` / `field.time` /
357375
`field.timestamp`) and takes **`onCreate`** (stamp on insert) or **`onUpdate`**

fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,8 @@ Canonical form for common field needs — reach for these before inventing anyth
313313
| Long / unbounded text | bare `field.string` | add `@maxLength` only when you want `varchar(N)` |
314314
| Nested structured value | `field.object` | `@objectRef` + `@storage` |
315315
| Open JSON bag (no fixed shape) | `field.string` + `@dbColumnType: jsonb` | logical type stays string; column is jsonb |
316-
| URL / URI | `field.uri` | native `URI`/`Uri`; `text` column; URL validation — a real native type + behavior, so a subtype (not a validated string) |
317-
| IP address | `field.inet` | native IP type; Postgres `inet` column |
316+
| URL / URI | `field.uri` | native `URI`/`Uri`; `text` column; strict absolute-URI validation (add `@lenient: true` to store any string) — a real native type + behavior, so a subtype (not a validated string) |
317+
| IP address | `field.inet` | native IP type; Postgres `inet` column; strict IPv4/IPv6-literal validation (add `@lenient: true` for a plain-string `text` column) |
318318
| Validated plain string (email / hostname) | `field.string` + `@stringFormat` | `@stringFormat: email` or `@stringFormat: hostname` — idiomatic per-port validation; don't hand-write the `validator.regex` |
319319

320320
**UUID columns are `field.uuid``field.string` + `@dbColumnType: uuid` is a forbidden smell.**
@@ -352,6 +352,24 @@ lives in `field.timestamp` (instant by default) + the `@localTime` naive opt-out
352352
{ "field.timestamp": { "name": "opensAt", "@localTime": true } }
353353
```
354354

355+
**URIs / IPs — strict by default, `@lenient` to store any string (#234).** `field.uri` is a
356+
strict **absolute, scheme-bearing URI** (`https://a.com`, `mailto:a@b`; a scheme-less
357+
`example.com` or a bare `/path` is rejected) and `field.inet` is a strict **IPv4/IPv6 literal**
358+
(no hostnames, no CIDR). When a field must hold a *not-necessarily-well-formed* value — an
359+
LLM-emitted citation URL, a user-supplied host that might be a name — add **`@lenient: true`**:
360+
codegen then binds a plain string (no URL/IP validator) and a `field.inet @lenient` uses a plain
361+
`text` column instead of the native `inet` type (so a value the native column would reject at
362+
INSERT round-trips unchanged). Strict is the default; `@lenient` is the deliberate opt-out (never
363+
default-true — the same shape as `@localTime`). **Toggling `@lenient` on an existing `field.inet`
364+
is schema-affecting** (`inet``text`), so it shows up as an `ALTER` on the next `meta migrate`.
365+
366+
```json
367+
{ "field.uri": { "name": "homepage" } } // strict — must be an absolute URL
368+
{ "field.uri": { "name": "citationUrl", "@lenient": true } } // any string (LLM-emitted, may be malformed)
369+
{ "field.inet": { "name": "sourceIp" } } // strict — IPv4/IPv6 literal only
370+
{ "field.inet": { "name": "reportedHost", "@lenient": true } } // any string; text column
371+
```
372+
355373
**`@autoSet` — let the runtime stamp created/updated times; never hand-set them.**
356374
`@autoSet` is registered on the temporal subtypes (`field.date` / `field.time` /
357375
`field.timestamp`) and takes **`onCreate`** (stamp on insert) or **`onUpdate`**

fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,8 @@ Canonical form for common field needs — reach for these before inventing anyth
313313
| Long / unbounded text | bare `field.string` | add `@maxLength` only when you want `varchar(N)` |
314314
| Nested structured value | `field.object` | `@objectRef` + `@storage` |
315315
| Open JSON bag (no fixed shape) | `field.string` + `@dbColumnType: jsonb` | logical type stays string; column is jsonb |
316-
| URL / URI | `field.uri` | native `URI`/`Uri`; `text` column; URL validation — a real native type + behavior, so a subtype (not a validated string) |
317-
| IP address | `field.inet` | native IP type; Postgres `inet` column |
316+
| URL / URI | `field.uri` | native `URI`/`Uri`; `text` column; strict absolute-URI validation (add `@lenient: true` to store any string) — a real native type + behavior, so a subtype (not a validated string) |
317+
| IP address | `field.inet` | native IP type; Postgres `inet` column; strict IPv4/IPv6-literal validation (add `@lenient: true` for a plain-string `text` column) |
318318
| Validated plain string (email / hostname) | `field.string` + `@stringFormat` | `@stringFormat: email` or `@stringFormat: hostname` — idiomatic per-port validation; don't hand-write the `validator.regex` |
319319

320320
**UUID columns are `field.uuid``field.string` + `@dbColumnType: uuid` is a forbidden smell.**
@@ -352,6 +352,24 @@ lives in `field.timestamp` (instant by default) + the `@localTime` naive opt-out
352352
{ "field.timestamp": { "name": "opensAt", "@localTime": true } }
353353
```
354354

355+
**URIs / IPs — strict by default, `@lenient` to store any string (#234).** `field.uri` is a
356+
strict **absolute, scheme-bearing URI** (`https://a.com`, `mailto:a@b`; a scheme-less
357+
`example.com` or a bare `/path` is rejected) and `field.inet` is a strict **IPv4/IPv6 literal**
358+
(no hostnames, no CIDR). When a field must hold a *not-necessarily-well-formed* value — an
359+
LLM-emitted citation URL, a user-supplied host that might be a name — add **`@lenient: true`**:
360+
codegen then binds a plain string (no URL/IP validator) and a `field.inet @lenient` uses a plain
361+
`text` column instead of the native `inet` type (so a value the native column would reject at
362+
INSERT round-trips unchanged). Strict is the default; `@lenient` is the deliberate opt-out (never
363+
default-true — the same shape as `@localTime`). **Toggling `@lenient` on an existing `field.inet`
364+
is schema-affecting** (`inet``text`), so it shows up as an `ALTER` on the next `meta migrate`.
365+
366+
```json
367+
{ "field.uri": { "name": "homepage" } } // strict — must be an absolute URL
368+
{ "field.uri": { "name": "citationUrl", "@lenient": true } } // any string (LLM-emitted, may be malformed)
369+
{ "field.inet": { "name": "sourceIp" } } // strict — IPv4/IPv6 literal only
370+
{ "field.inet": { "name": "reportedHost", "@lenient": true } } // any string; text column
371+
```
372+
355373
**`@autoSet` — let the runtime stamp created/updated times; never hand-set them.**
356374
`@autoSet` is registered on the temporal subtypes (`field.date` / `field.time` /
357375
`field.timestamp`) and takes **`onCreate`** (stamp on insert) or **`onUpdate`**

fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,8 @@ Canonical form for common field needs — reach for these before inventing anyth
313313
| Long / unbounded text | bare `field.string` | add `@maxLength` only when you want `varchar(N)` |
314314
| Nested structured value | `field.object` | `@objectRef` + `@storage` |
315315
| Open JSON bag (no fixed shape) | `field.string` + `@dbColumnType: jsonb` | logical type stays string; column is jsonb |
316-
| URL / URI | `field.uri` | native `URI`/`Uri`; `text` column; URL validation — a real native type + behavior, so a subtype (not a validated string) |
317-
| IP address | `field.inet` | native IP type; Postgres `inet` column |
316+
| URL / URI | `field.uri` | native `URI`/`Uri`; `text` column; strict absolute-URI validation (add `@lenient: true` to store any string) — a real native type + behavior, so a subtype (not a validated string) |
317+
| IP address | `field.inet` | native IP type; Postgres `inet` column; strict IPv4/IPv6-literal validation (add `@lenient: true` for a plain-string `text` column) |
318318
| Validated plain string (email / hostname) | `field.string` + `@stringFormat` | `@stringFormat: email` or `@stringFormat: hostname` — idiomatic per-port validation; don't hand-write the `validator.regex` |
319319

320320
**UUID columns are `field.uuid``field.string` + `@dbColumnType: uuid` is a forbidden smell.**
@@ -352,6 +352,24 @@ lives in `field.timestamp` (instant by default) + the `@localTime` naive opt-out
352352
{ "field.timestamp": { "name": "opensAt", "@localTime": true } }
353353
```
354354

355+
**URIs / IPs — strict by default, `@lenient` to store any string (#234).** `field.uri` is a
356+
strict **absolute, scheme-bearing URI** (`https://a.com`, `mailto:a@b`; a scheme-less
357+
`example.com` or a bare `/path` is rejected) and `field.inet` is a strict **IPv4/IPv6 literal**
358+
(no hostnames, no CIDR). When a field must hold a *not-necessarily-well-formed* value — an
359+
LLM-emitted citation URL, a user-supplied host that might be a name — add **`@lenient: true`**:
360+
codegen then binds a plain string (no URL/IP validator) and a `field.inet @lenient` uses a plain
361+
`text` column instead of the native `inet` type (so a value the native column would reject at
362+
INSERT round-trips unchanged). Strict is the default; `@lenient` is the deliberate opt-out (never
363+
default-true — the same shape as `@localTime`). **Toggling `@lenient` on an existing `field.inet`
364+
is schema-affecting** (`inet``text`), so it shows up as an `ALTER` on the next `meta migrate`.
365+
366+
```json
367+
{ "field.uri": { "name": "homepage" } } // strict — must be an absolute URL
368+
{ "field.uri": { "name": "citationUrl", "@lenient": true } } // any string (LLM-emitted, may be malformed)
369+
{ "field.inet": { "name": "sourceIp" } } // strict — IPv4/IPv6 literal only
370+
{ "field.inet": { "name": "reportedHost", "@lenient": true } } // any string; text column
371+
```
372+
355373
**`@autoSet` — let the runtime stamp created/updated times; never hand-set them.**
356374
`@autoSet` is registered on the temporal subtypes (`field.date` / `field.time` /
357375
`field.timestamp`) and takes **`onCreate`** (stamp on insert) or **`onUpdate`**

fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,8 @@ Canonical form for common field needs — reach for these before inventing anyth
313313
| Long / unbounded text | bare `field.string` | add `@maxLength` only when you want `varchar(N)` |
314314
| Nested structured value | `field.object` | `@objectRef` + `@storage` |
315315
| Open JSON bag (no fixed shape) | `field.string` + `@dbColumnType: jsonb` | logical type stays string; column is jsonb |
316-
| URL / URI | `field.uri` | native `URI`/`Uri`; `text` column; URL validation — a real native type + behavior, so a subtype (not a validated string) |
317-
| IP address | `field.inet` | native IP type; Postgres `inet` column |
316+
| URL / URI | `field.uri` | native `URI`/`Uri`; `text` column; strict absolute-URI validation (add `@lenient: true` to store any string) — a real native type + behavior, so a subtype (not a validated string) |
317+
| IP address | `field.inet` | native IP type; Postgres `inet` column; strict IPv4/IPv6-literal validation (add `@lenient: true` for a plain-string `text` column) |
318318
| Validated plain string (email / hostname) | `field.string` + `@stringFormat` | `@stringFormat: email` or `@stringFormat: hostname` — idiomatic per-port validation; don't hand-write the `validator.regex` |
319319

320320
**UUID columns are `field.uuid``field.string` + `@dbColumnType: uuid` is a forbidden smell.**
@@ -352,6 +352,24 @@ lives in `field.timestamp` (instant by default) + the `@localTime` naive opt-out
352352
{ "field.timestamp": { "name": "opensAt", "@localTime": true } }
353353
```
354354

355+
**URIs / IPs — strict by default, `@lenient` to store any string (#234).** `field.uri` is a
356+
strict **absolute, scheme-bearing URI** (`https://a.com`, `mailto:a@b`; a scheme-less
357+
`example.com` or a bare `/path` is rejected) and `field.inet` is a strict **IPv4/IPv6 literal**
358+
(no hostnames, no CIDR). When a field must hold a *not-necessarily-well-formed* value — an
359+
LLM-emitted citation URL, a user-supplied host that might be a name — add **`@lenient: true`**:
360+
codegen then binds a plain string (no URL/IP validator) and a `field.inet @lenient` uses a plain
361+
`text` column instead of the native `inet` type (so a value the native column would reject at
362+
INSERT round-trips unchanged). Strict is the default; `@lenient` is the deliberate opt-out (never
363+
default-true — the same shape as `@localTime`). **Toggling `@lenient` on an existing `field.inet`
364+
is schema-affecting** (`inet``text`), so it shows up as an `ALTER` on the next `meta migrate`.
365+
366+
```json
367+
{ "field.uri": { "name": "homepage" } } // strict — must be an absolute URL
368+
{ "field.uri": { "name": "citationUrl", "@lenient": true } } // any string (LLM-emitted, may be malformed)
369+
{ "field.inet": { "name": "sourceIp" } } // strict — IPv4/IPv6 literal only
370+
{ "field.inet": { "name": "reportedHost", "@lenient": true } } // any string; text column
371+
```
372+
355373
**`@autoSet` — let the runtime stamp created/updated times; never hand-set them.**
356374
`@autoSet` is registered on the temporal subtypes (`field.date` / `field.time` /
357375
`field.timestamp`) and takes **`onCreate`** (stamp on insert) or **`onUpdate`**

fixtures/conformance/field-uri-inet-stringformat/expected.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@
3333
"@stringFormat": "hostname"
3434
}
3535
},
36+
{
37+
"field.uri": {
38+
"name": "citationUrl",
39+
"@lenient": true
40+
}
41+
},
42+
{
43+
"field.inet": {
44+
"name": "reportedIp",
45+
"@lenient": true
46+
}
47+
},
3648
{
3749
"identity.primary": {
3850
"name": "id",

fixtures/conformance/field-uri-inet-stringformat/input/meta.contact.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@
3333
"@stringFormat": "hostname"
3434
}
3535
},
36+
{
37+
"field.uri": {
38+
"name": "citationUrl",
39+
"@lenient": true
40+
}
41+
},
42+
{
43+
"field.inet": {
44+
"name": "reportedIp",
45+
"@lenient": true
46+
}
47+
},
3648
{
3749
"identity.primary": {
3850
"name": "id",

0 commit comments

Comments
 (0)