feat(style): admit object emoji in a neutral reply, bounded at three #840

Merged
coilysiren merged 1 commit from aos/claude/bk79-object-emoji into main 2026-08-15 21:30:14 +00:00
Member

Closes #203. Both steps of the recorded build order, in the order Lucia argued for: check first, doctrine second.

The check

ValidateNeutralStyle refused every emoji, so the legibility this issue asks for could not ship at all. It now refuses tone and admits an emoji that names a thing.

  • Refused - faces, people, body, hearts, gestures; the celebration marks; status dots, geometric shapes, verdict marks, arrows.
  • Admitted - everything else, at most maxObjectEmoji (3) per reply, joined glyphs counting once.

A style refusal is inside the repair loop at proxy.go:516, so a fourth emoji costs one repair attempt rather than the turn outright. Worth stating because the thread twice described style rejection as going straight to failTurn with no repair - that is no longer true, and it is what makes enforcing the bound reasonable.

Three judgement calls to review

The recorded decision has a contradiction, and I resolved it toward Kai's words. It says the emotive-versus-object split "needs no separate status-indicator carve-out", and also that The Eco server is online 🟢 is out. Both cannot hold: 🟢 is U+1F7E2, a geometric shape, not an emotive one, so "refuse emotive, admit the rest" would have admitted the exact example ruled out. Indicators are their own refused set.

A party popper is an object to Unicode and tone to a reader. 🎉 sits in Misc Symbols and Pictographs beside genuine objects, and my first implementation admitted it. "NOT for whimsy" is the issue's own emphasis, so the celebration range is refused even though nothing about it is a face.

Denylist, not allowlist. Angie asked for a principled split rather than a list to maintain, and the failure asymmetry agrees: an allowlist refuses any object nobody thought of, costing a repair and then the turn, while a denylist admits an unlisted emoji that merely reads slightly off. The cheap mistake is the one it makes.

The doctrine

references/object-emoji.md in sirens-echo-knowledge, carrying the rules the check cannot judge: the emoji follows the object rather than replacing it, first mention only, objects only, and no obvious emoji means none.

A per-item table was drafted and cut. The full version cost 2142 bytes of Echo's prompt on every turn forever. The model already knows wood is 🪵; the rules are the part it cannot infer. Trimmed to one example line, the cost is 1535 bytes, recorded in docs/sirens-echo-prompt-budget.md with that reasoning.

Tests

objectemoji_test.go pins each decision: objects admitted, indicators refused, tone refused, three admitted and four refused, a joined glyph counted once, and a code span still surviving the scan (the 2ef1c16 regression). One more asserts the refused range tables stay sorted, because unicode.Is switches to binary search past eighteen entries and an unsorted table would start missing runes silently.

The shipped assertion that The Eco server is online 🟢. is refused is kept with its rationale changed rather than deleted, as the decision asked. Wood 🪵 ... moved out of that list into the admitting test.

Verification

ward gate PASS on all six steps. I also drove ValidateResponseStyle directly across all seven cases to confirm the social profile is untouched and admits everything.

Conflict note

This touches internal/community/tuning.go (adding maxObjectEmoji) and internal/community/prompt_test.go, and so does #835. Different constants and different lines, but they may need a merge.

