Skip to content

fix: conformance gate rejected correct documentation (stream-claim detection) - #84

Merged
chaoz23 merged 1 commit into
mainfrom
fix/conformance-stream-claim
Aug 19, 2026
Merged

chaoz23 merged 1 commit into
mainfrom
fix/conformance-stream-claim

Conversation

@chaoz23

@chaoz23 chaoz23 commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Follow-up to #82, found immediately by trying to use it.

The bug

The gate decided which stream a SKILL.md claims for its failure payload by asking whether the word stderr appeared in the paragraph:

stream_claim = ("stderr" if "stderr" in low else "stdout", para)

That works only while the documentation is wrong.

Fixing chaoz23/dmcheck#13 means writing a paragraph that names both streams, because that is what is actually true:

Every envelope — clean, findings, and the honest lane — prints JSON on stdout, never a traceback. stderr carries only argparse usage errors, which also exit 2 but emit no JSON.

The old test saw stderr, concluded stderr was the claim, and reported STREAM_MISMATCH against a file that had just become accurate. The gate rejected exactly the fix it exists to prompt.

A gate that blocks the correct change is worse than one that misses a defect — it teaches you to bypass it.

The fix

The claim is the stream governed by the printing verb, not any stream mentioned:

re.search(r"(?:prints?|emits?|writes?|outputs?)\b[^.]{0,60}?\b(stdout|stderr)\b", low)

Falls back to the old heuristic when no verb is found, so nothing that parsed before stops parsing.

Verified both directions against real files

input resolves to STREAM_MISMATCH
dmcheck SKILL.md at origin/main stderr reported ✅
dmcheck SKILL.md with chaoz23/dmcheck#13 applied stdout clears ✅

MISSING_PIPE and HONEST_LANE_OVERLOAD still report on both, so this narrows nothing else.

This is the second time this gate's parser was the defect rather than the tools it checks — the first was matching line-by-line against a claim that wrapped across two lines (#82). Both had the same shape: the checker silently could not perceive the thing it was checking. Worth remembering when adding rules to it.

🤖 Generated with Claude Code

The gate decided which stream a SKILL.md claims by asking whether the word
"stderr" appeared anywhere in the paragraph. That works only while the
documentation is wrong.

A correct doc names both streams -- "every envelope prints JSON on stdout;
stderr carries only argparse usage errors" -- and the old test saw "stderr",
concluded stderr was the claim, and reported STREAM_MISMATCH against a file
that was now accurate. It rejected exactly the fix it exists to prompt.

A gate that blocks the correct change is worse than one that misses a defect:
it teaches you to bypass it.

The claim is the stream governed by the printing verb, not any stream
mentioned. Detection now matches (prints|emits|writes|outputs) ... (stdout|
stderr) within a clause, falling back to the old heuristic when no verb is
found.

Verified both directions against real files: dmcheck's SKILL.md at
origin/main still resolves to "stderr" and still reports STREAM_MISMATCH,
while the corrected text resolves to "stdout" and clears -- with MISSING_PIPE
and HONEST_LANE_OVERLOAD still reported, so the fix narrows nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chaoz23
chaoz23 merged commit 0ee7b71 into main Aug 19, 2026
14 checks passed
@chaoz23
chaoz23 deleted the fix/conformance-stream-claim branch August 19, 2026 20:42
chaoz23 added a commit that referenced this pull request Aug 20, 2026
v2 introduced member classes but stated them only in FAMILY.md's members
table. The gate had no way to read them, so the moment v2 merged it began
reporting MISSING_PIPE against table-kit -- a transport member that v2
explicitly exempts. The gate contradicted the contract it enforces.

That is the same failure as #84 arriving from the other side: a gate that
flags a correct state teaches you to bypass it.

Members now declare their own class in tool.json:

    "family_class": "verdict" | "transport" | "adjacent"

The declaration lives in the artifact rather than in FAMILY.md's prose. The
alternative -- parsing the members table out of the contract -- would couple
the gate to markdown formatting, and this parser has already been the defect
twice for exactly that kind of brittleness.

Gate changes:

- CLASS_SURFACES gates --pipe (and MCP, still unprobed) to the verdict class
- CLAUSE_1_CLASSES skips honest-lane and stream checks for the adjacent class,
  which clause 1 does not bind
- an undeclared class emits UNDECLARED_CLASS and names the surfaces it did not
  check, rather than guessing: assuming "verdict" invents breaches for a
  transport, assuming "transport" hides real ones for a verdict tool
- family_class is reported in output

tool.json is generated here, so the field is added in gen_tool_json.py and the
card regenerated; tests/test_metadata_fresh.py passes.

Verified in all three states: srdcheck (verdict) PASS; table-kit undeclared ->
UNDECLARED_CLASS + SCHEMA_NOT_FLAG with --pipe correctly unevaluated;
table-kit declared transport -> MISSING_PIPE gone, SCHEMA_NOT_FLAG and
HONEST_LANE_OVERLOAD correctly retained.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
chaoz23 added a commit that referenced this pull request Aug 25, 2026
* feat: FAMILY.md v2.2 harmonises exit 3; gate can see codes above 3

Two halves of one problem, surfaced by chaoz23/charactercheck#18.

CONTRACT. v2.1 said "srdcheck's exit 3 is the family precedent", which was too
weak to settle what a member should do when 3 is already spent. v2.2 states
the pattern outright: 0/1/2 are the universal verdict contract, >= 3 is the
no-verdict taxonomy, and 3 is usage error in EVERY member. A malformed call is
the one non-verdict outcome every tool has, so harmonising it means an agent
that mis-invokes any member gets the same answer. Further outcomes take 4+.

charactercheck therefore moves could-not-retrieve from 3 to 4 rather than
taking a different usage code. Decided deliberately over the cheaper option of
letting it keep 3 and putting usage on 4: that would have left the family with
two spellings for the concept an agent hits most often.

GATE. parse_skill capped recorded codes at 0..3, so a documented exit 4 was
silently dropped -- and the CLI returning 4 then surfaced as a bogus
UNDOCUMENTED_EXIT_CODE against a tool that had documented itself correctly.
The gate would have rejected the very refactor the contract now requires.

That is the third time this parser has been the defect rather than the tools
it checks (line-vs-paragraph in #82, stream-word matching in #84). The shape
repeats: the checker silently cannot perceive the thing it is checking.

Verified both directions: a synthetic 0/1/2/3/4 SKILL.md now parses all five
with honest-lane=2 and usage=4, and all four real SKILL.md files parse
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: drop charactercheck's HONEST_LANE_OVERLOAD waiver

charactercheck is harmonising onto exit 3 for usage errors
(chaoz23/charactercheck#18). The waiver comes out before the fix lands, as the
ratchet requires; the SHA pin keeps charactercheck's CI green in the meantime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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