Skip to content

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; the rehuco-core sweep 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: Final without 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: @override on 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-locals is 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 .ui files, not built up in Python. For a custom widget, prefer a .ui too, 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 .ui with its controller class: foo_widget.ui, foo_widget.py next to each other; the generated foo_widget_ui.py is gitignored and rebuilt via make uis — never hand-edit it.
  • A custom widget used inside a .ui is placed as a base/QWidget placeholder 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 qrcs regenerates the gitignored _rc.py. A .ui referencing a qrc-managed resource (e.g. windowIcon) must declare <resources><include location="....qrc"/></resources> so make uis emits the matching resource import — verify the generated file after adding one.
  • Ask before adding a new asset-conversion pipeline (e.g. deriving .ico from the .svg master) rather than improvising one inline.
  • Every property/attribute in a .ui file 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 after setupUi(), not in the .ui.
  • Name widgets in .ui files snake_case, not Qt Designer's default camelCase — this is a recommendation, not a hard rule; use camelCase where 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) becomes central_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 QAction serves 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(), a QRunnable posting 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 a threading.Event to 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 a QThread destroyed 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 #123 issue/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" in docs/specs/README.md.
  • A plain relative link from a doc into a repo file outside docs/ (e.g. a .py source file under packages/) resolves natively on GitHub, but 404s on the built mkdocs site, since only docs/ is ever published. Write such links as plain relative paths regardless — tools/mkdocs_relative_link_hook.py rewrites, at build time only, any link whose target falls outside docs_dir to 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]].