Contributing and reference3 of 12
House rules
On this page
- Everything is written in English
- A comment says WHY, and stops
- A fix arrives with a test that was seen failing
- Nothing in the repository names a person or a machine
- No secrets in a commit
- No emoji in the interface
- Registration is explicit; an import is not a registration
- A tool declares its voice patterns
- The fourteen emotions live in more than one file
- No Live2D model is ever committed
- Formatting
These are the conventions the repository actually holds itself to. Some are enforced by a test; those say which one. The rest are enforced at review, and this page says so rather than pretending otherwise.
Everything is written in English
Code, comments, commit messages, test names, docstrings. The product speaks whatever language its user speaks; the repository speaks one.
A comment says WHY, and stops
The code already says what. A comment earns its place by explaining a decision the code cannot: the trap that was hit, the alternative that was rejected, the measurement that settled it.
- Ten lines is the ceiling for any one comment or docstring.
- A long docstring is the same violation wearing a different hat. Moving a wall of
#into"""does not shorten it. - A comment that restates the line under it gets deleted.
Not enforced by a test. There is no comment-length check in either suite; the pull request template carries the checkbox and a reviewer reads the diff.
A fix arrives with a test that was seen failing
The pull request template puts it plainly: a test that fails without the change and passes with it is the strongest form of this; "it should work" is the weakest.
So write the test first, run it, and watch it go red. A green test written after a green fix proves that the two agree, not that either is right. This repository has been bitten by that shape more than once — guards that had never fired, and therefore measured nothing.
Two habits that follow from it:
- Break what a guard guards, and confirm the guard goes red. If you cannot make it fail, it is not an instrument.
- Run the command and read the output. "Should work" is not an acceptable claim in a pull request description; the template asks for the command you ran and what it said.
Nothing in the repository names a person or a machine
This one is enforced, by api/tests/test_the_repository_belongs_to_nobody.py — seven tests over
the whole shipping tree: api/src, api/tests, lib, components, app, scripts, docs, soul,
assets, tests, public/worklets, plus every top-level config and documentation file by name.
It refuses:
| Shape | Example of what fails |
|---|---|
| A real home directory | /home/<a real name>, /Users/<a real name>, C:\Users\<a real name> |
| A personal directory as a tilde path | ~/<a folder only one machine has> |
| A mailbox outside the reserved documentation domain | anything not @example.com |
| A decision credited to a named individual | Someone chose …, Someone's call |
| A real Discord account, server or role id | a snowflake whose timestamp decodes to a real moment |
| The author's name outside the four places it belongs | anywhere but pyproject.toml, LICENSE, package.json, README.md |
| The name of the tooling that wrote the code | any occurrence, anywhere |
Two details worth copying if you ever write a guard like it:
- It is an allowlist of what is generic, not a blocklist of names to avoid. A blocklist would have to spell out the name it removes, and would go stale the moment anybody else worked on the tree.
- The credit check is structural: a capitalised word followed by a verb of authorship. It catches a name without ever writing one down.
The rule underneath is the fact survives, the person does not. A measurement stays. The person who asked for it goes, because a reader who cannot ask that person is left with an argument from authority they have no way to check.
Placeholders exist for when a literal path is unavoidable — a path-elision test needs a real absolute
path to shorten. Use one already in PLACEHOLDER_HOMES; adding to that list is a deliberate act, and
adding a real home directory to it cannot be.
No secrets in a commit
Never a key, a token, or a .env with real values. The templates to copy are api/.env.example and
.env.local.example. .gitignore already refuses .env, .env.* and *.env, with the two example
files re-admitted by name.
No emoji in the interface
Enforced, by tests/no-emoji-in-the-interface.test.mjs. It reads every .ts/.tsx under
components/, app/ and lib/ and fails on any pictographic codepoint. The reason is practical: an
emoji is the one glyph the design cannot style, size or recolour, and it renders as somebody else's
artwork on every platform. The interface's own marks — ✓ ✕ ⚠ ✦ ♥ ★ — are typography and stay.
Registration is explicit; an import is not a registration
A tool module is offered to the model because it is named in a list, not because it exists:
tools/registry.py's discover() for a built-in, tools/action/__init__.py's ACTION_TOOLS for an
action tool. A module left off the list is silently offered to nobody — nothing raises, she simply
cannot do the thing.
Enforced, by api/tests/test_no_tool_module_is_left_unregistered.py, which asks the file system
rather than the import graph: every .py in either package must reach the live registry.
A tool declares its voice patterns
ANNOUNCE, HEARTBEAT, COMPLETE, FAIL, beside SCHEMA and execute. She narrates before a tool
runs, again while it is still going, and confirms when it lands. A tool without them goes quiet
mid-turn.
There is exactly one module in the tree with no ANNOUNCE: tools/builtin/web_search.py, and its
silence is deliberate — the provider runs that search server-side, so the narration branch it would use
is never reached. Counted at the time of writing; it is the only one.
The fourteen emotions live in more than one file
Measured at the time of writing: api/src/kotoba/core/emotions.py (VALID_EMOTIONS),
api/src/kotoba/soul/prompt.py (EMOTIONS) and lib/expressions.ts (EMOTIONS) all hold the same
fourteen names.
api/tests/test_the_three_emotion_lists_agree.py compares all three, in both directions, so a name
added to one and not the others goes red. If you add an emotion, change all three — and then the
terminal's kaomoji table, both trees of face sprites, and the fallback list in lib/avatar-config.ts,
which that test does not cover. Which expression a model plays for an emotion is a separate,
per-model question.
No Live2D model is ever committed
public/models/ is gitignored and must stay that way. The good models — including Live2D's own free
samples — forbid redistribution, so a model pushed here would republish somebody else's work under this
repository's licence. Models live on the user's machine, in ~/.kotoba/models/.
scripts/build_web.py refuses to build at all if it finds a .moc3 or a .model3.json anywhere under
public/, symlinked directories included.
Formatting
.editorconfig decides it, so editors do not fight the diff: UTF-8, LF, a trailing newline, trimmed
trailing whitespace, 110 columns, 2-space indent everywhere except Python, which is 4.
