Skip to content

feat(iris_production_item): read several items' settings in one round trip - #372

Merged
PYDuquesnoy merged 3 commits into
masterfrom
feat/production-item-batch-settings
Sep 28, 2026
Merged

PYDuquesnoy merged 3 commits into
masterfrom
feat/production-item-batch-settings

Conversation

@PYDuquesnoy

Copy link
Copy Markdown
Contributor

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_use blocks: iris_production_item 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 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.

what the real data showed consequence
ReplyCodeActions = :?R=F,:?E=F,:~=S,:?A=C,:*=S,:I?=W,:T?=C splitting on every = truncates a real setting at :?R — only the first separates
23 of 65 settings are Target="Adapter", and Setting.Target is not in the output a Host and an Adapter setting of the same name collapse to one map key

The single-item program wrote bare Name=Value lines. With more than one item those cannot be
attributed 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 %Status chain detectable — and a line
matching 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 and
    said 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, 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 rather than silent. On the production
    above 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}, ... ] }

success is about the call — the read happened. all_found is about the answer, and a
single short section is enough to make it false, so a caller reading only success cannot conclude
every item came back whole. Every name absent returns the same ITEM_NOT_FOUND envelope a
single-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 Quit in the missing branch

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 exactly 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, at the end.
a_missing_item_does_not_end_the_program guards it, reading only the missing branch.

A defect in the existing guard that this change had to fix

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 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 rather
than 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 the
first and dropping the rest is the partial-work shape doc.rs's require_name records.

Verification

  • cargo fmt --all -- --check clean, cargo clippy --workspace --all-targets -- -D warnings clean,
    cargo build --workspace before the run.
  • Gate with IRIS env unset: 5 result lines, 0 FAILED. Each of the 26 new tests confirmed by
    name; a fabricated name returns 0 as the control.
  • 26 new tests, and the four existing ProductionItemParams literals updated.
  • 14 mutations, each red on its named tests and restored green: only the first item written into
    the program; the missing branch Quits; the missing branch writes a marker the reader does not
    know; the value split on every =; a continuation line dropped; a short section reading as
    complete; a missing item becoming an item with no settings; a collapse not counted; all_found
    ignoring completeness; an unframed reply reading as an empty batch; every action accepting a list;
    items not counting as naming an item; the handler reading item alone again; the description
    not 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_*_code in this file is, but it has not
been run. get_settings reaches IRIS through execute_via_generator, which PUTs a scratch
class, 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 Quit in the missing branch — are each pinned by a test above.

Issue 327 stays open for item 1.

@PYDuquesnoy

Copy link
Copy Markdown
Contributor Author

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. all_four_sites_and_all_four_readers_use_the_shared_definitions is the sibling-defect guard for ITEM_NOT_FOUND, and this change legitimately altered the population it counts: get_settings no longer interpolates {not_found}, because that block ends in Quit — which returns from the METHOD, so one missing item would end the whole program. 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, 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. matches("item_not_found(") reads 5, because this file has a test called production_item_error_mapping_item_not_found — an identifier that merely ends with the name. call_sites skips declarations and requires a non-identifier character before the name, with three controls (a declaration → 0, that test's name → 0, a real call → 1) so the two negatives cannot be satisfied by counting nothing.

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 tests/ for the moved symbols finds item_not_found_names_candidates and nine others; that derived gate is green (11 result lines, 0 FAILED). Worth knowing generally: a guard that counts a population lives in a file named after the defect it guards, never after the code it reads.

PYDuquesnoy and others added 3 commits September 28, 2026 11:46
… 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>
@PYDuquesnoy
PYDuquesnoy force-pushed the feat/production-item-batch-settings branch from 0197e83 to bd54689 Compare September 28, 2026 09:50
@PYDuquesnoy
PYDuquesnoy merged commit dbe4c51 into master Sep 28, 2026
4 of 5 checks passed
@PYDuquesnoy
PYDuquesnoy deleted the feat/production-item-batch-settings branch September 28, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant