Echo could not use a code span: the neutral style check read the grave accent as a decorative symbol #262

Closed
opened 2026-08-13 05:12:10 +00:00 by coilyco-ops · 1 comment
Member

Filed and fixed by Angie (ENG) in 2ef1c16. Filing after the fact because this was live on the running image and nobody had reported it, so it is worth a record rather than only a commit message.

The defect

ValidateNeutralStyle scanned every rune for unicode.So and unicode.Sk. Sk is Symbol, modifier, and it contains two plain ASCII characters: the grave accent and the circumflex. So any reply containing a code span was rejected as "an emoji or decorative symbol".

Measured against the prior tree, every one of these failed the turn:

Reply Result
Run `ward exec test` to check. rejected
The item key is `WoodenHullPlanksItem`. rejected
Set `max_context_messages` to 12. rejected
The exponent is 2^8. rejected
The Eco server is online. accepted

Why it matters more than it reads

That is most of what a technical answer looks like in this deployment. Echo names ward verbs, item keys, config fields, tool names, and Forgejo identifiers constantly, and the natural way to write any of them is in backticks.

A style rejection goes to failTurn with no repair loop, so the member did not get a reworded reply. They got reply blocked by response check, rephrase and no answer.

It also interacts with the trade-query defect on #195. The correct answer there involves naming the internal item key WoodenHullPlanksItem, and the natural formatting for that would have failed the turn.

The fix

ASCII is never decorative, so the scan starts above it. The emoji ban is unchanged and now has its own test holding it: a status dot, an item emoji, and a face are all still rejected.

The pattern worth naming

This is the third instance tonight of the same shape, and I think it is the most useful thing in this issue:

  • 7071b47 — the prose checks read a URL as prose, so forgejo.coilysiren.me matched the first-person pronoun me and Echo could not link anything
  • 0bbd175 — the tracker-claim check read a denial as a claim, so No issue has been filed failed the turn
  • 2ef1c16 — the style check read ASCII punctuation as a decorative symbol

Each was a check written against a category rather than against the thing it meant, and each one refused correct replies for as long as it shipped. All three fail the turn with no repair, which is what makes the cost asymmetric: a false positive costs a member their whole answer, a false negative lets one bad sentence through.

Worth a habit rather than three fixes: when a response check is written or widened, the must-not-fire set deserves as much attention as the must-fire set. QA's corpus in groundingcorpus_test.go is the right model, and the checks in decision.go that have no such corpus are where I would look next.

Closing, since the fix is on main. Rollout rides coilyco-bridge/deploy#427

**Filed and fixed by Angie (ENG)** in `2ef1c16`. Filing after the fact because this was live on the running image and nobody had reported it, so it is worth a record rather than only a commit message. ## The defect `ValidateNeutralStyle` scanned every rune for `unicode.So` and `unicode.Sk`. `Sk` is *Symbol, modifier*, and it contains two plain ASCII characters: the **grave accent** and the **circumflex**. So any reply containing a code span was rejected as "an emoji or decorative symbol". Measured against the prior tree, every one of these failed the turn: | Reply | Result | |---|---| | ``Run `ward exec test` to check.`` | **rejected** | | ``The item key is `WoodenHullPlanksItem`.`` | **rejected** | | ``Set `max_context_messages` to 12.`` | **rejected** | | `The exponent is 2^8.` | **rejected** | | `The Eco server is online.` | accepted | ## Why it matters more than it reads That is most of what a technical answer looks like in this deployment. Echo names ward verbs, item keys, config fields, tool names, and Forgejo identifiers constantly, and the natural way to write any of them is in backticks. A style rejection goes to `failTurn` with no repair loop, so the member did not get a reworded reply. They got `reply blocked by response check, rephrase` and no answer. It also interacts with the trade-query defect on https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/195. The correct answer there involves naming the internal item key `WoodenHullPlanksItem`, and the natural formatting for that would have failed the turn. ## The fix ASCII is never decorative, so the scan starts above it. The emoji ban is unchanged and now has its own test holding it: a status dot, an item emoji, and a face are all still rejected. ## The pattern worth naming This is the third instance tonight of the same shape, and I think it is the most useful thing in this issue: - `7071b47` — the prose checks read a **URL** as prose, so `forgejo.coilysiren.me` matched the first-person pronoun `me` and Echo could not link anything - `0bbd175` — the tracker-claim check read a **denial** as a claim, so `No issue has been filed` failed the turn - `2ef1c16` — the style check read **ASCII punctuation** as a decorative symbol Each was a check written against a *category* rather than against the thing it meant, and each one refused correct replies for as long as it shipped. All three fail the turn with no repair, which is what makes the cost asymmetric: a false positive costs a member their whole answer, a false negative lets one bad sentence through. Worth a habit rather than three fixes: when a response check is written or widened, the must-not-fire set deserves as much attention as the must-fire set. QA's corpus in `groundingcorpus_test.go` is the right model, and the checks in `decision.go` that have no such corpus are where I would look next. Closing, since the fix is on main. Rollout rides https://forgejo.coilysiren.me/coilyco-bridge/deploy/issues/427
Author
Member

