feat(iris_production_item): read several items' settings in one round trip - #372
Conversation
|
CI is green now (all 5 jobs; 14 new tests confirmed by name in the log, a fabricated name returning 0 as the control). The first run failed, and on the right test. So the instrument now counts what the claim is about — every generator that can report a missing item emits a declared candidate count, every reader routes through the one envelope builder — instead of one call spelling. Both halves are still 4; the fourth member changed. Two things worth flagging for review: The reader count needed a real counter. Why it reached CI. My local gate ran the targets I expected to matter rather than the targets that read what the diff touches. Deriving the list by grepping |
01dba22 to
0197e83
Compare
… trip Item 3 of issue 327. Measured over 31,009 parsed tool_use blocks: the tool arrives in bursts (53 runs of 2, 14 of 3, up to 8) and get_settings is 296 of the 513 action invocations inside them — the bursts are mostly reading several items' settings one at a time. 174 calls go away. `get_settings` now builds one framed program for one item or twenty. Not two paths: a second copy of the read is the #105 shape, where the copy nobody exercises keeps the old behaviour and looks more trustworthy for having a working sibling. The RESPONSE shape still differs — a call that passes no list reports exactly what it always did, so no existing caller changes. The single-item program wrote bare `Name=Value` lines. With more than one item those cannot be attributed, and a value is free text. Read from a live production for the fixtures (`Comun.Produccion`, IRIS 2026.1, read-only over Atelier — 10 items, 65 settings): * `ReplyCodeActions` is `:?R=F,:?E=F,:~=S,:?A=C,:*=S,:I?=W,:T?=C`, so splitting on every `=` truncates a real setting at `:?R`. * 23 of the 65 settings are Adapter-targeted, and `Setting.Target` is not in the output. So each section declares how many settings to expect — the mechanism #347 used to make a short `%Status` chain detectable — and a line matching no marker is appended to the previous value rather than dropped. * `Missing` — the production holds no such item. * `Unreadable` — the section carried neither a count nor a marker. Neither of these is an item with no settings; reporting them that way answers a failure with a fact (#310). * `settings_incomplete` — fewer (or more) settings arrived than IRIS declared: the output was cut. * `duplicate_setting_names` — the rows that DID arrive collapsed in the map, because the program drops `Setting.Target`. Pre-existing, now counted instead of silent. `success` stays about the CALL; `all_found` and `missing` are about the answer, and a single short section is enough to make `all_found` false. Every name absent returns the same ITEM_NOT_FOUND envelope a single-item call has always given, built by the one reader that has always built it. Measured on IRIS 2026.1: a `Quit` inside an `If` block returns from the METHOD. One missing item would have ended the run, and every later item would have been absent from the output — silently, since absence is how a caller reads "no settings". That is why `item_not_found_block`, which ends in `Quit` by design, is not reused per item; its candidate block is emitted once. `ACTIONS_NEEDING_AN_ITEM`'s refusal read `params.item` alone. A caller passing only `items: ["A","B"]` — the shape the new parameter invites — was told "nothing was sent to FindItemByConfigName" while having named two things: a true sentence about the wrong parameter, which is the failure mode this issue is about. The guard now asks whether ANY spelling named something, and a list of exactly one name is filled into `item` so every action accepts it. Only `get_settings` reads a list of several. On every other action a list of more than one is refused by name rather than acted on for the first and dropped for the rest. fmt, clippy -D warnings, cargo build --workspace, and the gate with IRIS env unset: 5 result lines, 0 FAILED. 26 new tests, fixtures read from a live production rather than invented. 14 mutations applied one at a time — including "only the first item is written into the program", "the missing branch Quits", "the value is split on every equals sign" and "the handler reads `item` alone again" — each red on its named tests, restored green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…l spelling
CI caught this: `all_four_sites_and_all_four_readers_use_the_shared_definitions` went red on the
previous commit, and it was right to. That test is the sibling-defect guard for ITEM_NOT_FOUND,
and the batch read changed the POPULATION it counts.
`get_settings` no longer interpolates `{not_found}`. It cannot: the shared block ends in `Quit`,
which returns from the METHOD, so one missing item would end the whole program and every later
item would be absent from the output. It emits the same declared-count candidate protocol
itself, once, after the last item.
So the instrument now counts what the claim is about — every generator that can report a missing
item emits a declared candidate count, and every reader routes through the one envelope builder —
rather than one particular call spelling. Both halves are still 4; the fourth member changed.
The reader count needed a real counter, not a substring match. `text.matches("item_not_found(")`
reads 5, because `interop.rs` has a test called `production_item_error_mapping_item_not_found` —
an identifier that merely ENDS with the name, which is the commonest false witness in a
source-reading guard. `call_sites` skips declarations and requires a non-identifier character
before the name, with three controls: a declaration counts 0, that test's name counts 0, and a
real call counts 1 — so the two negatives cannot be satisfied by counting nothing at all.
Also the reason this reached CI at all: the local gate ran the targets I expected to matter
rather than the targets that read what the change touched. The target list is now derived —
grep the tests directory for the symbols and files the diff moves — which finds this one and
nine others.
Mutations, both red on the guard and restored: get_settings stops emitting the protocol inline;
the batch reader stops routing through the shared envelope builder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… that never ran
Ran the batch read against a real production for the first time. Every get_settings call — the new
batch path AND the pre-existing single-item path — reported ITEM_NOT_FOUND for items the production
demonstrably has. The refusal even contradicted itself:
Item not found: Probe.BS.Feed
The production has 3 item(s): Probe.BS.Feed, Probe.BO.Out, Probe.BO.NoSettings
## Measured, on IRIS 2026.1 against a configured-but-never-started production
%OpenId("IOProbe.Produccion") -> object, no error
tProd.Items.Count() -> 3, all three names present
tProd.FindItemByConfigName("Probe.BS.Feed") -> NO object, ERROR #00: (no error description)
^Ens.Runtime("DispatchName") -> does not exist, 0 entries
Ens.Director.GetProductionStatus -> running production '', state 2
walking tProd.Items and matching .Name -> resolves all three, settings and Targets intact
FindItemByConfigName resolves through the RUNTIME dispatch index, which is only populated once a
production has been started or updated. This fork's whole workflow is to configure items and then run
tests, so the items of a production being BUILT are exactly the ones it cannot find — and the %Status
it sets carries no text, so there was nothing to report.
The batch program now walks tProd.Items and matches .Name, which is config-level, needs no runtime
index, and is what build_list_items_code already did successfully.
NOT FIXED HERE: enable, disable, set_settings and add use the same call and fail the same way on a
never-started production. Filed separately rather than widened into this PR.
## Two more defects the live run found
* The wire marker leaked. `synthesise_not_found_payload` built "ERROR:ITEM_NOT_FOUND:Item not found:
…", and `item_not_found` takes its message from line 0 — so the caller's `error` began with the
marker. The single-item arm strips it before handing the payload over; the synthesised one must not
add it back. Same leak #358 fixed for INTEROP_ERROR.
* The response SHAPE regressed for every existing caller. `items` non-empty selects the batch
payload, and `item_names_arg` folds a single `item` into that same vector — so a plain `item=` call
came back as `results[]` instead of `{item, settings}`. Now keyed on `list_parameter_given`, which
asks whether a LIST key was supplied. The pure-function test could not see this: it sets the flag
by hand, and the WIRING computed it wrongly.
## One mutation survived, and it was the actual defect
Reverting the codegen to FindItemByConfigName passed all 25 tests in the file, because they feed the
PARSER hand-written marker text and cannot see the ObjectScript — the third time this week that
blindness hid a wrong premise about what IRIS does. The new program-level assertion is red against
it.
Its first control was also wrong — an exact count of GetAt references, which failed on correct code
because the trailing candidate block has one of its own. Replaced with a count of per-item walks.
Verified end to end after the fix: the 3-item batch returns 4, 3 and 0 settings with all_found true;
one-real-one-missing gives all_found false with the miss named; all-missing gives ITEM_NOT_FOUND with
no wire marker; `item=` returns the legacy shape and `items=[one]` the list shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0197e83 to
bd54689
Compare
Item 3 of issue 327. Items 1 and 2 are elsewhere — item 2 is #371; item 1 is still open.
Measured over 31,009 parsed
tool_useblocks:iris_production_itemarrives in bursts (53 runs of2, 14 of 3, up to 8), and
get_settingsis 296 of the 513 action invocations inside them. Thebursts are mostly reading several items' settings one at a time. 174 calls disappear.
iris_production_item { action: "get_settings", items: ["Censo.BS.HL7", "Censo.Router", "Censo.BO.DietoolsHL7"] }One program, whatever the count
One framed program serves one item or twenty. Not two paths — a second copy of the read is the #105
shape, where the copy nobody exercises keeps the old behaviour and looks more trustworthy for
having a sibling that works. The response shape still differs: a call that passes no list gets
exactly the payload it always did, so no existing caller changes.
The fixtures are real, and two of their properties drove the design
Read on 2026-09-23 from a live production —
Comun.Produccion, IRIS 2026.1, read-only over Atelier:10 config items, 65 settings.
ReplyCodeActions=:?R=F,:?E=F,:~=S,:?A=C,:*=S,:I?=W,:T?=C=truncates a real setting at:?R— only the first separatesTarget="Adapter", andSetting.Targetis not in the outputThe single-item program wrote bare
Name=Valuelines. With more than one item those cannot beattributed to an item at all, and a setting value is free text. So each section now declares how many
settings to expect — the mechanism #347 used to make a short
%Statuschain detectable — and a linematching no marker is appended to the previous value rather than dropped, because a plausible
shorter value is the worse of the two failures.
What a caller can now tell apart
Four distinguishable outcomes per item, none of which is "this item has no settings":
Missing— the production holds no item by that name.Unreadable— the section carried neither a count nor a missing marker. The program ran andsaid nothing this build can read; that is not an empty settings map ([meta] "a failure must never be answered with a negative fact" — 5 prior issues, 4 more found tonight, and no index anyone can find #310).
settings_incomplete(+settings_received/settings_declared) — fewer, or more, settingsarrived than IRIS declared. The output was cut.
duplicate_setting_names— the rows that did arrive collapsed in the map, because theprogram drops
Setting.Target. Pre-existing; now counted rather than silent. On the productionabove no name is used under both targets, so this is a latent risk there rather than an observed
one — but the round-trip half of it is unconditional, and I have filed it separately.
And at the batch level:
{ "success": true, "count": 3, "all_found": false, "missing": ["Nope"], "results": [ {"item": "...", "found": true, "settings": {...}, "settings_declared": 5}, ... ] }successis about the call — the read happened.all_foundis about the answer, and asingle short section is enough to make it false, so a caller reading only
successcannot concludeevery item came back whole. Every name absent returns the same
ITEM_NOT_FOUNDenvelope asingle-item call has always given, with its candidate list, built by the one reader that has always
built it rather than a second copy growing here.
No
Quitin the missing branchMeasured on IRIS 2026.1: a
Quitinside anIfblock returns from the method. One missing itemwould have ended the run, and every later item would have been absent from the output — silently,
since absence is exactly how a caller reads "no settings". That is why
item_not_found_block, whichends in
Quitby design, is not reused per item; its candidate block is emitted once, at the end.a_missing_item_does_not_end_the_programguards it, reading only the missing branch.A defect in the existing guard that this change had to fix
ACTIONS_NEEDING_AN_ITEM's refusal readparams.itemalone. A caller passing onlyitems: ["A","B"]— the shape the new parameter invites — was told "nothing was sent toFindItemByConfigName" while having named two things. A true sentence about the wrong parameter is
the failure mode this whole issue is about. The guard now asks whether any spelling named
something, and a list of exactly one name is filled into
item, so every action accepts it ratherthan rejecting a request that named precisely what it needs in the other spelling.
A list of more than one is refused, by name, on every action but
get_settings— acting on thefirst and dropping the rest is the partial-work shape
doc.rs'srequire_namerecords.Verification
cargo fmt --all -- --checkclean,cargo clippy --workspace --all-targets -- -D warningsclean,cargo build --workspacebefore the run.name; a fabricated name returns 0 as the control.
ProductionItemParamsliterals updated.the program; the missing branch
Quits; the missing branch writes a marker the reader does notknow; the value split on every
=; a continuation line dropped; a short section reading ascomplete; a missing item becoming an item with no settings; a collapse not counted;
all_foundignoring completeness; an unframed reply reading as an empty batch; every action accepting a list;
itemsnot counting as naming an item; the handler readingitemalone again; the descriptionnot naming what comes back.
Two mutations did not produce a verdict on the first pass and were re-run rather than counted:
two anchors missed the escaped quotes inside the
format!literal, and one of those, once applied,deleted a named format argument and so failed to compile — an invalid mutation, not a survivor. It
was re-targeted to change the marker text instead, which compiles and is red.
Not verified against a live instance
The generated program is unit-tested, as every other
build_*_codein this file is, but it has notbeen run.
get_settingsreaches IRIS throughexecute_via_generator, which PUTs a scratchclass, and the only interop-enabled namespace I can reach (43080/DEMO) is the read-only instance;
the two writable instances have no interop namespace. So the CI e2e job is the first place this
program executes. The framing decisions that a live run would have checked — per-item attribution,
the declared count, no
Quitin the missing branch — are each pinned by a test above.Issue 327 stays open for item 1.