Skip to content

feat(status): add Brother 32-byte status block parser#29

Merged
strausmann merged 2 commits into
mainfrom
feat/status-block-parser
May 10, 2026
Merged

feat(status): add Brother 32-byte status block parser#29
strausmann merged 2 commits into
mainfrom
feat/status-block-parser

Conversation

@strausmann

Copy link
Copy Markdown
Owner

Summary

First real code in the project — implements the StatusBlockParser per ADR 0006. Decodes the binary status block that Brother PT-Series and QL-Series printers return on ESC i S and emit during print jobs.

This PR also lays down the backend project skeleton (pyproject.toml with FastAPI/SQLModel/pytest/ruff/mypy stack, conftest with --hardware skip flag, package layout per ADR 0001 §3.2).

Linked issue

Refs #11, #19

Type of change

  • New feature (non-breaking)

Hardware tested on

  • Brother PT-Series (PT-P750W) — Phase 0 hardware test sample data validates against this parser
  • Brother QL-Series (QL-820NWB) — Phase 0 hardware test confirmed identical SNMP map; ESC i S not standalone-supported (documented in ADR 0006)
  • Real 32-byte responses (sanitised, no maintainer-private values) used as test fixtures

Test coverage

Checklist

  • PR title follows Conventional Commits (feat(status): …)
  • No private IPs, hostnames, domains, real tokens, or PII — privacy scan green locally
  • Documentation updated (ADR 0006 already references this module)
  • Type hints + mypy strict — green
  • ruff lint + format — green
  • CI green (will check after push)

Design notes

Why one parser for both PT and QL families:

  • The 32-byte layout is shared between PT-Series (Raster Reference v1.02) and QL-Series (v1.01). Both use the same offsets for header (0-7), error bytes (8-9), media width (10), media type (11), status type (18), phase type (19), notification (22).
  • Family-specific quirks are absorbed: PT uses media codes 0x01/0x03/0x11/0x17, QL uses 0x4A/0x4B; PT byte 14 = 0x00, QL = 0x3F; PT bytes 24-25 carry tape/text colour, QL leaves them reserved.
  • Unknown enum values map to UNKNOWN rather than raise — keeps the parser robust against firmware quirks and future code values.
  • StatusBlock is a frozen=True, slots=True dataclass — consumers can't accidentally mutate it.

Files

  • backend/pyproject.toml — FastAPI/Pydantic/SQLModel/brother_ql/ptouch/pysnmp deps; ruff strict lint; mypy strict
  • backend/app/services/status_block.py — parser + StatusBlock dataclass + enums
  • backend/tests/unit/services/test_status_block.py — 24 tests
  • backend/tests/conftest.py — pytest config with --hardware skip-by-default flag

Implements the StatusBlockParser per ADR 0006 — decodes the binary
status block that Brother PT-Series and QL-Series printers return
from ESC i S and emit during print jobs.

Key design points:
- One parser handles both PT and QL families (the layout is shared with
  family-specific quirks in media-type codes, reserved bytes, and
  tape/text-colour bytes 24-25 which only PT populates)
- Frozen dataclass output (StatusBlock) — consumers never mutate
- Unknown enum values map to UNKNOWN rather than raising, so unfamiliar
  hardware doesn't crash the parser
- PrinterError IntFlag covers all error bits documented in both Brother
  spec versions (PT v1.02 and QL v1.01)
- Phase number decoded as big-endian (Brother convention: bytes 20+21)
- Convenience properties: is_ready, is_printing

Backend project structure:
- pyproject.toml with FastAPI/Pydantic/SQLModel/pytest/ruff/mypy stack
- pytest.ini config with strict markers, coverage 80% floor
- conftest.py with --hardware skip-by-default flag
- 24 unit tests cover: size validation, PT decoding, QL decoding,
  error flag combinations, phase transitions, unknown value fallback,
  raw bytes preservation, frozen dataclass

References:
- Brother Raster Command Reference v1.02 (PT-E550W/P750W/P710BT)
- Brother Raster Command Reference v1.01 (QL-800/810W/820NWB)
- ADR 0004 — Plugin architecture (consumers of this parser)
- ADR 0006 — Status sources by phase

Refs #11, #19
Copilot AI review requested due to automatic review settings May 10, 2026 18:08
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces the core status monitoring functionality for the label printer hub, enabling the system to decode binary status blocks from Brother printers. It also sets up the foundational backend architecture, ensuring a strict, type-safe development environment for future printer-model plugins.

Highlights

  • Status Block Parser: Implemented a robust 32-byte binary status block parser for Brother PT and QL series printers, supporting various media types, error flags, and status phases.
  • Project Infrastructure: Established the backend project skeleton using a modern Python stack (FastAPI, SQLModel, pytest, ruff, mypy) and configured package layout per project ADRs.
  • Testing Suite: Added 24 comprehensive unit tests covering parsing logic, error flag combinations, and hardware-specific quirks, along with a custom pytest configuration for hardware-in-the-loop testing.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a status block parser for Brother PT and QL series label printers, including comprehensive unit tests and project configuration. The implementation decodes 32-byte binary status messages into structured dataclasses. Feedback focuses on simplifying the StatusBlock dataclass by using IntFlag more idiomatically, refactoring the error decoding logic to use bitwise operations, and removing a forbidden dependency on requests type stubs. Additionally, a test assertion was identified as being too loose regarding enum values.

Comment thread backend/app/services/status_block.py Outdated
Comment on lines +183 to +196
error_flags_1: int
error_flags_2: int
media_width_mm: int
media_type: MediaType
media_length_mm: int
mode: int
status_type: StatusType
phase_type: PhaseType
phase_number: int
notification: NotificationCode
tape_color: TapeColor
text_color: TextColor

errors: list[PrinterError] = field(default_factory=list)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The StatusBlock dataclass can be simplified. The error_flags_1 and error_flags_2 fields are redundant as their information is already captured in the errors field (and the raw bytes are preserved in raw). Furthermore, since PrinterError is an IntFlag, it is more idiomatic and efficient to store it as a single flag value rather than a list of flags. This also ensures true immutability, as a list remains mutable even within a frozen dataclass.

Suggested change
error_flags_1: int
error_flags_2: int
media_width_mm: int
media_type: MediaType
media_length_mm: int
mode: int
status_type: StatusType
phase_type: PhaseType
phase_number: int
notification: NotificationCode
tape_color: TapeColor
text_color: TextColor
errors: list[PrinterError] = field(default_factory=list)
media_width_mm: int
media_type: MediaType
media_length_mm: int
mode: int
status_type: StatusType
phase_type: PhaseType
phase_number: int
notification: NotificationCode
tape_color: TapeColor
text_color: TextColor
errors: PrinterError = PrinterError.NONE

Comment thread backend/app/services/status_block.py Outdated
Comment on lines +221 to +249
def _decode_errors(error_flags_1: int, error_flags_2: int) -> list[PrinterError]:
"""Translate Brother error info 1+2 into a list of distinct flags."""
flags: list[PrinterError] = []
bit_map_1: list[tuple[int, PrinterError]] = [
(0x01, PrinterError.NO_MEDIA),
(0x02, PrinterError.END_OF_MEDIA),
(0x04, PrinterError.CUTTER_JAM),
(0x08, PrinterError.WEAK_BATTERIES),
(0x10, PrinterError.PRINTER_IN_USE),
(0x20, PrinterError.PRINTER_TURNED_OFF),
(0x40, PrinterError.HIGH_VOLTAGE_ADAPTER),
(0x80, PrinterError.FAN_MOTOR_ERROR),
]
bit_map_2: list[tuple[int, PrinterError]] = [
(0x01, PrinterError.REPLACE_MEDIA),
(0x02, PrinterError.EXPANSION_BUFFER_FULL),
(0x04, PrinterError.COMMUNICATION_ERROR),
(0x10, PrinterError.COVER_OPEN),
(0x20, PrinterError.OVERHEATING),
(0x40, PrinterError.MEDIA_CANNOT_BE_FED),
(0x80, PrinterError.SYSTEM_ERROR),
]
for mask, flag in bit_map_1:
if error_flags_1 & mask:
flags.append(flag)
for mask, flag in bit_map_2:
if error_flags_2 & mask:
flags.append(flag)
return flags

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The _decode_errors function is unnecessarily complex. Since the PrinterError IntFlag members are already defined with bit shifts that match the Brother status block layout (where byte 8 is the low byte and byte 9 is the high byte of a 16-bit error field), you can replace the manual bit mapping with a simple bitwise operation or int.from_bytes.

Suggested change
def _decode_errors(error_flags_1: int, error_flags_2: int) -> list[PrinterError]:
"""Translate Brother error info 1+2 into a list of distinct flags."""
flags: list[PrinterError] = []
bit_map_1: list[tuple[int, PrinterError]] = [
(0x01, PrinterError.NO_MEDIA),
(0x02, PrinterError.END_OF_MEDIA),
(0x04, PrinterError.CUTTER_JAM),
(0x08, PrinterError.WEAK_BATTERIES),
(0x10, PrinterError.PRINTER_IN_USE),
(0x20, PrinterError.PRINTER_TURNED_OFF),
(0x40, PrinterError.HIGH_VOLTAGE_ADAPTER),
(0x80, PrinterError.FAN_MOTOR_ERROR),
]
bit_map_2: list[tuple[int, PrinterError]] = [
(0x01, PrinterError.REPLACE_MEDIA),
(0x02, PrinterError.EXPANSION_BUFFER_FULL),
(0x04, PrinterError.COMMUNICATION_ERROR),
(0x10, PrinterError.COVER_OPEN),
(0x20, PrinterError.OVERHEATING),
(0x40, PrinterError.MEDIA_CANNOT_BE_FED),
(0x80, PrinterError.SYSTEM_ERROR),
]
for mask, flag in bit_map_1:
if error_flags_1 & mask:
flags.append(flag)
for mask, flag in bit_map_2:
if error_flags_2 & mask:
flags.append(flag)
return flags
def _decode_errors(error_flags_1: int, error_flags_2: int) -> PrinterError:
"""Translate Brother error info 1+2 into a single IntFlag."""
return PrinterError((error_flags_2 << 8) | error_flags_1)

Comment thread backend/pyproject.toml Outdated
"respx>=0.21",
"ruff>=0.8",
"mypy>=1.13",
"types-requests",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The types-requests dependency should be removed. The repository style guide explicitly forbids the use of the requests library in favor of httpx (see 'Things to flag', line 30). Including type stubs for a forbidden library is inconsistent and adds unnecessary bloat to the dev dependencies.

References
  1. The style guide forbids the use of the requests library, recommending httpx instead. (link)

def test_qlseries_has_no_tape_colour(self) -> None:
sb = StatusBlockParser.parse(QL_READY_12MM_CONTINUOUS)
# QL doesn't populate these — bytes should land on zero/unknown
assert sb.tape_color in (TapeColor.UNKNOWN, TapeColor.WHITE) or sb.tape_color is None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The assertion in test_qlseries_has_no_tape_colour is too loose and contains an impossible condition. sb.tape_color is an IntEnum and will never be None. Since the QL-Series does not populate this byte, it should consistently map to TapeColor.UNKNOWN (0x00).

Suggested change
assert sb.tape_color in (TapeColor.UNKNOWN, TapeColor.WHITE) or sb.tape_color is None
assert sb.tape_color == TapeColor.UNKNOWN

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces the backend’s initial Python project skeleton and adds a Brother PT/QL 32-byte status block parser (ESC i S) with unit tests, aligning with ADR 0006’s “status sources by phase”.

Changes:

  • Added StatusBlockParser + StatusBlock dataclass and related enums/flags for decoding Brother 32-byte status blocks.
  • Added pytest configuration (including --hardware opt-in skipping) and a unit test suite covering parsing behaviors and edge cases.
  • Added backend packaging/tooling configuration in pyproject.toml (FastAPI/SQLModel stack, ruff, mypy strict, coverage).

Reviewed changes

Copilot reviewed 4 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
backend/app/services/status_block.py New status-block parser, enums, error decoding, and StatusBlock dataclass.
backend/tests/unit/services/test_status_block.py Unit tests for parsing PT/QL fixtures, errors, phases, and unknown-enum fallbacks.
backend/tests/conftest.py Pytest --hardware flag and default skip behavior for hardware tests.
backend/pyproject.toml Backend project dependencies and tooling configuration (ruff/mypy/pytest/coverage).
.gitignore Ignores backend virtualenv directory.

Comment thread backend/app/services/status_block.py Outdated
Comment on lines +196 to +197
errors: list[PrinterError] = field(default_factory=list)

Comment on lines +170 to +171
# QL doesn't populate these — bytes should land on zero/unknown
assert sb.tape_color in (TapeColor.UNKNOWN, TapeColor.WHITE) or sb.tape_color is None
Comment on lines +273 to +276
def test_dataclass_is_frozen(self) -> None:
sb = StatusBlockParser.parse(PT_READY_12MM_WHITE_BLACK)
with pytest.raises((AttributeError, Exception)): # FrozenInstanceError or similar
sb.media_width_mm = 99 # type: ignore[misc]
Comment thread backend/app/services/status_block.py Outdated
Comment on lines +221 to +246
def _decode_errors(error_flags_1: int, error_flags_2: int) -> list[PrinterError]:
"""Translate Brother error info 1+2 into a list of distinct flags."""
flags: list[PrinterError] = []
bit_map_1: list[tuple[int, PrinterError]] = [
(0x01, PrinterError.NO_MEDIA),
(0x02, PrinterError.END_OF_MEDIA),
(0x04, PrinterError.CUTTER_JAM),
(0x08, PrinterError.WEAK_BATTERIES),
(0x10, PrinterError.PRINTER_IN_USE),
(0x20, PrinterError.PRINTER_TURNED_OFF),
(0x40, PrinterError.HIGH_VOLTAGE_ADAPTER),
(0x80, PrinterError.FAN_MOTOR_ERROR),
]
bit_map_2: list[tuple[int, PrinterError]] = [
(0x01, PrinterError.REPLACE_MEDIA),
(0x02, PrinterError.EXPANSION_BUFFER_FULL),
(0x04, PrinterError.COMMUNICATION_ERROR),
(0x10, PrinterError.COVER_OPEN),
(0x20, PrinterError.OVERHEATING),
(0x40, PrinterError.MEDIA_CANNOT_BE_FED),
(0x80, PrinterError.SYSTEM_ERROR),
]
for mask, flag in bit_map_1:
if error_flags_1 & mask:
flags.append(flag)
for mask, flag in bit_map_2:
Both reviewers caught the same set of issues — addressing them in one
follow-up commit on the feature branch:

Gemini findings (all addressed):
- StatusBlock.errors changed from list[PrinterError] (mutable, breaks
  frozen=True guarantee) to a single PrinterError IntFlag value. Use
  bitwise membership: 'PrinterError.NO_MEDIA in sb.errors' still works
  because Python IntFlag supports it natively.
- error_flags_1 / error_flags_2 fields removed — redundant when raw
  bytes are preserved in .raw[8] / .raw[9].
- _decode_errors() collapsed to a one-liner using IntFlag's CONFORM
  boundary: combined = info1 | (info2 << 8); PrinterError(combined).
  No more bit_map allocations on each parse.
- types-requests removed from dev dependencies — the project uses httpx
  exclusively (per CLAUDE.md and .gemini/styleguide.md).

Copilot findings (all addressed):
- frozen=True now actually frozen (no list field). FrozenInstanceError
  is raised on attribute mutation.
- test_qlseries_has_no_tape_colour assertion tightened to
  '== TapeColor.UNKNOWN' (was permissive 'in (UNKNOWN, WHITE) or None').
- test_dataclass_is_frozen catches the specific
  dataclasses.FrozenInstanceError instead of bare Exception.
- bit_map_1 / bit_map_2 module-level constants no longer needed —
  the IntFlag bitwise operation replaces them entirely.

Test count: 24 → 25 (added test_errors_field_is_immutable_intflag to
guard against regression to mutable list).
@strausmann

Copy link
Copy Markdown
Owner Author

Thanks both — every finding addressed in .

Gemini:

  1. error_flags_1 / error_flags_2 removed — redundant when raw bytes are preserved in .raw[8] / .raw[9]
  2. errors is now a single PrinterError IntFlag (was: list[PrinterError] which broke frozen=True)
  3. _decode_errors collapsed to a one-liner: PrinterError(info1 | (info2 << 8)) — IntFlag's default CONFORM boundary handles unknown bits
  4. types-requests removed from dev deps (project uses httpx exclusively)
  5. test_qlseries_has_no_tape_colour tightened to == TapeColor.UNKNOWN