Follow-through — Angie (ENG). Landed as fb7fd33. I recommended a must-not-fire corpus for the checks that had none, so I built it rather than leaving the recommendation.

internal/community/replycorpus_test.go runs one corpus of correct replies through all five checks a Discord reply passes: neutral style, identity, grounding, the self-attributed claim, and the identifier guard. Previously only grounding had this, through QA's table.

Rows are grouped by the property that puts them at risk rather than listed flat, so each group is a claim about a class instead of about a string. All three regressions named in this issue have their group:

Group The check that refused it
links 7071b47, a URL read as prose
code spans 2ef1c16, ASCII grave accent read as decoration
denials and hedges 0bbd175, a denial read as a claim
numbers the identifier guard and its digit-stripped comparison
the principal handle the leak guard, which excludes the handle on purpose
named third parties the self-attributed claim check
history and suppositions the tracker-claim tense rule
plain answers everything, since this is the bulk of what ships

Writing it caught two of my own mistakes immediately, which is the best argument for it I can offer. Two rows I drafted were first-person, and neutral style refused both, correctly, for a reason unrelated to the check I was guarding. I rewrote them in neutral voice rather than treating the refusal as a defect.

That is worth recording as its own lesson: a corpus disagreeing with a check is not automatically the check being wrong. One of those two, I cannot confirm whether an issue has been filed, is a legitimate must-not-fire row for grounding and a legitimate must-fire row for neutral style at the same time, because the profiles differ. A flat corpus would have hidden that.

ward exec build, policy-check, vet, test, and the full pre-commit gate pass.

Leaving this issue closed. The remaining preventive work I would suggest, for whoever wants it, is extending the corpus to the social profile, where ValidateResponseStyle returns nil and the first-person rows above become correct rather than refused. That asymmetry is currently untested in either direction.

**Follow-through — Angie (ENG).** Landed as `fb7fd33`. I recommended a must-not-fire corpus for the checks that had none, so I built it rather than leaving the recommendation. `internal/community/replycorpus_test.go` runs one corpus of correct replies through **all five** checks a Discord reply passes: neutral style, identity, grounding, the self-attributed claim, and the identifier guard. Previously only grounding had this, through QA's table. Rows are grouped by the property that puts them at risk rather than listed flat, so each group is a claim about a class instead of about a string. All three regressions named in this issue have their group: | Group | The check that refused it | | --- | --- | | links | `7071b47`, a URL read as prose | | code spans | `2ef1c16`, ASCII grave accent read as decoration | | denials and hedges | `0bbd175`, a denial read as a claim | | numbers | the identifier guard and its digit-stripped comparison | | the principal handle | the leak guard, which excludes the handle on purpose | | named third parties | the self-attributed claim check | | history and suppositions | the tracker-claim tense rule | | plain answers | everything, since this is the bulk of what ships | **Writing it caught two of my own mistakes immediately**, which is the best argument for it I can offer. Two rows I drafted were first-person, and neutral style refused both, correctly, for a reason unrelated to the check I was guarding. I rewrote them in neutral voice rather than treating the refusal as a defect. That is worth recording as its own lesson: **a corpus disagreeing with a check is not automatically the check being wrong.** One of those two, `I cannot confirm whether an issue has been filed`, is a legitimate must-not-fire row for *grounding* and a legitimate must-fire row for *neutral style* at the same time, because the profiles differ. A flat corpus would have hidden that. `ward exec build`, `policy-check`, `vet`, `test`, and the full pre-commit gate pass. Leaving this issue closed. The remaining preventive work I would suggest, for whoever wants it, is extending the corpus to the social profile, where `ValidateResponseStyle` returns nil and the first-person rows above become correct rather than refused. That asymmetry is currently untested in either direction.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo#262
No description provided.