Affected port(s): Python (behaviour likely shared — please check TS/Java/C#)
Package + version: metaobjects 1.0.0 (PyPI)
What happened
The JSON payload locator behind extract_object(...) takes the first object that
parses and stops. It does not prefer a fenced block, and it does not fall through
when that first object fails the declared shape.
So any object-shaped text appearing before the real answer wins, and a perfectly good
reply sitting in a following ```json fence is never reached. Because the
declared @required fields are then missing, the reply is reported as unusable — the
answer is not mis-parsed, it is silently discarded.
Measured against 1.0.0 with this value-object:
metadata:
package: probe
children:
- object.value:
name: gate
children:
- field.string: {name: topic}
- field.boolean: {name: worth_forming, required: true}
- field.string: {name: reason, required: true}
and GOOD = {"topic":"real","worth_forming":true,"reason":"r"}:
| input |
topic |
has_lost_required() |
GOOD |
'real' |
False |
```json\nGOOD\n``` |
'real' |
False |
Thinking...\n```json\nGOOD\n``` |
'real' |
False |
{"topic":"draft_idea"}\n```json\nGOOD\n``` |
'draft_idea' |
True |
The format is {"topic": "<slug>"}.\n```json\nGOOD\n``` |
'<slug>' |
True |
Consider {a set} of options.\n```json\nGOOD\n``` |
None |
True |
The last three are the defect. In each, GOOD is present and complete in the fence.
The trigger is ordinary model behaviour, not adversarial input: a model that reasons
before answering writes a partial object in its reasoning, or echoes the requested
format back, or simply writes prose containing a brace.
What you expected
Two changes, both of which the locator has enough information to make:
- Prefer fenced blocks. When the reply contains
``` blocks, look inside
them before scanning the whole text. Models are explicitly instructed to fence, so
the fenced block is the answer and anything outside it is commentary.
- Fall through on shape mismatch. An object that does not carry any declared field
should not end the search. Keep scanning; a later, better-shaped object should win.
(Symmetrically: a fenced block of the wrong shape — a fenced example, a fenced note —
must not shadow a real answer elsewhere, so fence-preference needs the same
fall-through.)
With both, all six rows above resolve to topic='real'.
Why this matters beyond the rows
This is the failure mode that makes adopters hand-roll their own readers instead of
using the generated extract. In one Python codebase I audited there were six
independent JSON extractors, each having separately discovered a different variation of
dirty model output. Two measurements from that codebase's production telemetry, on
replies stored verbatim:
- one prompt: 80 calls, 79 fenced, and a naive reader rejected every one — the
subsystem produced nothing for ten days and nobody noticed, because its failure was
indistinguishable from its "nothing to report";
- another prompt: 22 of 270 replies (8.1%) lost to commentary after the closing
fence, degrading retrieval on roughly one turn in twelve.
Both are exactly what the generated extract should make unnecessary. It nearly does —
1.0.0 already handles fenced, prose-before, commentary-after, two fenced blocks, and
even a trailing comma at NORMAL tolerance, which is more than several of those
hand-rolled readers managed. The first-object-wins rule is the remaining gap, and while
it stands, an adopter's hand-rolled reader is measurably more capable than the
platform's — which is the wrong way round.
Reproduction
Load the metadata above, then:
from metaobjects.meta.core.object.object_extract import extract_object
from metaobjects.render import Format
GOOD = '{"topic":"real","worth_forming":true,"reason":"r"}'
text = '{"topic":"draft_idea"}\n```json\n' + GOOD + '\n```'
out = extract_object(mo, text, Format.JSON, None)
# actual: topic='draft_idea', has_lost_required() == True
# expected: topic='real', has_lost_required() == False
Environment
Linux, CPython 3.11.14, metaobjects 1.0.0 from PyPI, installed with pip.
Related
Filed alongside a separate FR for the extension-surface gap this exposed — the override
surface (ExtractOptions.on_field / normalizers / tolerance) is per-field-value
only, so an adopter cannot supply a payload-location strategy without forking.
Affected port(s): Python (behaviour likely shared — please check TS/Java/C#)
Package + version:
metaobjects1.0.0 (PyPI)What happened
The JSON payload locator behind
extract_object(...)takes the first object thatparses and stops. It does not prefer a fenced block, and it does not fall through
when that first object fails the declared shape.
So any object-shaped text appearing before the real answer wins, and a perfectly good
reply sitting in a following
```jsonfence is never reached. Because thedeclared
@requiredfields are then missing, the reply is reported as unusable — theanswer is not mis-parsed, it is silently discarded.
Measured against 1.0.0 with this value-object:
and
GOOD = {"topic":"real","worth_forming":true,"reason":"r"}:topichas_lost_required()GOOD'real'```json\nGOOD\n```'real'Thinking...\n```json\nGOOD\n```'real'{"topic":"draft_idea"}\n```json\nGOOD\n```'draft_idea'The format is {"topic": "<slug>"}.\n```json\nGOOD\n```'<slug>'Consider {a set} of options.\n```json\nGOOD\n```NoneThe last three are the defect. In each,
GOODis present and complete in the fence.The trigger is ordinary model behaviour, not adversarial input: a model that reasons
before answering writes a partial object in its reasoning, or echoes the requested
format back, or simply writes prose containing a brace.
What you expected
Two changes, both of which the locator has enough information to make:
```blocks, look insidethem before scanning the whole text. Models are explicitly instructed to fence, so
the fenced block is the answer and anything outside it is commentary.
should not end the search. Keep scanning; a later, better-shaped object should win.
(Symmetrically: a fenced block of the wrong shape — a fenced example, a fenced note —
must not shadow a real answer elsewhere, so fence-preference needs the same
fall-through.)
With both, all six rows above resolve to
topic='real'.Why this matters beyond the rows
This is the failure mode that makes adopters hand-roll their own readers instead of
using the generated extract. In one Python codebase I audited there were six
independent JSON extractors, each having separately discovered a different variation of
dirty model output. Two measurements from that codebase's production telemetry, on
replies stored verbatim:
subsystem produced nothing for ten days and nobody noticed, because its failure was
indistinguishable from its "nothing to report";
fence, degrading retrieval on roughly one turn in twelve.
Both are exactly what the generated extract should make unnecessary. It nearly does —
1.0.0 already handles fenced, prose-before, commentary-after, two fenced blocks, and
even a trailing comma at NORMAL tolerance, which is more than several of those
hand-rolled readers managed. The first-object-wins rule is the remaining gap, and while
it stands, an adopter's hand-rolled reader is measurably more capable than the
platform's — which is the wrong way round.
Reproduction
Load the metadata above, then:
Environment
Linux, CPython 3.11.14,
metaobjects1.0.0 from PyPI, installed with pip.Related
Filed alongside a separate FR for the extension-surface gap this exposed — the override
surface (
ExtractOptions.on_field/normalizers/tolerance) is per-field-valueonly, so an adopter cannot supply a payload-location strategy without forking.