Serial-Studio/CLAUDE.md at master · Serial-Studio/Serial-Studio · GitHub
Skip to content

Latest commit

 

History

History
423 lines (383 loc) · 38.6 KB

CLAUDE.md

File metadata and controls

423 lines (383 loc) · 38.6 KB

CLAUDE.md

Behavioral Rules

  • Read before writing. Never edit a file you haven't read this session.
  • Read hotpath code in full (FrameBuilder, CircularBuffer, FrameReader, Dashboard) before touching it. Read BluetoothLE.h/.cpp before writing any new driver — it's the canonical reference.
  • Read existing signal/slot wiring in a file before adding or changing any.
  • Plan before multi-file changes (>3 files): state the plan, get confirmation. Non-trivial or multi-file work runs through spec-driven development (/ss-spec → /ss-plan → /ss-tasks → /ss-implement); see doc/claude/spec-driven.md.
  • Edit, don't rewrite. Targeted Edit calls; full rewrite only when asked or >70% changed.
  • No preamble, no trailing summary — except a one-line statement of intent before non-trivial work, and one or two sentences naming what changed (and what's next) when you stop. Skip both on trivial edits. (The Context Canary line below is exempt — it is mandatory on every response.)
  • Do not create markdown/doc files unless asked. Share info conversationally.
  • Never build — but always syntax-check. Never invoke cmake/jom or produce build artifacts — the developer builds the app themselves. The ONE sanctioned compiler use (2026-10-04) is scripts/syntax-check.py (clang -fsyntax-only via the newest compile database under build/): run it on every C++ file you edit, every time — lint checks shape, only the compiler checks truth (types, includes, reachability), and every AI batch-edit failure of 2026-10 was compiler-class and lint-invisible. A hook runs it per edit; a SKIP (stale database) means the claim "it compiles" is unverified — say so.
  • Runtime experiments are sanctioned. Ground truth beats on-paper reasoning: drive a running app through the API server (localhost:7777, tests/utils/api_client.py) to probe hypotheses, not only to verify fixes; run ctest and already-built binaries (--selftest, --benchmark-hotpath) against an existing build dir; prototype ideas as throwaway Node/Python sims in the scratchpad. Launching the GUI app and compiling stay the user's.
  • Sample the running app before theorizing about a GUI stall. Never reason a freeze, stutter, or ignored resize out of the source — sample <pid> in the background while the user reproduces. Recipe, known stack signatures, and the 2026-08-13 incident: doc/claude/common-mistakes.md "Diagnosing a GUI Stall".
  • Update CLAUDE.md for any architectural change that future me would otherwise miss.
  • scripts/ is the style contract. When in doubt, run it; don't restate it here.

Context Canary — Last Line of Every Response

End every response — including one-word answers — with this exact line, reproduced from memory:

canary: qt 6.11.2 | cpp20 | hotpath 256k (native 1024k, js 64k) | queue 65536 | api 7777 | style 100/2

It is a context-health probe in plain ASCII. This file is in context every turn, so the line is reproduced from the prompt, not recalled: the probe measures whether the always-present instruction still steers. Compaction or attention drift shows as a mutated or missing value, and which value drifted names the rule that lost its hold. Retyping it also re-anchors the constants (J-Space, below). One plain-text line at the very end.

  • From the prompt only. Never Read/Grep this file or anything else to reconstruct the line — a looked-up canary defeats the attention measurement. If you cannot reproduce it confidently, write canary: lost instead of guessing: that is the signal firing.
  • Verbatim. Same values, same order, every turn. Do not paraphrase, reformat, extend, or "improve" it.
  • For the developer: a mutated, partial, or vanished canary means a degraded context: treat recent output as suspect, checkpoint, /compact or restart.

Trust Contract

Predictability, not productivity — the difference between a tool the user re-audits every time and a collaborator they rely on. Capability without predictability gets disabled. Full text and the incidents behind each rule: doc/claude/trust-contract.md.

  • Never touch, revert, or restore files outside your own edits — the one rule whose violation loses real work. A working-tree file you did not edit this session is the user's in-progress work: never git checkout/restore/reset/stash/clean it, overwrite it, or "clean it up". Derived artifacts (.ts/.qm, build output) included. In the way or seems wrong? Quote the path in chat and ask. When unsure whether a file is yours: it is not.
  • Stay in your lane. Spot an adjacent fix? Name it in chat ("noticed X — want it in this pass?") rather than slipping it into the diff.
  • Show the why, not the what. Comment/reply/commit message explains why, and only when the choice was non-obvious. One sentence.
  • State the plan before non-trivial work — visible before execution, not a summary after. Operationalized as /ss-spec, whose premise check measures the one number that would kill the spec. Spec and plan land in their own commit before the code; scripts/commit-shape.py refuses that mix, a protected surface staged with other code, and bulk without a Mechanical: trailer. Never cite the archive as proof of gating.
  • Self-review before handoff. Re-read the diff: is this what was asked, and only that?
  • Security-sensitive surfaces are decision-visible. app/src/Licensing/, core/Core/Licensing/, MachineID, and the API Server auth/consent files: any behavioral change there is named in chat with its failure modes BEFORE it lands, and never rides inside a bulk/agent package (the 2026-09-03 stored-fingerprint inversion shipped a seat-cloning hole into master for a month exactly that way). The checklist is doc/claude/specs/0042-license-token-hardening/consumers.md.

Delegation & Cost — Advisor Economics

Quality concentrates in planning and review; bulk token generation doesn't need the strong model. Anthropic's advisor-tool measurements and the spec-0069 post-mortem agree on the shape: the main session advises, cheaper executors type.

  • The main session plans, reviews, and integrates; mechanical execution is delegated. Applying an approved brief, repetitive multi-file sweeps, test-writing against a fixed pattern: run them in subagents with a cheaper model override (Sonnet by default, Haiku for purely mechanical work). Design decisions, hotpath edits, and anything on a protected surface stay in the main session.
  • Two high-tier touchpoints per task, not continuous involvement. One after orientation but before committing to an approach (the /ss-plan gate), one before declaring done (the self-review gate). Between them the strong model directs; it doesn't type.
  • Briefs and reports are budgeted. A per-component brief fits half a page: files, the invariants that bind, one acceptance check. Subagents return conclusions, never file dumps. Measured result: hard output caps cut advice volume ~7x with no detectable quality loss — unbounded high-tier prose is the dominant avoidable cost, so cap first and raise only on evidence.
  • Searches fan out, conclusions come back. Multi-file exploration runs in read-only search subagents; the main context receives the answer, not the excerpts. This also keeps the cached prompt prefix stable.
  • No process tax on trivial work. Mandating a consult/plan step on tasks whose first action needs no planning measured net-zero quality gain at pure cost. Keep the "skip for trivial" escape hatches, and never write a rule that both mandates and restrains the same mechanism — conflicting steering is worse than either alone.

Scripts

All scripts in scripts/ are CWD-independent and write LF endings; run from anywhere. The per-script table lives in doc/claude/scripts.md. What binds you: run sanitize-commit.py before every commit (sanitize only — it never commits or pushes); code-verify.py is the structural linter (--fix rewrites, --check regenerates .code-report; errors block CI). Suppression: // code-verify off / // code-verify on (C++/QML), <!-- doc-verify off --> / <!-- doc-verify on --> (Markdown) — suppressions are a code-review trigger. .code-report / .doc-report / .claim-report / .tidy-report are the cleanup checklists; advisories are baseline debt, new code still clears them. term-verify.py gates user-facing terminology (spec 0100): one name per concept across UI strings, command manifests, the manual and the assistant corpus, banned synonyms in scripts/term-rules.json; internal identifiers keep their old names on purpose.

Four gates ratchet growth against a checked-in baseline instead of capping absolute size, and --accept re-seeds each: code-verify.py --singleton-census, code-verify.py --tu-census (excess over 1500 lines), claim-verify.py, which resolves every path, symbol and pinned constant in the AI-facing docs — the canary's values included — against the tree, and ledger-verify.py (uncodified ledger rows); layer-verify.py gates the core/ layering the same way. Baselines, fences and anchor mechanics: doc/claude/scripts.md.

Tests

Python/pytest suite under tests/. Full catalog — per-file coverage, fixtures, markers, the delay/operation-mode tables, the C++ ctest tier, the --selftest in-app tier — lives in tests/README.md; read it before writing a test. What binds you:

  • You don't build or launch the app, but you may drive a running one. Integration, security, and performance tests drive a running Serial Studio over TCP — they need the app up with Preferences → API & Plugins → Enable API Server (localhost:7777). The user launches it; once up, you may run those tests and poke the API directly (tests/utils/api_client.py) to test hypotheses. Probe first: nc -z 127.0.0.1 7777.
  • tests/scripts/ is the exception you can run — pure JS frame-parser unit tests, fresh Node.js subprocess per case, no Qt, no app. pip install -r tests/requirements.txt once; pytest.ini registers all markers and a 30 s per-test timeout.
  • scripts/tests/ is the tooling tier, also yours: linter fixtures, census gates, the CI shape, the hooks on fixture transcripts.
  • C++ units under app/tests/ (spec 0032) run via ctest (no CMake presets — the repo uses hand-written cmake -B ... -D... configures; CI inlines the unit-tier flags) — the maintainer builds; you may run ctest against an existing build dir (never configure or compile). --selftest suites run inside CLI::process() before the composition root: never touch an application singleton there.
  • The C++ hotpath has no pytest path — throughput is the user-run --benchmark-hotpath gate (see Threading & Hotpath), piece correctness the ctest tier; ci.yml runs both.
pytest tests/scripts/ -v                  # JS-parser units (Node.js only) — safe for you
pytest tests/integration/ -v              # all integration (needs running app)
pytest tests/ -m "not destructive" -v     # skip server-crashing tests

Project Overview

Serial Studio: cross-platform telemetry dashboard, Qt 6.11.2 + C++20. Data sources: UART, TCP/UDP/WebSocket/HTTP (one Network driver, spec 0068), BLE, Audio, Modbus, CAN Bus (Qt plugins plus direct gs_usb/SLCAN/Seeed backends; J1939 TP + ISO-TP reassembly, spec 0073), MQTT (Sparkplug B both directions, specs 0073/0074: host-application subscribe with project generation, edge-node publish with stable aliases), OPC UA (embedded open62541 + mbedTLS, six security policies None through Aes256_Sha256_RsaPss, spec 0067), Siemens S7comm (S7-300/400/1200/1500, in-house stack), EtherNet/IP (libplctag), IEC 60870-5-104 (in-house stack; all three PLC/telecontrol drivers are read-only clients, spec 0073), USB (libusb), HID (hidapi), Process I/O. 15+ visualization widgets, 5 output (control) widgets, 256 kHz+ data rate (CI-gated; see below). Frame parsers in JavaScript (QJSEngine), Lua (embedded LuaJIT 2.1, 5.1 + shims; per-project Safe/Fast execution mode — spec 0051), or Built-In ("Native" in all internal identifiers — SerialStudio::Native, CFrameParser, NativeTemplate; only user-facing strings/docs say Built-In. Parametrized C++ templates configured via a JSON descriptor, no user code). Per-dataset value transforms in JS or Lua, with one shared library per language per project and per-dataset params (spec 0083). Pro features: Output widgets, Modbus, CAN Bus, OPC UA, S7comm, EtherNet/IP, IEC 60870-5-104, Sparkplug B, MDF4, 3D, ImageView, Waterfall, file-transfer protocols (X/Y/ZMODEM), Modbus map importer, Historian (per-session SQLite recording; "Session Database" pre-2026-08), InfluxDB 2.x sink (core/Storage/InfluxDB/, line protocol per published block). Aural alerts (spec 0087, every build): an ISA-18.1 annunciator (UI::Alarms::AlarmAnnunciator, root-owned, Cpp_UI_Alarms, alarms.* API) with IEC 60601-1-8 WAV signatures played by IO::Audio::SoundPlayer on a raw miniaudio device. User-facing renames of 2026-08-19 — internal identifiers unchanged: Historian (was Session Database; Sessions:: namespace, sessions.* API, "Session Databases" folder stay), Variables (was Shared Memory; registers → variables in UI/docs, RegisterDef/registers JSON stay), Computed Dataset (was Virtual Dataset; virtual_ field + "Virtual" JSON key stay), Canvas Widget (was Painter; "painter" widget key, project.painter.* API, Painter* classes stay). In doc/help the hotpath is called the "acquisition pipeline" — code, benchmarks, and internal docs keep "hotpath". The code base is seven static libraries under core/, every one a STRICT layer since spec 0077 (SerialStudioCore and SerialStudioProtocols with prefixed includes; SerialStudioPipeline, SerialStudioDevices, SerialStudioStorage, SerialStudioApi and SerialStudioUi keeping their relative include roots) plus the composition root in app/src; a library includes and links only the layers below it. Cross-library talk goes over Core::Bus::MessageBus (typed topics, shared-pointer messages, retained state), through the interfaces the root binds, or through the three root-bound module sets (Core::services(), DataModel::pipelineModules(), API::handlerContext()) — never an instance() reach into another library; scripts/layer-verify.py fails on any include, include root or link outside the graph, and code-verify.py --singleton-census fails on any cross-library reach.

Sub-Documentation

Deep subsystem detail and the silent-breakage lookup live in doc/claude/; read the section a pointer names before working there. The inline blocks are pointers, not substitutes.

Document When to read it
architecture.md Before touching any subsystem: the index into doc/claude/architecture/ — dataflow (hotpath), startup, io, project, scripting, dashboard, kernels, export, mirror, ai, commands-icons.
architecture/ai.md Before touching core/Ui/AI/, app/rcc/ai/ or app/qml/AI/: the command safety tiers, checkpoint-not-save semantics, the meta-tool discovery seam, the provider/reply contract, and the FileSandbox / KeyVault trust boundaries.
common-mistakes.md The silent-breakage lookup: gotchas the linter can't catch (timestamp capture, queued-vs-direct hotpath, operator[] inserts, macOS file-dialog reentrancy, the GUI-stall sampling recipe).
code-style.md Full style spec + NASA Power of Ten. The Code Style block below is the inline essentials.
trust-contract.md Full text of the Trust Contract above, with the incidents behind each rule.
directory-map.md The core/ / app/src / app/qml / lib tree, one line of role per subsystem.
scripts.md The per-script table for scripts/.
working-relationship.md How to collaborate here: recommend don't enumerate, push back when a choice will cost, ground truth outranks on-paper reasoning. Read once per session.
j-space.md The verbalization discipline and its grounding. Read when tuning any AI-facing doc or skill.
repo-skills.md The project /-skills catalog and when each fires. Most auto-activate; this is the deliberate-pick lookup.
spec-driven.md Before any non-trivial or multi-file feature: the four gated phases, where artifacts live, when to skip.

J-Space Discipline — Verbalize the Binding Constraints

Deliberate reasoning runs on a small set of verbalized concepts; familiar-shaped work runs on autopilot and bypasses it (doc/claude/j-space.md). The repo's rules only steer an edit if named at the point of action, so:

  • Name before acting. Before any edit on a protected path (hotpath, ctor closure, signal wiring, cmake flag modules), state in chat the 3-5 invariants that bind this change — in your own words, not a doc citation.
  • Few, late, specific. Only the constraints that bind the change at hand; never recite whole rule files.
  • Counterfactual check at handoff. Which rule does this diff most risk violating, and what concrete evidence says it doesn't? Name both.
  • Diverge by naming. Design/review work sketches named alternatives before recommending (the human still gets one recommendation, per working-relationship.md). At least one alternative must violate an assumed constraint — then check whether that constraint actually binds: wrong answers usually hide in premises, not derivations. Wide design spaces may fan out a judge-panel workflow of independent attempts.
  • Externalize long state. Write intermediate state into durable artifacts (spec/plan/ tasks files, a chat checklist); re-name only what binds the current edit.

Threading & Hotpath — Non-Negotiable

The rules most likely to cause silent breakage. Full detail (data flow, threading table, cached flags, benchmark mechanics) in doc/claude/architecture/dataflow.md; invoke the ss-hotpath skill before editing these paths (nothing loads it for you) and it re-states them, including the spec-0055 block caps, the time-ring/plot-clock rules, and the kernel macros.

  • The frame pipeline runs on IO::PipelineHost's processing thread (spec 0051 M3), not the GUI thread. FrameReaders, FrameParser and FrameBuilder all live there; the GUI drains finished pooled frames from an SPSC ring in Dashboard::onDisplayTick. Public FrameBuilder/FrameParser mutators SELF-MARSHAL — GUI→pipeline waits go through IO::PipelineHost::runOnObjectThread (event-loop-backed), pipeline→GUI reads through runOnGuiThreadBlocking. Never add a plain BlockingQueuedConnection from the GUI into the pipeline.
  • runOnObjectThread is for command-rate waits, never for a GUI-thread script API. The wait spins a nested QEventLoop that fires the display tick and re-enters the calling script. DataModel::readTableView / writeTableStore (DataTable.h) hold the ONE routing rule, and both the JS bridge and the Lua closures go through them. No lua_* call may run inside a routed lambda. Any new GUI-callable script API follows the same rule — see doc/claude/architecture/scripting.md "Data Tables".
  • FrameReader and CircularBuffer are pipeline-thread / SPSC. Never add mutexes. Recreate via resetFrameReader() / reconfigure(). Enforced: code-verify:spsc-lock.
  • In-pipeline signal hops must be Qt::DirectConnection. Queued between two pipeline-thread objects fills the 65536-slot queue at 10+ kHz and drops frames. GUI↔pipeline traffic is chunk/command/tick rate only — never a per-frame queued emission. Enforced: code-verify:pipeline-queued-hop.
  • No allocation on the publish path. Blocks come from a pool (use_count()==1 probe, aliasing shared_ptr) with columns sized once at bind — DataModel::BlockStager owns it on the frame lane, StreamProcessor::claimBlockSlot() on the stream lane. The one copy is clone_block_trimmed, made by DataModel::BlockPublisher for the async sinks — deliberate and gated on a sink being on.
  • One publication payload, one ingestion path (spec 0055). Nothing publishes a TimestampedFrame any more: FrameBuilder's BlockStager stages parsed rows into a pooled DataModel::DataBlock and flushes on the display tick or a sample cap (kFrameBlockSampleCap 64, kStreamBlockSampleCap 4096); BlockPublisher fans the finished block out. Dense sources do per-sample work on their own IO::StreamWorker thread but emit blockReady queued to the pipeline thread, the SINGLE producer for every sink. Structure travels separately as a StructureSnapshot. Never add a rate cap or a per-view reduction; an overrun drops whole blocks and counts them.
  • The two republish lanes never share one "already republished" mark (spec 0064), or the dashboard stays live while every recording comes out empty. Enforced: compile:DataModel::RepublishLane, ctest:tst_republish_lanes. Detail: dataflow.md "The Two Republish Lanes".
  • The session boundary is the sink contract (spec 0075). Recording sinks close on FrameBuilder::sessionBoundary(connected, paused) — pause included — and the builder flushes every open block BEFORE emitting, so the tail lands in the file that was open. The export-time tie-break is per source (monotonicSourceNs) and a uniform-grid block never takes it.
  • Time rings are sized from a rate, never a sample count, and a ring's clock never rewinds. Since spec 0097 they start rate-true and grow per block on coverage (EnvelopeRing::growFor, the one named allocation on the ingest path, never inside a per-sample loop), keep every sample up to the per-curve ceiling or the 512 MB rail, and display storage is never pinned. Enforced: compile:DSP::RingCapacity, compile:UI::PlotClockState, ctest:tst_time_ring_sizing, ctest:tst_ring_exactness, code-verify:lock-memory-scope. Rules + the 2026-08 incidents: dashboard.md "Time-Ring Sizing & the Plot Clocks".
  • Native + PlainText parses through the span fast lane (trySpanLane → parseUtf8Spans → applyDatasetValuesSpans), zero steady-state allocation. Hotpath flags are DataModel::Cached<T> re-derived at 1 Hz (spec 0095); a new input wires its change signal to the cache refresh, a new cache is never a plain bool. Enforced: ctest:tst_cached_flags, anchor:cached-hotpath-flags. Read dataflow.md "Cached Hotpath Flags" before touching any.
  • Source owns time. Stamp at the driver boundary; never re-stamp in export/report workers (use monotonicFrameNs(...) as the safety net only). Enforced: code-verify:--clock-census.
  • Driver opens are synchronous calls; most drivers dial asynchronously behind them. An async dial's verdict has ONE owner, HAL_Driver::openFinished(ok, reason), emitted exactly once per attempt; a driver that reports only success wedges the connect button. Doctrine: doc/claude/architecture/io.md "Opening a Link".
  • Diagnostics are pulled, never pushed (specs 0033/0035). Pipeline counters are plain quint64 increments polled on the 1 Hz tick, never a per-frame signal, allocation or lock; a recreated FrameReader zeroes them, so consumers work on deltas. Detail: dataflow.md "Diagnostic Counters" (the polled-PLC atomic deviation: io.md).
  • JS scripts: always JsScriptEngine::guardedCall(), never parseFunction.call(). setInterrupted(true) only in JsWatchdogThread.cpp. Enforced: code-verify:js-raw-call, code-verify:js-interrupt-site.
  • 256 kHz is a CI gate, not a slogan. --benchmark-hotpath runs nine gates tiered off --min-fps (default 256000) as a hard gate in ci.yml; don't regress it. Enforced: anchor:hotpath-min-fps. Tier table and the ~70-80% datasets+publish share: ss-hotpath skill.
  • The message bus is not a hotpath primitive (spec 0076): frames and blocks keep the pooled SPSC path. Enforced: code-verify:bus-on-hotpath. Detail: directory-map.md "Message bus".
  • Reuse the kernels; never inline intrinsics or invent a macro — core/Core/DSPSimd.h (bit-exact per lane, runtime-dispatched) and core/Core/HotpathOptimization.h; never fast-math, no-unwind or GCC optimize("..."), and no TU is compiled wide. Rules: doc/claude/architecture/kernels.md. Debt: no intrinsic-header lint yet.

Startup & Composition Root — Non-Negotiable

Full contract, including the ctor-edge proof and the licensing consumer inventory: doc/claude/architecture/startup.md.

  • ModuleManager::instantiateCoreModules() pins singleton construction order (ProjectModel before AppState, Dashboard last); never reorder or add entries without re-running the ctor-edge proof in doc/claude/specs/0001-composition-root/. Enforced: anchor:composition-root-order.
  • No new function-static singleton reach in a module ctor closure — the one residual hazard now that an early reach into an adopted module fails by name. Enforced: compile:Core::ModuleConstruction, script:scripts/code-verify.py#--singleton-census.
  • SessionContext (spec 0039) owns the nine core modules; never call SessionContext::current() from a method body, and no library includes SessionContext.h. Enforced: code-verify:arch-session-adopt-site, script:scripts/code-verify.py#--singleton-census.
  • Every composition root goes through ModuleManager::composeSession(BindMode); an unbound root publishes through a null host and a sink left off its list records a valid-looking empty file. Enforced: compile:Misc::ModuleManager::composeSession.
  • License-gated state must exist before restoreLastProject() or re-derive on activatedChanged. The licensing block is the FIRST thing instantiateCoreModules() builds after Translator (spec 0042). activatedChanged fires only on real token-validity transitions. Anything baking SerialStudio::activated() into derived state at load time still needs a LemonSqueezy::activatedChanged hook, or late/async activation ships fallback widgets (2026-07-09: Plot3D degraded to MultiPlot).

Subsystem Contracts — Read Before Touching

Each row is a trigger, not a summary: read the named section before editing that area. The hazard column names what breaks silently; the doc holds the rule.

Touching Read Hazard
Any ProjectModel mutator, undo/redo (spec 0031) project.md "Undo History" Mementos are two-phase: ProjectUndoScope stages, the first setModified(true) commits — omit it and nothing is recorded. Enforced: code-verify:undo-scope-missing.
A dataset property or any API-surface generator (specs 0036, 0037) project.md "Dataset Property Registry", project.md "Generated API Surfaces" app/rcc/properties/dataset.json generates six checked-in artifacts — never hand-edit a generated file. gRPC field numbers are append-only; a moved one fails CI.
A toolbar button, palette entry, context menu, shortcut, or fixed icon (specs 0028/0063) commands-icons.md "Icons", commands-icons.md "Commands" Icons resolve ONLY via Misc::IconRegistry — never hardcode qrc:/icons/.... New command = one manifest entry + one bindings entry; Enforced: script:scripts/registry-verify.py. Project Editor context menus are registry-driven too (editor-menus.json + ProjectEditorMenuBindings.qml + CommandMenu.qml) — never hand-write a Menu there.
Installable widgets, UI::WidgetExtensions (spec 0038) dashboard.md "Widget Extensions" Packages resolve to DashboardExtension = 100, persist as "ext:<id>"; readsStringValues is what registers one in string_targets. Trust model is consent, not containment — never call an extension sandboxed.
Alarm bands: UI::AlarmMonitor trackers, the annunciator's band points, the Project Editor's Alarm Bands buttons, the suppressAlarms or alarmsWhileVisible option (specs 0093/0105) dashboard.md "Alarm Bands" Whether a dataset's bands are drawn has ONE home, SerialStudio::datasetRendersAlarmBands (Bar, Gauge, Meter, LED or a Bar Panel member); whether they alarm is datasetRaisesBandAlarms beside it (drawn, non-empty, suppressAlarms off) and, with alarmsWhileVisible, datasetAlarmsNow (a widget on screen). A second copy of one lets a band drawn nowhere sound while the editor refuses to open it. Bands on other datasets are kept and ignored, never cleared.
core/Ui/Console/Annotations.*, ConsoleAnnotations.qml (spec 0059) dashboard.md "Frame annotation layer" annotate() stages, commitPending() publishes per tick — reading count() right after needs a commit. reset() clears the model before re-reading the offset.
core/Api/API/Mirror/, streamAvailable() (spec 0040) mirror.md "Wire protocol" Dataset ordering or wireUniqueId changes are wire breaks: bump kWireVersion, regenerate tests/fixtures/mirror/. Viewer frames never reach the export fan-out.
Chrome motion: a popup, menu, dialog or button animation in app/qml PopupEnter.qml, PopupExit.qml Popups take the two shared transitions plus a transformOrigin, never an ad-hoc Transition. Anything that scales, slides or bounces gates on Cpp_Misc_GraphicsBackend.reduceMotion (fades stay). Animate opacity/scale/Translate only — never width/height of a laid-out item, never inside Widgets/Dashboard/ or a recycled TreeView/ListView delegate.
HDR output: HdrSurface/HdrBoost, Misc::HdrOutput, an emissive material, a shader under app/shaders/, or any MultiEffect/layer near an emissive element (spec 0089) dashboard.md "HDR Output" The whole UI composites SDR inside a per-window FP16 layer; only the output transform converts. An emissive element behind a MultiEffect or default-format layer silently clamps to SDR ("slightly dim" is a defect). The transfer math lives in Misc/HdrTransfer.h and every shader copies it verbatim (tst_hdr_transfer pins it); _qt_sg_hdr_format is private Qt API — re-verify per Qt bump.
An embedded code editor's render cadence scripting.md "Embedded Code Editors" Never give a main-window-embedded editor an unconditional per-tick grab() — cost 13% of the GUI thread (2026-08-17).
The AI assistant: a tool tier, the checkpoint timer, a provider, the tool surface, the sandbox roots ai.md "The Tier Model", ai.md "Checkpoint Semantics" A mutating tool call takes a checkpoint, never a save — the API descriptions and app/rcc/ai/skills/ say so to the model, so a disk-contract change is incomplete until those strings change too. Every command sits in exactly one tier of command_safety.json; an unlisted name silently falls through to Confirm. The bundled source behind the source/ prefix (spec 0078) is read-only and joins a search ONLY through the path scope — never add it to the default walk.
Locating a god object's concerns (ProjectModel, ProjectHandler, FrameBuilder, Dashboard) directory-map.md Spec 0070 re-formed the god objects into facades owning real sub-object classes (one class = one .h/.cpp, in a sibling dir named after the facade). Never split one class across TUs: decompose into member sub-objects instead.
The connect gate, a dataset transform or user-table mutator, or any new way to author either (spec 0094) io.md "Opening a Link" A project with transforms or user tables refuses to CONNECT unlicensed (IO::EntitlementGate, rule in SerialStudio::proContentSummary), and a live unlicensed session ends when its content grows past what its connect admitted; it never runs with the math switched off, because uncalibrated values on a dashboard are worse than a refusal. A new authoring path (API command, importer, generator) needs the license refusal too, and removal paths stay ungated on purpose. Playback is not gated: replay never runs a transform.
Anything that switches the operation mode: a project load, save or generation, a file drop, an example download, the Project Editor opening (spec 0100) AppState.h An implicit switch goes through AppState::requestOperationMode(), which defers while a link is live and applies on disconnect; only explicit mode choices (radios, mode.*, CLI, the API mode command) call setOperationMode(). A direct setOperationMode from a load path rebuilds devices under a live connection.
A new API command, one that runs or installs code, loads a project or launches a process, or one that re-dispatches other commands (spec 0098) CommandCapabilities.h, CommandRegistry.h, LaunchAuthorizer.h Every command carries a capability classification, and the remote gate refuses one without. Enforced: ctest:tst_command_capability_registry. Dry runs and compile checks build their engines with ScriptSurface::Inert, never the live bridges. Every program start, a script's system.exec or a Process source, asks IO::authorizeLaunch (the per-project launch consent). Remote origin is a dispatch scope (RemoteDispatchScope), never a per-site check, and the assistant's tiers ride a separate DispatchPolicyScope, because licensing reads RemoteDispatchScope::active(). SERIAL_STUDIO_API_AUTO_CONSENT=1 grants every gate for headless runs.
A new API command, or anything that serializes the project or a live frame outward (spec 0096) io.md "Sealed Sessions" A process opened from a .ssdeploy package is sealed one-way (Core::Runtime::sealed()). A new command needs a classification in API::SealedPolicy, and an unclassified name is refused (Enforced: ctest:tst_sealed_policy_registry). A new exit for the project or a live frame consults the seal and strips through DataModel::stripSealedContent, or it hands the package's script source to any API client (Enforced: ctest:tst_sealed_strip).
Anything under core/ or a source that could move there (spec 0076) directory-map.md "core/" A library never includes app/src; a moved Q_OBJECT header listed in both a library and the executable mocs twice; every .h/.cpp pair lives in one target; a relative include must resolve in exactly one root; an upward include beyond scripts/layer-baseline.json fails CI; the bus is never on the per-frame path.
The driving surface (spec 0101): core/Ui/UI/Automation/, AutomationHandler, an objectName under app/qml, or the DrivesUi consent automation.md A control is addressed by its objectName and nothing else: a translated or theme-derived name breaks every client, an unnamed control is invisible, and click/key sit behind a consent no other consent unlocks. Roles lean on Qt internal class names pinned by tst_ui_automation; re-verify per Qt bump.

Code Style — Essentials

scripts/code-verify.py is the contract — read its --check output, don't re-derive the rules. Full spec and the NASA Power of Ten live in doc/claude/code-style.md. The handful you need before typing:

  • Format: 100-col, 2-space indent, LF, pointer/ref binds to type (int* p). No braces on single-statement bodies; blank line after a brace-free body. Max 3 nesting levels (guard clauses); functions 40-80 lines, hard limit 100; translation units under 1500 lines (cxx-tu-too-long — the function cap alone let god TUs accrete a method per feature). Run clang-format.
  • Headers (.h): Q_OBJECT → Q_PROPERTY → signals: → ctor/deleted copy → public: (instance() first) → public slots: → private slots: → private:, Christmas-tree in each block. [[nodiscard]] on every non-void return. Never Q_INVOKABLE void (use public slots:). No in-header member init — ctor init list only.
  • Signals: Q_EMIT not emit; lowercase signals:/public slots:; never SIGNAL()/SLOT(). Never disconnect(nullptr) as the slot — capture the Connection.
  • Comments: code is the spec; label, don't narrate. No comments inside a function body (cxx-inbody-comment, advisory) — functions are short, so the one-line /** @brief ... */ above the function plus self-explanatory code carry it; fold a load-bearing why into the @brief, or fence a genuinely-needed note with // code-verify off. //--- concern-group banners live between functions, never inside one. No inline EOL comments, no AI narration. Don't fake the em-dash with -- — rewrite the sentence.
  • Naming: CamelCase types, camelCase functions, lower_case locals + public members, s_/m_/k/UPPER_CASE for static/private/constexpr/macro (full table in the sub-doc).
  • Safety-critical (NASA Power of Ten) — hotpath violations are blockers. The ones that bite: no alloc/Frame-copy on the dashboard path; fixed loop bounds + capped recursion; assertion density ≥2/function; [[nodiscard]] + return checks at every system boundary; zero warnings; no reinterpret_cast/dynamic_cast on the hotpath; SPDX header per file — first-party is GPL-3.0-or-later (relicensed from -only, 2026-07). The repo is REUSE-compliant: REUSE.toml + LICENSES/, reuse lint gates in CI.
  • Assertions are SS_ASSERT(cond, action) (core/Core/SSAssert.h), not Q_ASSERT: the condition evaluates in every build; debug aborts, release reports once per site and runs the recovery action instead of the guarded code. Condition side-effect-free and cheap; action side-effect-complete, single statement, no top-level comma, never continue/break (use SS_ASSERT_LOG(cond); if (!(cond)) continue;). On the per-frame/per-cell kernels use SS_ASSERT_HOTPATH(cond) instead — see doc/claude/architecture/kernels.md.