Copilot:

  1. StatusBlock truly immutable now (no list field)
  2. ✅ Same as Gemini feat(pwa): browser notifications for job/status events (Web Notifications API + Web Push) #5 (tape colour assertion)
  3. test_dataclass_is_frozen catches specific dataclasses.FrozenInstanceError instead of broad Exception
  4. _decode_errors no longer allocates per-parse — single bitwise op replaces both bit-maps

Added one new test (test_errors_field_is_immutable_intflag) to guard against regression to a mutable list. 25/25 green locally.

@gemini-code-assist please re-review.

@strausmann
strausmann merged commit ced0ff8 into main May 10, 2026
9 checks passed
@strausmann
strausmann deleted the feat/status-block-parser branch May 10, 2026 18:23

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a status block parser for Brother PT/QL series printers, including comprehensive enums and unit tests, and sets up the backend project structure. Feedback focuses on ensuring PrinterError flags correctly mask unknown bits by explicitly setting the CONFORM boundary for IntFlag in Python 3.11+. Additionally, a request was made to justify new dependencies in the project configuration as per the repository's style guide.

INCOMPATIBLE = 0xFF


class PrinterError(IntFlag):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In Python 3.11+, the default boundary for IntFlag is KEEP, which preserves unknown bits. This contradicts the docstring for _decode_errors (line 231) and could cause is_ready to return False if the printer returns reserved bits in the error bytes. Explicitly setting boundary=FlagBoundary.CONFORM will mask off unknown bits during instantiation, matching the intended behavior.

Suggested change
class PrinterError(IntFlag):
class PrinterError(IntFlag, boundary=FlagBoundary.CONFORM):

from __future__ import annotations

from dataclasses import dataclass
from enum import IntEnum, IntFlag

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import FlagBoundary to enable the CONFORM behavior for the PrinterError enum, ensuring that unknown bits are handled as described in the documentation.

Suggested change
from enum import IntEnum, IntFlag
from enum import FlagBoundary, IntEnum, IntFlag

to the low byte (bits 0-7) and error info 2 maps to the high byte
(bits 8-15). Combining them is therefore a single bitwise operation.

