Code Conventions
[[[appendices.code-conventions]]]
Overview
[[[appendices.code-conventions#overview]]]
Conventions for writing code and docs in this repo, for any contributor — not just Claude Code sessions.
CLAUDE.md at the repo root covers how to work with the AI assistant; this page covers how the codebase
itself is written.
1. Python
[[[appendices.code-conventions#python]]]
- Imports: relative within a package's library code (
from .field import Field); absolute only in tests and the app's entry file (from rehuco_agent.fields.field import Field). Relative keeps intra-package imports stable when a package is published or moved between depths. The codebase is mid-migration to this rule; therehuco-coresweep is a follow-up. - Module filenames: match their main class (
rehu_document.py→RehuDocument,field_registry.py→FieldRegistry), unless the file deliberately groups several related classes (properties.py→TypedProperty+SimpleProperty,fields/field.py→Field+FieldBinding+FieldModel). - Visibility: public or private (
__); no protected (_) unless the class is designed for inheritance. This holds at module level too — no_-prefixed module globals or classes. A helper used by only one class belongs inside that class (a__-private member, or a public classmethod if it's API — even the sentinel-in-signature case works, verified against 3.14's lazy annotations); anything that must stay at module level is a plain public name, kept out of the package API by what the package__init__exports (name mangling doesn't exist outside a class body, so a module-level_would be convention-only anyway). - Constants:
Finalwithout an explicit type when it can be inferred. - Naming: don't bake development-methodology/process labels into identifiers (e.g. a bare
TRACER_prefix naming a constant after "this was built during the tracer-bullet slice") — that context rots once the slice ends; put it in a docstring, not the name. - Overrides:
@overrideon every method that overrides a base-class method. - Docstrings: Sphinx-style on all functions, including private ones — one-line summary +
:param:/:returns:/:raises:as needed; no multi-paragraph docstrings for routine code. Closing"""on its own line for multi-line docstrings, on the same line for single-line ones. Constructor:param:entries go on the class docstring (IDE hover shows them);__init__gets no docstring. - Comments: only when the why is non-obvious (hidden constraint, subtle invariant, bug workaround). No narration of what the code does.
- Line length: 120 (ruff enforced).
- Size caps: a source module past a pylint cap splits along a cohesive seam (
RootsChecksumVerbs,RootsRowActions), never by inlining or a pragma; a pragma is right only for a module that is one cohesive thing (CatalogCache), and says why.too-many-localsis cured by a helper. A test module past 1000 lines is split, importing helpers from its sibling (#457, #458, #469, #395).
2. PySide6 UI
[[[appendices.code-conventions#pyside-ui]]]
- Window/widget layouts live in
.uifiles, not built up in Python. For a custom widget, prefer a.uitoo, unless it's trivial (simple layout, or the class only overrides/adds behavior on an existing widget rather than composing a new layout). - Collocate a
.uiwith its controller class:foo_widget.ui,foo_widget.pynext to each other; the generatedfoo_widget_ui.pyis gitignored and rebuilt viamake uis— never hand-edit it. - A custom widget used inside a
.uiis placed as a base/QWidgetplaceholder and promoted to the real class, not embedded as literal custom XML. - Assets go through
.qrc. Icons are authored in an Affinity Designer master file and exported as.svg;make qrcsregenerates the gitignored_rc.py. A.uireferencing a qrc-managed resource (e.g.windowIcon) must declare<resources><include location="....qrc"/></resources>somake uisemits the matching resource import — verify the generated file after adding one. - Ask before adding a new asset-conversion pipeline (e.g. deriving
.icofrom the.svgmaster) rather than improvising one inline. - Every property/attribute in a
.uifile must be one Qt Designer's own Property Editor can display and edit. Don't hand-add XML that's schema-valid and works at runtime but isn't something Designer surfaces — a developer opening the file in Designer needs to be able to find and change it there. Anything Designer can't set belongs in the controller class's Python code aftersetupUi(), not in the.ui. - Name widgets in
.uifilessnake_case, not Qt Designer's defaultcamelCase— this is a recommendation, not a hard rule; usecamelCasewhere justified (e.g. overriding a name Qt itself expects). A widget name that's already a single dictionary word a spell checker recognizes (checkbox,toolbar) doesn't need splitting; a compound default that isn't a real single word (centralwidget,statusbar,menubar) becomescentral_widget,status_bar,menu_bar.
Command surfaces
[[[appendices.code-conventions#command-surfaces]]]
A command can be reached from several places; each has one job.
- The menu bar is the complete index. Every command is in a menu, whatever else shows it, so a command is findable without knowing which widget owns it.
- A button goes at the narrowest widget containing everything it affects. Scan affects every browser of a
catalog, so it is on the catalog shell's toolbar; Add Root and Remove Root affect only the Roots view, so
they are on its sub-dock's title bar (
CDockWidget.setTitleBarActions). A command on one item — a root, a row — goes on that item's context menu. - A sub-dock earns a toolbar only past about three frequent actions. Up to that, its title-bar actions are enough.
- One
QActionserves every surface, so enabled state, text and shortcut are decided once, by the widget that owns it.
Worker threads
[[[appendices.code-conventions#worker-threads]]]
Two kinds of background work, two kinds of thread. The reasoning, and the measurements behind it, are in the module
docstring of borco_core.path_presence — read it there, where the code is.
- Bounded work whose answer goes back to a widget runs on a
QThreadPool(QThreadPool.globalInstance(), aQRunnableposting its answer, judged on the GUI thread so a stale one is dropped — [[appendices.task-queue#serial]] explains why the task queue is not one). This is the default, and nearly all of the agent's background work. - Work that can block on storage or a network the app does not control, and so must never hold up quitting, runs on
a Python
threading.Thread(daemon=True)with athreading.Eventto stop it. Qt joins its pools at exit, so a worker stuck in a call to a switched-off server keeps the process alive for as long as the call takes (21 s measured for an SMB host); a daemon thread is simply left behind. Nothing is killed:QThread.terminate()is unsafe, and aQThreaddestroyed while running aborts the process. - A pool job never lets a reader's exception escape (#480): the pool swallows it and no answer ever arrives. Catch broadly with a reasoned pragma, log it, and answer unreadable.
- A daemon thread holds no Qt object and delivers nothing after
stop(). Its callback runs on its own thread and only emits a signal, which Qt queues to the GUI thread; the owner stops it first thing when the window closes. - First users: the task queue's worker ([[appendices.task-queue#teardown]], daemon for the same reason) and
borco_core.PresenceScan(#464).
3. Markdown
[[[appendices.code-conventions#markdown]]]
Docs under docs/ are markdownlint-checked (.markdownlint.json; MD013 line length 120, tables exempt). Also:
- Blank lines around headings and lists (MD022/MD032); inside a blockquote the separator is an empty
>line, not a truly blank line. - Spaced table delimiter rows —
| --- | --- |— and single-space-padded cells. - No emphasis-as-heading (MD036) — use real
###headings. - Under a spec section heading, list its GitHub issue(s) as plain list items —
- [#N: title](url)— regardless of whether the issue is open or closed. Deliberately not a- [ ]/- [x]task list: a checkbox has to be ticked in a commit every time an issue closes, which is churn in the specs for state GitHub already tracks authoritatively. The link records which issues a section belongs to; their status is read on GitHub. - Cross-renderer link rendering. Docs are read both on GitHub (blob view, PRs) and the published mkdocs site, which render some references differently:
- A bare
#123issue/PR reference autolinks natively on GitHub already;pymdownx.magiclink(mkdocs.yml) gives it the same treatment on the published site. Its issue-shorthand pattern requires digits right after#, so it never collides with a[[doc#slug]]cross-reference token (slug segments are never all-digit) — see "Symbolic cross-references" indocs/specs/README.md. - A plain relative link from a doc into a repo file outside
docs/(e.g. a.pysource file underpackages/) resolves natively on GitHub, but 404s on the built mkdocs site, since onlydocs/is ever published. Write such links as plain relative paths regardless —tools/mkdocs_relative_link_hook.pyrewrites, at build time only, any link whose target falls outsidedocs_dirto an absolute{repo_url}/blob/{branch}/...link; GitHub needs no change.
4. Testing
[[[appendices.code-conventions#testing]]]
Each test's docstring ends with a **Test steps:** bullet list spelling out the steps and checks, so intent
is readable without tracing the code — a project convention, not a pytest feature. The test stack and
how to drive it are covered in [[appendices.testing#qa-gate]].