Closes #203. Both steps of the recorded build order, in the order Lucia argued for: check first, doctrine second. ## The check `ValidateNeutralStyle` refused every emoji, so the legibility this issue asks for could not ship at all. It now refuses tone and admits an emoji that names a thing. * **Refused** - faces, people, body, hearts, gestures; the celebration marks; status dots, geometric shapes, verdict marks, arrows. * **Admitted** - everything else, at most `maxObjectEmoji` (3) per reply, joined glyphs counting once. A style refusal is inside the repair loop at `proxy.go:516`, so a fourth emoji costs one repair attempt rather than the turn outright. Worth stating because the thread twice described style rejection as going straight to `failTurn` with no repair - that is no longer true, and it is what makes enforcing the bound reasonable. ## Three judgement calls to review **The recorded decision has a contradiction, and I resolved it toward Kai's words.** It says the emotive-versus-object split "needs no separate status-indicator carve-out", and also that `The Eco server is online 🟢` is out. Both cannot hold: `🟢` is U+1F7E2, a geometric shape, not an emotive one, so "refuse emotive, admit the rest" would have admitted the exact example ruled out. Indicators are their own refused set. **A party popper is an object to Unicode and tone to a reader.** `🎉` sits in Misc Symbols and Pictographs beside genuine objects, and my first implementation admitted it. "NOT for whimsy" is the issue's own emphasis, so the celebration range is refused even though nothing about it is a face. **Denylist, not allowlist.** Angie asked for a principled split rather than a list to maintain, and the failure asymmetry agrees: an allowlist refuses any object nobody thought of, costing a repair and then the turn, while a denylist admits an unlisted emoji that merely reads slightly off. The cheap mistake is the one it makes. ## The doctrine `references/object-emoji.md` in `sirens-echo-knowledge`, carrying the rules the check cannot judge: the emoji follows the object rather than replacing it, first mention only, objects only, and no obvious emoji means none. **A per-item table was drafted and cut.** The full version cost 2142 bytes of Echo's prompt on every turn forever. The model already knows wood is 🪵; the rules are the part it cannot infer. Trimmed to one example line, the cost is 1535 bytes, recorded in `docs/sirens-echo-prompt-budget.md` with that reasoning. ## Tests `objectemoji_test.go` pins each decision: objects admitted, indicators refused, tone refused, three admitted and four refused, a joined glyph counted once, and a code span still surviving the scan (the `2ef1c16` regression). One more asserts the refused range tables stay sorted, because `unicode.Is` switches to binary search past eighteen entries and an unsorted table would start missing runes silently. The shipped assertion that `The Eco server is online 🟢.` is refused is **kept with its rationale changed** rather than deleted, as the decision asked. `Wood 🪵 ...` moved out of that list into the admitting test. ## Verification `ward gate` PASS on all six steps. I also drove `ValidateResponseStyle` directly across all seven cases to confirm the social profile is untouched and admits everything. ## Conflict note This touches `internal/community/tuning.go` (adding `maxObjectEmoji`) and `internal/community/prompt_test.go`, and so does #835. Different constants and different lines, but they may need a merge.
feat(style): admit object emoji in a neutral reply, bounded at three
All checks were successful
ci / image-build (pull_request) Successful in 26s
ci / test (pull_request) Successful in 42s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
c933dafe2a
The neutral check refused every emoji, which meant the legibility this
issue asks for could not ship. It now refuses tone and admits an emoji
that names a thing.

Refused: faces, people, body, hearts, and the celebration marks; status
dots, geometric shapes, verdict marks, and arrows. Admitted: everything
else, at most maxObjectEmoji of them.

Two judgement calls worth reviewing.

A party popper is an object to Unicode and tone to a reader. "NOT for
whimsy" is the issue's own emphasis, so the celebration range is refused
even though nothing about it is a face.

The rule is a denylist rather than an allowlist of object ranges. An
allowlist refuses any object nobody thought of, and a style refusal costs
a repair attempt and then the turn. A denylist fails the cheaper way.

The recorded decision said the emotive split "needs no separate
status-indicator carve-out". It does: a green circle is a geometric
shape, not an emotive one, so admitting the non-emotive rest would have
admitted the exact example Kai ruled out. Indicators are their own
refused set.

The shipped assertion that a status dot is refused is kept with its
rationale changed rather than deleted, as the decision asked.

Echo's prompt grows 1535 bytes for the doctrine, recorded in the budget
ledger. A per-item emoji table was drafted and cut to one example line:
the model already knows wood is a log, and the rules are the part it
cannot infer.

`ward gate` PASS on all six steps.

Closes #203

Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
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!840
No description provided.