Unknown bits are silently dropped — Python ``IntFlag`` (default

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the docstring to reflect that CONFORM is being explicitly set on the PrinterError class, as it is not the default behavior in Python 3.11+.

Suggested change
Unknown bits are silently droppedPython ``IntFlag`` (default
Unknown bits are silently droppedPython ``IntFlag`` (with ``CONFORM`` boundary) masks them off automatically.

Comment thread backend/pyproject.toml
Comment on lines +20 to +35
dependencies = [
"fastapi>=0.115",
"uvicorn[standard]>=0.32",
"pydantic>=2.9",
"pydantic-settings>=2.6",
"sqlmodel>=0.0.22",
"aiosqlite>=0.20",
"jinja2>=3.1",
"python-multipart>=0.0.12",
"httpx>=0.28",
"pillow>=11.0",
"qrcode[pil]>=8.0",
"brother-ql>=0.9.4",
"ptouch>=1.1.0",
"pysnmp>=6.2",
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The PR description mentions the FastAPI/SQLModel stack but does not provide a rationale for several new dependencies, including brother-ql, ptouch, pillow, and qrcode. Per the repository style guide (rule 31), new dependencies should be justified in the PR description.

References
  1. New dependencies without rationale in PR description should be flagged. (link)

github-actions Bot pushed a commit that referenced this pull request May 10, 2026
## 1.0.0 (2026-05-10)

* ci: make Python lint/test job branch-tolerant for dependabot PRs (#30) ([6ee8e16](6ee8e16)), closes [#30](#30) [2-#9](https://github.com/2-/issues/9) [#2](#2) [#3](#3) [#4](#4) [#6](#6) [#7](#7) [#8](#8) [#9](#9)
* ci(deps): bump the actions-all group across 1 directory with 14 updates (#9) ([a88a027](a88a027)), closes [#9](#9)
* chore: initial repository skeleton ([0319e7e](0319e7e))
* chore(deps-dev): bump @commitlint/cli from 19.8.1 to 21.0.0 (#3) ([2642b26](2642b26)), closes [#3](#3)
* chore(deps-dev): bump @commitlint/config-conventional (#4) ([44be5f9](44be5f9)), closes [#4](#4)
* chore(deps-dev): bump @semantic-release/exec from 6.0.3 to 7.1.0 (#2) ([a8097b0](a8097b0)), closes [#2](#2)
* chore(deps-dev): bump @semantic-release/github from 11.0.6 to 12.0.8 (#8) ([098e766](098e766)), closes [#8](#8)
* chore(deps-dev): bump conventional-changelog-conventionalcommits (#7) ([9f5370f](9f5370f)), closes [#7](#7)
* chore(deps-dev): bump semantic-release from 24.2.9 to 25.0.3 (#6) ([306010f](306010f)), closes [#6](#6)
* feat(status): add Brother 32-byte status block parser (#29) ([ced0ff8](ced0ff8)), closes [#29](#29) [#11](#11) [#19](#19) [#29](#29)
* docs: refactor — ADRs in docs/decisions/, policies in docs/policies/, slim CLAUDE.md ([51c2cf1](51c2cf1)), closes [#1](#1)
* docs(decisions): ADR 0006 — PT vs QL ESC i S behaviour from Phase-0 hardware test ([0d12c63](0d12c63)), closes [#12](#12) [#11](#11)
* docs(decisions): ADR 0012 — layout management; clarify integration push/pull capabilities ([effcbdf](effcbdf)), closes [#17](#17) [#19](#19)
* docs(decisions): ADR 0013 + cart UI spec + AI-review workflow ([c72dc85](c72dc85)), closes [#26](#26) [#27](#27) [#28](#28)
* docs(docker): document image tag scheme (latest, 1.0.0, 1.0, 1) ([1f72396](1f72396))
* docs(examples): sample compose files for standalone/Traefik/Pangolin/Caddy ([a2f6f3d](a2f6f3d))

[skip ci]
strausmann added a commit that referenced this pull request May 10, 2026
…TRIBUTING/AI configs

Captures recurring AI-reviewer findings (Gemini Code Assist + Copilot) so
contributors and AI tools alike can avoid the same mistakes in the next PR.

Initial entries from PR #29 (StatusBlockParser), PR #30 (CI cache fix),
and PR #32 (release trigger):

Python:
- frozen=True dataclasses must not have mutable fields (list, dict, set)
- IntFlag is the idiomatic type for combined error/flag states
- IntFlag(combined_int) replaces manual bit-mapping loops
- Don't include type stubs for forbidden libraries (e.g. types-requests
  when the project uses httpx exclusively)
- Test assertions should be tight (== UNKNOWN, not 'in (A, B) or None')
- Catch specific exception types (FrozenInstanceError) not bare Exception

GitHub Actions:
- workflow_dispatch input names: no hyphens (parsed as subtraction)
- workflow_dispatch jobs publishing must have branch guards
- semantic-release evaluates full history since last tag, not just
  'today's' commits — be precise when describing skip behaviour

Process:
- Wait for AI reviewers (~1-2 min) even on small PRs; --admin merge
  before review forfeits the value
- Side-effects must be in PR description; reviewers shouldn't have to
  discover them via line-by-line diff reading

Cross-references added in:
- CLAUDE.md (point #6 of 'Read these first')
- CONTRIBUTING.md (after the §7 Code review section)
- .github/copilot-instructions.md (new 'Repeated patterns' section)
- .gemini/styleguide.md ('Recurring review patterns' section, asks
  Gemini to suggest updates to the file rather than re-flagging the
  same finding)

The file is meant to grow: when an AI review surfaces a new recurring
pattern, the rule is to add it in the same follow-up commit.
strausmann added a commit that referenced this pull request May 10, 2026
…TRIBUTING/AI configs (#33)

Captures recurring AI-reviewer findings (Gemini Code Assist + Copilot) so
contributors and AI tools alike can avoid the same mistakes in the next PR.

Initial entries from PR #29 (StatusBlockParser), PR #30 (CI cache fix),
and PR #32 (release trigger):

Python:
- frozen=True dataclasses must not have mutable fields (list, dict, set)
- IntFlag is the idiomatic type for combined error/flag states
- IntFlag(combined_int) replaces manual bit-mapping loops
- Don't include type stubs for forbidden libraries (e.g. types-requests
  when the project uses httpx exclusively)
- Test assertions should be tight (== UNKNOWN, not 'in (A, B) or None')
- Catch specific exception types (FrozenInstanceError) not bare Exception

GitHub Actions:
- workflow_dispatch input names: no hyphens (parsed as subtraction)
- workflow_dispatch jobs publishing must have branch guards
- semantic-release evaluates full history since last tag, not just
  'today's' commits — be precise when describing skip behaviour

Process:
- Wait for AI reviewers (~1-2 min) even on small PRs; --admin merge
  before review forfeits the value
- Side-effects must be in PR description; reviewers shouldn't have to
  discover them via line-by-line diff reading

Cross-references added in:
- CLAUDE.md (point #6 of 'Read these first')
- CONTRIBUTING.md (after the §7 Code review section)
- .github/copilot-instructions.md (new 'Repeated patterns' section)
- .gemini/styleguide.md ('Recurring review patterns' section, asks
  Gemini to suggest updates to the file rather than re-flagging the
  same finding)

The file is meant to grow: when an AI review surfaces a new recurring
pattern, the rule is to add it in the same follow-up commit.
github-actions Bot pushed a commit that referenced this pull request May 10, 2026
## 0.1.0 (2026-05-10)

* docs: refactor — ADRs in docs/decisions/, policies in docs/policies/, slim CLAUDE.md ([51c2cf1](51c2cf1)), closes [#1](#1)
* docs(ci): document mypy as hard gate; clarify CI gate policy in CONTRIBUTING (#31) ([e1e6f18](e1e6f18)), closes [#31](#31) [#30](#30) [#30](#30) [#30](#30)
* docs(decisions): ADR 0006 — PT vs QL ESC i S behaviour from Phase-0 hardware test ([0d12c63](0d12c63)), closes [#12](#12) [#11](#11)
* docs(decisions): ADR 0012 — layout management; clarify integration push/pull capabilities ([effcbdf](effcbdf)), closes [#17](#17) [#19](#19)
* docs(decisions): ADR 0013 + cart UI spec + AI-review workflow ([c72dc85](c72dc85)), closes [#26](#26) [#27](#27) [#28](#28)
* docs(docker): document image tag scheme (latest, 1.0.0, 1.0, 1) ([1f72396](1f72396))
* docs(examples): sample compose files for standalone/Traefik/Pangolin/Caddy ([a2f6f3d](a2f6f3d))
* docs(learnings): add code-review-patterns + reference from CLAUDE/CONTRIBUTING/AI configs (#33) ([0fc61d2](0fc61d2)), closes [#33](#33) [#29](#29) [#30](#30) [#32](#32) [#6](#6)
* ci: make Python lint/test job branch-tolerant for dependabot PRs (#30) ([6ee8e16](6ee8e16)), closes [#30](#30) [2-#9](https://github.com/2-/issues/9) [#2](#2) [#3](#3) [#4](#4) [#6](#6) [#7](#7) [#8](#8) [#9](#9)
* ci(deps): bump the actions-all group across 1 directory with 14 updates (#9) ([a88a027](a88a027)), closes [#9](#9)
* ci(release): trigger releases via cron + workflow_dispatch only (#32) ([9941958](9941958)), closes [#32](#32) [#32](#32) [#32](#32)
* chore(deps-dev): bump @commitlint/cli from 19.8.1 to 21.0.0 (#3) ([2642b26](2642b26)), closes [#3](#3)
* chore(deps-dev): bump @commitlint/config-conventional (#4) ([44be5f9](44be5f9)), closes [#4](#4)
* chore(deps-dev): bump @semantic-release/exec from 6.0.3 to 7.1.0 (#2) ([a8097b0](a8097b0)), closes [#2](#2)
* chore(deps-dev): bump @semantic-release/github from 11.0.6 to 12.0.8 (#8) ([098e766](098e766)), closes [#8](#8)
* chore(deps-dev): bump conventional-changelog-conventionalcommits (#7) ([9f5370f](9f5370f)), closes [#7](#7)
* chore(deps-dev): bump semantic-release from 24.2.9 to 25.0.3 (#6) ([306010f](306010f)), closes [#6](#6)
* chore(release): 1.0.0 ([15abeb3](15abeb3))
* feat(status): add Brother 32-byte status block parser (#29) ([ced0ff8](ced0ff8)), closes [#29](#29) [#11](#11) [#19](#19) [#29](#29)

[skip ci]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants