feat(stream_inspect): adopt the capability #352 asked about, not upstream's implementation - #376
PYDuquesnoy wants to merge 5 commits into
Conversation
Issue 353's remaining acceptance item. #359 made the mechanical half computed — which of upstream's tools we advertise, which we ported and pruned, which we never ported, read from the two trees rather than restated. That says nothing about WHY, and why is the whole difference between a profile and an omission. ## The issue's own numbers are stale, and this writes none down 353 says upstream documents 71 tools, the profile is 23, and 48 are excluded. Computed from upstream/master and HEAD today: 93 upstream, 32 advertised (30 upstream + 2 fork-local), 63 upstream-only. So the file carries NO counts — a number here would be a second source of truth for something `scripts/validate-tools.sh` prints, and every prose count in this repo has gone stale in both directions. ## Four categories, because two cannot distinguish "never" from "not yet" `tools-excluded.json` classifies all 63: * `unsafe-here` (6) — lets an agent damage an instance or reach past what this fork is for. global_kill, global_preview, iris_global, and the iris_ws_* trio. * `not-interop` (41) — legitimate upstream capability outside the remit: instance administration, the server registry, containers, the skill/KB machinery, agent telemetry. * `covered-here` (5) — already answered by a tool the fork advertises: iris_source_control by iris_doc's own checkout, the four debug_* by iris_debug and iris_get_log. * `wanted` (11) — would help interop work and is not excluded on principle. compare_namespace, compare_document, stream_inspect, iris_generate_test, mermaid_production, resolve_storage carry the issue tracking the decision; iris_search, iris_info, iris_namespace_list, my_access and resolve_dynamic_dispatch say what they would be for. Every reason is grounded in upstream's own tool description, read from upstream/master rather than inferred from the tool's name. ## Two gates, split by what each can actually check The JOIN — did every upstream-only tool get classified — needs `upstream/master`, and CI clones do not carry that remote; the surface section already prints NOT CHECKED and exits 0 there for that reason. So it lives in the script as a maintainer gate that exits 1, and reports three distinct faults: UNCLASSIFIED (an upstream tool with no entry), STALE (a reason for a tool upstream no longer has), CONTRADICTORY (advertised and excluded at once). Everything that needs no remote is a Rust test, so it runs in the required CI gate: the file parses, every category is one the file itself defines, every reason is a real sentence rather than "not ported" restating the computed bucket, a tracking reference only ever sits on a `wanted` entry, nothing is both advertised and excluded, and no count is written down. ## Verification fmt, clippy -D warnings, cargo build --workspace, `scripts/validate-tools.sh` exit 0, and the derived test gate with IRIS env unset: 8 result lines, 0 FAILED. Targets chosen by grepping tests/ for what this diff touches rather than from memory. 9 mutations, each red and restored: a reason that restates the bucket; an undefined category; a reason too short to be one; a tracking ref on a tool excluded on principle; an advertised tool given an exclusion reason (caught by both the test and the script); a count written into the file; an upstream-only tool losing its entry; a reason for a tool upstream does not have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ream's implementation Issue 352's four-tool profile decision. One adopt, two declines with measured reasons, and one premise correction. ## iris_coverage — already adopted; the issue's premise is stale 352 says all four are "absent from our 23". iris_coverage is in INTEROP_TOOLS and has been since it was added after the issue was filed. Nothing to decide. ## stream_inspect — ADOPT, and its precondition was verified first 352 makes adoption conditional on "confirmed non-consuming before adoption". Measured on IRIS 2026.1 with a throwaway %Stream.GlobalCharacter (probe deleted afterwards): instance A: Read, Read again -> "line-1", then "" with AtEnd=1 instance B: a FRESH %OpenId, after A -> Size=6, Read -> "line-1" instance C: read .Size FIRST, then Read -> 6, then "line-1" instance A again -> still "", still AtEnd=1 Read position is per-instance and never saved. So an inspector that opens the stream itself cannot consume what a later reader needs, and reading Size advances nothing. That also corrects the issue's framing. It asks for "is the stream empty, or already read?" — but since position is never persisted, "already read" is not a state a later reader can observe. A stream a relay consumed still reads from the start for the next %OpenId. ### The capability, not the port Upstream's stream_inspect_impl was read at upstream/master. Three deliberate differences: 1. It calls `Do stream.Rewind()` — a mutation on an object the caller asked us to LOOK at, whose harmlessness is a property of IRIS rather than of the tool. Nothing here writes. 2. It reads the ENTIRE stream into one variable with no cap. Measured on this instance family, 3,600,000 characters survive a round trip and 3,700,000 fails <MAXSTRING> -> SQLCODE -400 -> -149, so it would fail on exactly the payload a caller most wants to see. This reads max_chars and reports the full size separately, with `truncated` when what came back is a prefix. 3. It writes the body on ONE line. An HL7 v2 message is CR-separated and any XML or JSON body contains newlines, so every line after the first is dropped silently — the #347 defect this repo has already fixed twice. The body goes through write_marker_lines with a declared line count, so a short arrival is detectable instead of plausible. And the answer #352 actually wanted is kept distinct: an id that opens nothing reports STREAM_NOT_FOUND and says out loud that it is NOT an empty stream; a stream that opened and holds nothing reports size 0 with empty: true; a reply with no marker at all is STREAM_UNREADABLE. Three different places to look, three different answers. ## iris_generate_test — DECLINE, on the precondition it cannot meet 352 requires "output confirmed to match tdd's prescribed shape". Its system prompt asks the model to "Extend %UnitTest.TestCase", while tdd requires %UnitTest.TestProduction with Parameter PRODUCTION and TestControl neutralised. Its only output check is validate_cls_syntax — contains "Class ", contains a brace, braces balanced. So the shape is a request to an LLM, not a guarantee, and the acceptance criterion has no answer for a non-deterministic generator. ## mermaid_production — DECLINE, on measurement It reads only Ens_Config.Item and draws every item as a child of the production node: a one-level fan-out with no edge between items. On a real 10-item production, 2 item-to-item edges are knowable from TargetConfigName(s) and 2 more items route through a business rule — the imported diagram would show 0 of those 4. This fork already reads both sources, and #372 made the settings read batched, so it can answer a question the port cannot. ## A defect this found in #375's own gate The upstream-surface scan read `ours` from HEAD and INTEROP_TOOLS from the WORKTREE. While this change was uncommitted the two disagreed — stream_inspect showed as advertised AND never-ported, and the #353 join then reported it UNCLASSIFIED. Measured: exit 1 before the commit, exit 0 after, with no source change in between. Both halves now read HEAD and the section prints the caveat. ## Re-recorded counts, measured row by row Three gates pin the advertised count and caught this, as designed: the_read_only_split_is_pinned_per_toolset (all four toolsets +1) and two wire-level pins in mcp_handshake. The read-only figures are UNCHANGED and that is the measured point: stream_inspect is in GENERATOR_WRITE_TOOLS because it reads through a scratch class, so it is honestly not advertised readOnlyHint:true even though it writes nothing to the stream. ## Verification fmt, clippy -D warnings, cargo build --workspace, scripts/validate-tools.sh exit 0, and the derived test gate with IRIS env unset: 11 result lines, 0 FAILED. 12 new tests confirmed by name. 10 mutations, each red on its named test and restored: the program rewinds the stream; the read is not the capped Read; the body goes out on one line; size read after the body; an id that opens nothing reports an empty stream; a no-marker reply reads as a found empty stream; a prefix not flagged truncated; a binary stream's bytes reported as text; a short body not flagged incomplete; the profile read from the worktree again. Three needed re-targeting first — two anchors broke the format string and one left an unused named argument, all three compile errors rather than survivors, and none was counted until it compiled and went red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI caught six pinned counts the previous commit missed, and they were right to. Adding a tool moves the advertised surface, and this repo pins it in more places than one grep found: the_read_only_split_is_pinned_per_toolset mod.rs (already done) mcp_handshake x2 (already done) interop_e2e_tests::tools_list_returns_interop_profile 32 -> 33 test_toolset::test_baseline_tool_count 61 -> 62 test_toolset::test_nostub_tool_count 57 -> 58 test_toolset::test_merged_tool_count 53 -> 54 test_toolset::test_toolset_counts_match_doc_comments all four rows test_toolset::test_new_uses_pruned_baseline_router 61 -> 62 Toolset enum doc comments three headline counts Why my derived target list missed them: it globbed crates/iris-agentic-dev-core/tests/*.rs and test_toolset.rs lives in tests/unit/. A nested test module is reached through a top-level harness file, so the glob has to be recursive or the target is invisible to the derivation that is supposed to find it. The read-only figures are unchanged in every row, which is the measured point rather than an omission: stream_inspect is in GENERATOR_WRITE_TOOLS because it reads through a scratch class, so it is honestly not advertised readOnlyHint:true even though it writes nothing to the stream. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
CI green now — all 5 jobs, 11 new tests plus the re-recorded profile assertions confirmed by name in the log, fabricated name 0. The first run failed on six pinned tool counts, and they were right to fail: adding a tool moves the advertised surface and this repo pins it in nine places. Three I had already re-recorded; the other six were Why my derived target list missed them — worth recording because it is a second miss of the same kind: the derivation globs Every figure was re-recorded by running the gate and reading what it reported, not by arithmetic — which matters here, because the read-only counts do not move with the totals: |
|
Do not merge this as it stands — I ran the tool end-to-end for the first time and three of its headline claims are false. I had said live verification was blocked because my writable instance has no interop namespace; that was wrong, a
Why, measuredThree wrong premises of mine, each of which the unit tests could not see because they fed the parser hand-written marker text:
The third is the one I most regret: the PR text singles out HL7 as the case the line protocol exists for, and that is exactly the case it breaks. The fix I am implementingRead through Also worth recording: a |
…ing it
I had said live verification was blocked because my writable instance has no interop namespace.
That was wrong — a %Stream.GlobalCharacter needs no interop namespace. Driving the built binary
against a real instance for the first time, three of this PR's headline claims were false.
case PR claimed actually returned
stream_id 999, no such id STREAM_NOT_FOUND, 'NOT empty' success:true size:0 empty:true
a %Stream.GlobalBinary size given, bytes omitted class:GlobalCharacter, body_chars:1
a CR-separated HL7 body 'arrives whole' segments concatenated, both CRs gone
## The three wrong premises, measured
%OpenId("999") -> $IsObject 1, ISERR 0, text "" <- a usable EMPTY object, not a failure
GlobalCharacter.%OpenId on a BINARY stream -> succeeds; $classname = GlobalCharacter
%ExistsId: "999"->0 "5"->1 ""->0
GlobalBinary read of a CHARACTER stream -> 106 chars, both CRs intact
Base64Encode(x,1) -> lossless round trip
1. `'$IsObject` cannot detect a missing stream, so the not-found arm never fired. `%ExistsId` can.
2. Character vs binary is NOT knowable from which %OpenId succeeded — both open the same storage
and $classname reports the class you opened with. So `class` echoed my own choice back, and the
binary branch was unreachable while binary bytes came back mangled.
3. `write_marker_lines` deletes CR by design — right for a CRLF %Status chain, destructive for a
CR-only HL7 v2 body, which collapsed to one line with its segment boundaries gone while the
declared count still said "complete".
The third is the one I most regret: this PR's text singles out HL7 as the case the line protocol
exists for, and that is the case it broke.
## The fix
%ExistsId for existence; always read through %Stream.GlobalBinary (exact bytes for either kind);
base64 for the body, which needs no line protocol and survives CR, NUL and anything else.
`body_base64` is always exact; `body` appears only when the bytes are valid UTF-8, otherwise
`body_not_text` points at the base64 rather than handing over a mangled string. `class` becomes
`opened_as`, because the data's kind is not knowable from here.
Re-verified end to end against a live instance after the fix: the HL7 body returns 106 bytes with
both CRs, the binary stream 15 exact bytes with no `body` offered, max_chars=20 of 500 flagged
truncated, and id 999 reports STREAM_NOT_FOUND. Probe streams and classes deleted afterwards.
## Two mutations survived first, and both named a missing oracle
* Dropping base64 padding survived, because the round-trip test checked my encoder against my own
decoder — which filters padding out. `body_base64` goes to a caller using a real base64 library,
so the padding is contract: now pinned against RFC 4648's vectors.
* Re-adding the CR-deleting $TRANSLATE survived every parser test, because those feed hand-written
marker text and cannot see the ObjectScript. That is the same blindness that let the original
defect ship. The program is now asserted to contain no $TRANSLATE, no $CHAR(13) and no $PIECE.
8 mutations, all with verdicts after re-targeting two invalid ones (a removed format argument, and
one that failed to compile): character-class read; $IsObject existence; padding dropped; lossy UTF-8
offered as text; a missing id reporting emptiness; an undecodable body becoming an empty one; a short
arrival unflagged; the CR-deleting translate restored.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Issue 351. Two premise corrections, a defect in #376 fixed, and a survivor that named a missing assertion on the issue's own acceptance item. compare_namespace_impl takes server_a, server_b and ONE namespace: it compares the same namespace on two REGISTERED SERVERS. This fork has no server registry — it connects to one instance named by IRIS_HOST/IRIS_NAMESPACE, which is why iris_servers and its siblings are recorded as out of remit. There is nothing for server_a to resolve against. So the axis is turned: two NAMESPACES on the connected instance. That covers the deployment case server reaches IRIS over HTTP and has no access to the caller's project directory. That is the same constraint recorded in #374, and it is why the source-of-truth check is a hook rather than a tool. Its per-document arm is `_ => { different.push(doc.clone()) }` — a document neither side could read is reported as a document that DIFFERS. A caller reading "12 classes differ" has no way to learn that 12 fetches failed. Here an unreadable document is its own bucket, and an unreadable namespace names which side, never a match report. claimed only when nothing differs, nothing is one-sided AND nothing went unchecked. Running it against real namespaces corrected my own first design: DEMO holds 4 `Comun.*` classes and USER holds 0, so the intersection is empty — and collapsing that into "nothing compared" would have HIDDEN the four classes missing from the other side, which is the answer a deployment check exists to get. Only BOTH sides empty is that state now. %Dictionary.CompiledClass.Hash, one SQL read per namespace — 2 round trips rather than upstream's two document fetches per class (400 for a 200-class comparison). `compared_on: "Hash"` is on the payload, because that is a weaker claim than identical source and this code does not make the stronger one; compare_document reads both sources for a named class. The %-exclusion was MEASURED, because the obvious spelling is wrong and a test over the generated text cannot see it: on DEMO, no filter gives 15875 rows, `NOT LIKE '%%%'` gives 0 (three wildcards — it excludes everything), and `NOT LIKE '\%%' ESCAPE '\'` gives 10116. The first version emitted the middle one and its test asserted the clause was PRESENT, so it passed while the query it built would have listed nothing on both sides. The match I added `stream_inspect` to last night is `mutating_call`, not a label table — so #376 shipped a read-only inspector WRITE-GATED: refused on a production instance and whenever the write gate is closed, which is exactly where diagnosing a relay matters. I repeated the mistake for both compare tools, and the read-only count moving by 2 instead of 3 is what forced me to read annotate_tool. All three are out of mutating_call now, with the reason recorded where the next reader will look. Correcting myself: #376's claim that stream_inspect is not advertised readOnlyHint:true BECAUSE it is in GENERATOR_WRITE_TOOLS was right — `read_only = !tool_can_mutate && !tool_writes_via_generator` — but the write-gating was a second, unnoticed error in the same edit. Capping `unchecked_count` the way the NAME list is capped went undetected: the cap test leaves 3 unchecked and the name cap is 50, so `take(50).count()` and `len()` agree. #351's acceptance is exactly "unchecked_count surfaced, never silently truncated", so the oracle could not see the thing it was written for. `the_unchecked_count_is_exact_even_past_the_name_cap` drives 67 past the cap; the mutation is red against it. Separately, `a_tracking_reference_points_somewhere_real` asserted `tracked >= 3` and went red because this change ADOPTED three of the tracked tools. A test that fails when the thing it tracks gets done punishes the outcome it encourages, so the quota is now a positive control (>= 1) with the reason recorded. fmt, clippy -D warnings, cargo build --workspace, scripts/validate-tools.sh exit 0, and the derived test gate with IRIS env unset: 11 result lines, 16 new tests confirmed by name, control 0. Target list derived with a RECURSIVE glob this time, which is what missed tests/unit/ last night. 11 mutations, each red on its named test and restored: a failed side degrading to an empty listing; a reply with no content array becoming one; an unreadable side reported as a comparison; both sides empty reading as clean; an empty intersection collapsing and hiding one-sided classes; in_sync ignoring unchecked and one-sided classes; the unchecked count capped; an unreadable document reported as a difference; the %-exclusion losing its ESCAPE; the compare tools write-gated again; the last tracking reference removed. The nine pinned tool counts are re-recorded by running each gate and reading what it reported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Fix pushed and CI green on all 5 jobs (tests confirmed by name, fabricated name 0). Re-verified end to end against a live instance after the change: the HL7 body comes back 106 bytes with both CRs intact, the binary stream 15 exact bytes with no Two mutations survived the first pass and both named a missing oracle:
#377 needed a rebase onto this branch's new tip and now merges cleanly; its CI is green too. One process note: my first run of the handshake tests after that rebase reported the profile as 33 tools instead of 35, and I nearly wrote it up as the rebase having dropped the |
…entation
Found by RUNNING compare_namespace, not by reading it: the INVALID_PARAMS text came back as
"Comparing a namespace with itself always reports in_sync".
A Rust string continued with a trailing backslash collapses to single spaces, so the usual
multi-line literal is fine. These five had the continuation COLLAPSED into one source line at some
point, which leaves the indentation inside the runtime string. Verified with a real rustc compile
and run that the re-split form produces single spaces, and by driving the tool: the repaired
stream_inspect message now comes back with no double space in it.
One of the five is mine from this week (stream_inspect's MISSING_PARAMS); the other four are
pre-existing caller-facing text — three iris_coverage hints and iris_doc's put/names refusal. Fixed
together because a guard over this class is only sound on a clean tree, and an exemption list for
four known offenders would rot.
## The detector, and why the obvious one does not work
A first scan reported 592 hits across 68 files and every one I checked was a false positive: it
matched across lines, so it was reading the SOURCE layout of correct -continuations rather than
the runtime string. The signature that actually distinguishes them is a 3+ space run with non-space
on both sides WITHIN ONE source line — a correct continuation puts the indentation at the start of
the NEXT line, where a single-line view never sees it. That narrowed 592 to 92, of which these five
were the caller-facing ones; the rest are whitespace test fixtures (" "), embedded ObjectScript
whose indentation is deliberate, and aligned trailing comments.
No test ships with this. The detector needs exclusion rules for those three noise classes, and a
guard that reddens on correct code is one I have had to loosen twice this week — so it gets filed
with its measurements rather than bolted on at the end of a night.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Issue 351. Two premise corrections, a defect in #376 fixed, and a survivor that named a missing assertion on the issue's own acceptance item. compare_namespace_impl takes server_a, server_b and ONE namespace: it compares the same namespace on two REGISTERED SERVERS. This fork has no server registry — it connects to one instance named by IRIS_HOST/IRIS_NAMESPACE, which is why iris_servers and its siblings are recorded as out of remit. There is nothing for server_a to resolve against. So the axis is turned: two NAMESPACES on the connected instance. That covers the deployment case server reaches IRIS over HTTP and has no access to the caller's project directory. That is the same constraint recorded in #374, and it is why the source-of-truth check is a hook rather than a tool. Its per-document arm is `_ => { different.push(doc.clone()) }` — a document neither side could read is reported as a document that DIFFERS. A caller reading "12 classes differ" has no way to learn that 12 fetches failed. Here an unreadable document is its own bucket, and an unreadable namespace names which side, never a match report. claimed only when nothing differs, nothing is one-sided AND nothing went unchecked. Running it against real namespaces corrected my own first design: DEMO holds 4 `Comun.*` classes and USER holds 0, so the intersection is empty — and collapsing that into "nothing compared" would have HIDDEN the four classes missing from the other side, which is the answer a deployment check exists to get. Only BOTH sides empty is that state now. %Dictionary.CompiledClass.Hash, one SQL read per namespace — 2 round trips rather than upstream's two document fetches per class (400 for a 200-class comparison). `compared_on: "Hash"` is on the payload, because that is a weaker claim than identical source and this code does not make the stronger one; compare_document reads both sources for a named class. The %-exclusion was MEASURED, because the obvious spelling is wrong and a test over the generated text cannot see it: on DEMO, no filter gives 15875 rows, `NOT LIKE '%%%'` gives 0 (three wildcards — it excludes everything), and `NOT LIKE '\%%' ESCAPE '\'` gives 10116. The first version emitted the middle one and its test asserted the clause was PRESENT, so it passed while the query it built would have listed nothing on both sides. The match I added `stream_inspect` to last night is `mutating_call`, not a label table — so #376 shipped a read-only inspector WRITE-GATED: refused on a production instance and whenever the write gate is closed, which is exactly where diagnosing a relay matters. I repeated the mistake for both compare tools, and the read-only count moving by 2 instead of 3 is what forced me to read annotate_tool. All three are out of mutating_call now, with the reason recorded where the next reader will look. Correcting myself: #376's claim that stream_inspect is not advertised readOnlyHint:true BECAUSE it is in GENERATOR_WRITE_TOOLS was right — `read_only = !tool_can_mutate && !tool_writes_via_generator` — but the write-gating was a second, unnoticed error in the same edit. Capping `unchecked_count` the way the NAME list is capped went undetected: the cap test leaves 3 unchecked and the name cap is 50, so `take(50).count()` and `len()` agree. #351's acceptance is exactly "unchecked_count surfaced, never silently truncated", so the oracle could not see the thing it was written for. `the_unchecked_count_is_exact_even_past_the_name_cap` drives 67 past the cap; the mutation is red against it. Separately, `a_tracking_reference_points_somewhere_real` asserted `tracked >= 3` and went red because this change ADOPTED three of the tracked tools. A test that fails when the thing it tracks gets done punishes the outcome it encourages, so the quota is now a positive control (>= 1) with the reason recorded. fmt, clippy -D warnings, cargo build --workspace, scripts/validate-tools.sh exit 0, and the derived test gate with IRIS env unset: 11 result lines, 16 new tests confirmed by name, control 0. Target list derived with a RECURSIVE glob this time, which is what missed tests/unit/ last night. 11 mutations, each red on its named test and restored: a failed side degrading to an empty listing; a reply with no content array becoming one; an unreadable side reported as a comparison; both sides empty reading as clean; an empty intersection collapsing and hiding one-sided classes; in_sync ignoring unchecked and one-sided classes; the unchecked count capped; an unreadable document reported as a difference; the %-exclusion losing its ESCAPE; the compare tools write-gated again; the last tracking reference removed. The nine pinned tool counts are re-recorded by running each gate and reading what it reported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ran this PR's test-after-put against a real instance for the first time. The behaviour is right in all five cases — a green suite gives test_ok: true; a RED suite gives test_ok: false while the write's own success stays true, which is the #310 separation working live; no compile gives test_skipped; a compile failure gives COMPILE_ERROR with the write envelope untouched and no test block; and no `test` parameter adds nothing at all. What was wrong is the text. The skip message came back as: not run: `test` needs a COMPILED class and this call did not compile. Pass compile=true Four messages, all in this PR: the three TestGate::skipped_reason arms and the unparseable-result note. Same collapsed line-continuation as the seven fixed in #376/#377 — I fixed those on the branches I happened to be working in and did not check the others. ## The scan that found the rest, and the one that did not Running the earlier detector over every branch was useless: master-descended branches report 15-40 hits each, because most matches are whitespace test fixtures, embedded ObjectScript whose indentation is deliberate, and aligned doc-comment tables. No conclusion is available from those numbers. Scoping it to the lines a branch ADDS (`git diff origin/master...origin/<br>`, `^+` only) makes the population precise. Across all fifteen open PR branches that gives four real leaks, all four here. Everything else the scan flags on those branches is intentional: measurement tables in doc comments, the deliberate `"ERROR #5002: "` truncation fixture, the real IRIS SQLCODE text in #368's fixtures (which genuinely contains double spaces), and the leaked string the #380 detector test asserts on. Verified at runtime after the fix by driving the built binary: the skip message contains no double space. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Recording the #352 linkage, because nothing in either PR references it and merging would leave it open — but deliberately not adding #352 asks for "a decision recorded per tool: adopt, or decline with a reason" across four tools. Where each one's status actually lives:
So #352 is satisfied by #375 + #376 jointly. Closing it is a merge-time action once both are in, which is why neither PR carries the keyword. Two concerns I raised and then measured awayBoth worth recording so nobody re-derives them:
Also worth noting for whoever merges: #352's own counts are stale — it was filed when the profile was 23 tools; it is 35 now. |
Ran this PR's test-after-put against a real instance for the first time. The behaviour is right in all five cases — a green suite gives test_ok: true; a RED suite gives test_ok: false while the write's own success stays true, which is the #310 separation working live; no compile gives test_skipped; a compile failure gives COMPILE_ERROR with the write envelope untouched and no test block; and no `test` parameter adds nothing at all. What was wrong is the text. The skip message came back as: not run: `test` needs a COMPILED class and this call did not compile. Pass compile=true Four messages, all in this PR: the three TestGate::skipped_reason arms and the unparseable-result note. Same collapsed line-continuation as the seven fixed in #376/#377 — I fixed those on the branches I happened to be working in and did not check the others. ## The scan that found the rest, and the one that did not Running the earlier detector over every branch was useless: master-descended branches report 15-40 hits each, because most matches are whitespace test fixtures, embedded ObjectScript whose indentation is deliberate, and aligned doc-comment tables. No conclusion is available from those numbers. Scoping it to the lines a branch ADDS (`git diff origin/master...origin/<br>`, `^+` only) makes the population precise. Across all fifteen open PR branches that gives four real leaks, all four here. Everything else the scan flags on those branches is intentional: measurement tables in doc comments, the deliberate `"ERROR #5002: "` truncation fixture, the real IRIS SQLCODE text in #368's fixtures (which genuinely contains double spaces), and the leaked string the #380 detector test asserts on. Verified at runtime after the fix by driving the built binary: the skip message contains no double space. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ether anything compiled (#371) * feat(iris_doc): run the named test after a compiled write, and say whether anything compiled Items 2 and 4 of issue 327, measured over 31,009 parsed `tool_use` blocks from 364 transcripts. `iris_doc(put)` is immediately followed by `iris_test` 579 times. A boolean would cover only the 346 where the class written IS the class tested; a STRING covers all 579, because in the other 233 the suite lives beside the class (`Hospital.BO.PatientDb` -> `Hospital.Tests.BO.PatientDbTest`). The decision is a pure `TestGate` with four states, not a boolean, because the ways a test does NOT run need different fixes from the caller: * `Run(pattern)` — the payload reports a compiled class. * `NotRequested` — nothing added to the payload at all. A `test_skipped` on every ordinary put would be noise on the 2,979 measured puts that want none of this. * `SkippedCallFailed` — the write did not succeed. Its own envelope is returned UNTOUCHED: it already carries the compile error, SCM elicitation or refusal the caller must act on, and rebuilding it to add a note would risk dropping the isError flag and hints that are the answer. * `SkippedNotCompiled { compile_requested }` — two different sentences, because "you did not ask for a compile" and "the compile produced nothing" are different states and need different fixes. Keyed off the payload's `compiled`, not off the mode, so it needs no list of modes to keep in step with `DocMode`. The write's verdict and the test's verdict stay separate (#310): `success` stays the WRITE's — the document landed — and the test arrives as `test` with `test_ok` beside it. A red test must not read as a failed write, and a failed write must not read as a red test. Sequenced in the `iris_doc` tool method rather than inside `doc::handle_iris_doc`, because running a suite is `iris_test`'s own 600-line job (polling, the result query, the per-case shaping of #233/#273). Calling the tool means the two paths cannot disagree about what a red test looks like. 89 of the measured `put` -> `iris_compile` pairs are a put that did not pass `compile`, followed by a compile of the SAME class. Issue 327 records that as a 3% tail and proposes the cheap option rather than flipping the default: have the put say so. The no-compile branch carried NO `compiled` key at all, so "was it compiled?" was answerable only by noticing an absence — and an absence reads as "does not apply here", not as "no". All three write outcomes now report `compiled` AND `compile_requested`, so the states are distinct from those two fields alone: (false,false) written and nobody asked, (false,true) the compile ran and failed, (true,true) compiled clean. fmt, clippy `-D warnings`, `cargo build --workspace`, and the gate with IRIS env unset. 15 new tests. 10 mutations applied one at a time — including "a blank test counts as a request", "a red test rewrites the write's own verdict", "iris_doc stops handing its result to the sequencer" and "the sequencer stops calling iris_test" — each red on its named tests, restored green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(iris_doc): four TestGate messages carried leaked indentation Ran this PR's test-after-put against a real instance for the first time. The behaviour is right in all five cases — a green suite gives test_ok: true; a RED suite gives test_ok: false while the write's own success stays true, which is the #310 separation working live; no compile gives test_skipped; a compile failure gives COMPILE_ERROR with the write envelope untouched and no test block; and no `test` parameter adds nothing at all. What was wrong is the text. The skip message came back as: not run: `test` needs a COMPILED class and this call did not compile. Pass compile=true Four messages, all in this PR: the three TestGate::skipped_reason arms and the unparseable-result note. Same collapsed line-continuation as the seven fixed in #376/#377 — I fixed those on the branches I happened to be working in and did not check the others. ## The scan that found the rest, and the one that did not Running the earlier detector over every branch was useless: master-descended branches report 15-40 hits each, because most matches are whitespace test fixtures, embedded ObjectScript whose indentation is deliberate, and aligned doc-comment tables. No conclusion is available from those numbers. Scoping it to the lines a branch ADDS (`git diff origin/master...origin/<br>`, `^+` only) makes the population precise. Across all fifteen open PR branches that gives four real leaks, all four here. Everything else the scan flags on those branches is intentional: measurement tables in doc comments, the deliberate `"ERROR #5002: "` truncation fixture, the real IRIS SQLCODE text in #368's fixtures (which genuinely contains double spaces), and the leaked string the #380 detector test asserts on. Verified at runtime after the fix by driving the built binary: the skip message contains no double space. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…entation (#380) The narrow, sound half of issue 378. Seven caller-facing messages were found carrying runs of leaked indentation — a Rust string continuation collapsed onto one source line keeps its whitespace — and all seven are fixed in #376/#377. This is the gate that stops the same thing recurring on the one surface where the population is complete. ## Why descriptions and not "messages" #378 records that there is no cheap sound gate over "every string that can reach a caller": * A SOURCE scan cannot distinguish a collapsed continuation from a correct one without exclusion rules for whitespace test fixtures, embedded ObjectScript whose indentation is deliberate, and aligned trailing comments. Measured: the naive version reported 592 hits across 68 files, every one a false positive, because it matched across lines and so read the source layout rather than the runtime string. * Message CONSTANTS are a sound population but a tiny one — three in this crate — and would have caught none of the seven, which were literals inside functions. * Messages built at call time are only reachable by triggering their paths. Descriptions are the one population that is complete, wire-facing and free: every one goes to every client on every tools/list, and advertised_tools() returns all of them with no IRIS connection. So this asserts what it can assert exhaustively and says plainly that it is not the whole problem. ## What it checks, and its controls All four toolsets, because a description can be advertised by one and not another. Two controls guard against a vacuous pass: the tool list must hold at least 20 entries, and at least 20 must actually carry a description — a zero from an empty list would otherwise read as clean. The detector has its own test, using the real text as it came back from the tool before the fix. That matters here specifically: the first version of this scan produced 592 false positives, and a version tuned the other way would have produced zero real ones with nothing to show the difference. ## Verification fmt, clippy -D warnings, cargo build --workspace, and the gate with IRIS env unset: 5 result lines, 0 FAILED, both tests confirmed by name, a fabricated name returning 0. Two mutations, each red and restored: * a collapsed continuation injected into iris_execute's real description — caught, naming the tool and reporting a 22-space run. * the tool list truncated to empty — caught by the control, "advertised only 0 tools, so a clean result would prove nothing", rather than passing as clean. Currently zero descriptions are affected, so this commit is green on the tree as it stands: it locks in a property rather than fixing one. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Issue 352's four-tool profile decision: one adopt, two declines with measured reasons, one premise
correction. Stacked on #375, which created the file the decisions are recorded in — base it on
master after that merges.
iris_coverage— already adopted; the premise is stale352 says all four are "absent from our 23".
iris_coverageis inINTEROP_TOOLSand has been sinceit was added after the issue was filed. Nothing to decide.
stream_inspect— ADOPT, precondition verified first352 makes adoption conditional on one thing: "confirmed non-consuming before adoption". Measured on
IRIS 2026.1 against a throwaway
%Stream.GlobalCharacter(probe class deleted afterwards):Read(100), then again"line-1", then""withAtEnd=1%OpenIdon the same id, after A hit AtEndSize=6,Read→"line-1".Sizefirst, thenRead(100)6, then"line-1""", stillAtEnd=1Read position is per-instance and never saved, and reading
Sizeadvances nothing. Preconditionmet.
It also corrects the issue's framing. 352 wants an answer to "is the stream empty, or already
read?" — but since position is never persisted, "already read" is not a state a later reader can
observe. A stream a relay consumed to the end still reads from the start for the next
%OpenId.The real question is "what is in this stream", and the confusion worth preventing is a different one:
an id that opens nothing versus a stream that holds nothing.
Adopting the capability, not the port
Upstream's
stream_inspect_implwas read atupstream/masterbefore writing this. Three deliberatedifferences, each one a rule this repo already has:
Do stream.Rewind()While stream.AtEnd=0 { Set content=content_stream.Read(4096) }, no capRead(max_chars),sizereported separately<MAXSTRING>→ SQLCODE ‑400 → ‑149. It would fail on exactly the payload you most want to seewrite_marker_lineswith a declared line countThree answers where upstream has two
{"success": true, "size": 0, "empty": true, "body": ""} // opened, holds nothing {"error_code": "STREAM_NOT_FOUND", "error": "… This is NOT an empty stream. …"} // id opens nothing {"error_code": "STREAM_UNREADABLE", "error": "… not an empty stream, an unusable reply …"}Plus
truncated+max_charswhen the body is a prefix,body_incomplete+ both line counts whenless arrived than IRIS declared, and for a binary stream the size with the bytes omitted — bytes
cannot cross a text protocol intact, and a mangled body is worse than none.
iris_generate_test— DECLINE, on the precondition it cannot meet352 requires "output confirmed to match
tdd's prescribed shape". Read atupstream/master:It asks for
%UnitTest.TestCase, wheretddrequires%UnitTest.TestProductionwithParameter PRODUCTIONandTestControlneutralised. Nothing about asserting on a field thecomponent writes. And the only validation of what comes back is:
So the shape is a request to an LLM, not a guarantee — the acceptance criterion has no answer for a
non-deterministic generator, and adopting it would have this fork advertise a tool its own gates
fight.
mermaid_production— DECLINE, on measurementIts implementation reads only
Ens_Config.Itemand emits every item as a child of the productionnode — a one-level fan-out, no edge between items. Measured against a real 10-item production:
TargetConfigName(s)This fork already reads both sources — items via
iris_production_item(list), targets viaget_settings, which #372 made readable for many items in one round trip — andiris_business_rule_infoalready parses the rule. So it can answer a question the port cannot;importing it would ship the weaker one.
A defect this found in #375's own gate
The upstream-surface scan read
oursfromHEADandINTEROP_TOOLSfrom the worktree. Whilethis change was uncommitted the two halves disagreed:
stream_inspectappeared as advertised andnever-ported, and the #353 join then reported it
UNCLASSIFIED. Measured: exit 1 before thecommit, exit 0 after, with no source change in between. Both halves now read
HEADand the sectionprints that caveat;
both_halves_of_the_surface_comparison_read_the_same_refguards it.Re-recorded counts, measured row by row
Three pinned gates caught the new tool, which is what they are for:
the_read_only_split_is_pinned_per_toolset(all four toolsets +1) and two wire-level pins inmcp_handshake. The read-only figures are unchanged, and that is the measured point rather than anoversight:
stream_inspectis inGENERATOR_WRITE_TOOLSbecause it reads a stream through ascratch class, so it is honestly not advertised
readOnlyHint:trueeven though it writes nothing tothe stream. Each row was recorded by running the gate and reading what it said, not by arithmetic.
Manifest row is
unit-ok, notOK: the unit tests are named and green, and there is no e2e row yet.Verification
cargo fmt --all -- --checkclean,cargo clippy --workspace --all-targets -- -D warningsclean,cargo build --workspace,bash scripts/validate-tools.shexit 0.name; a fabricated name returns 0.
is not the capped
Read; the body goes out on one line; size read after the body; an id that opensnothing reports an empty stream; a no-marker reply reads as a found empty stream; a prefix not
flagged
truncated; a binary stream's bytes reported as text; a short body not flagged incomplete;the profile read from the worktree again.
Three of those needed re-targeting before they produced a verdict — two anchors broke the
format!string and one left an unused named argument. All three were compile errors, not survivors, and
none was counted until it compiled and went red.
Not verified against a live instance
The generated program is unit-tested, as every
build_*_codein this repo is, but it has not beenrun:
stream_inspectreaches IRIS throughexecute_via_generator, which PUTs a scratch class, andthe only instance I can write to has no reason to hold an interop stream. The measurement at the top
of this PR was taken by driving the same ObjectScript by hand over Atelier, which is why the
non-consuming claim is evidence rather than inference — but the tool's own path first executes in
CI's e2e job.