Skip to content

Add semantic instruction browser - #20

Merged
hbmartin merged 3 commits into
mainfrom
pyldraw-updates
Aug 2, 2026
Merged

Add semantic instruction browser#20
hbmartin merged 3 commits into
mainfrom
pyldraw-updates

Conversation

@hbmartin

@hbmartin hbmartin commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • add Whole model / Instructions navigation with independent reachable MPD section and step selection
  • expose cumulative geometry, LPub-aware PLI/BOM, rotation, camera, callout, and directive state
  • combine LDraw and instruction diagnostics with stable section and code fields
  • upgrade the pyldraw3 dependency floor to 1.5.0 and the package version to 0.3.0

User impact

Users can inspect renderer-neutral building instructions without changing existing whole-model browsing behavior. Embedded .dat dependencies remain inventory items, and unsupported directives remain raw and lossless.

Validation

  • uv run pytest (75 passed)
  • uv run ruff check .
  • uv run ruff format --check src tests
  • uv run ty check src tests
  • uv run pyrefly check src tests
  • visually inspected updated 120x40 SVG snapshots

Summary by cubic

Add a semantic instruction browser to the Model tab so you can navigate section-local steps and view renderer-neutral instruction state (rotation, camera, LPub PLI/BOM, callouts, directives). Diagnostics now combine LDraw and instruction issues, and the app updates to 0.3.0 with pyldraw3>=1.5.0.

  • New Features

    • Instructions mode with step selection in the Model view.
    • Show effective step state: rotation, camera, layout, callouts.
    • LPub-aware PLI/BOM and per-step cumulative geometry.
    • New directives table and unified issues table (section, line, code, message).
    • Piece and stats panels reflect step-scoped occurrences.
    • BOM export copies the currently displayed BOM.
  • Dependencies

    • Upgrade to pyldraw3>=1.5.0.
    • Bump package version to 0.3.0.
    • Update Ruff config to ignore CPY001 (copyright rule).

Written for commit 5f31a3e. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added instruction-based model viewing with section and step navigation.
    • Added step details, cumulative and per-step parts lists, statistics, bills of materials, and directives.
    • Added instruction-specific keyboard shortcuts and model-tab help.
    • Enhanced issue views with sections, line numbers, severity, codes, and messages.
  • Documentation

    • Updated documentation for version 0.3.0, instruction navigation, diagnostics, directives, and read-only behavior.
  • Bug Fixes

    • BOM copying now uses the currently displayed bill of materials.
    • Model and instruction state resets correctly when loading another model.

Model: GPT-5
Thread: 019fc2b0-5c19-75d0-bb00-4f97e318e787
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@socket-security

socket-security Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedpypi/​pyldraw3@​1.3.0 ⏵ 1.5.097 -110010010070
Updatedpypi/​ty@​0.0.64 ⏵ 0.0.65100 +1100100100100
Updatedpypi/​pyrefly@​1.1.1 ⏵ 1.2.0100 +1100100100100
Updatedpypi/​ruff@​0.16.0 ⏵ 0.16.1100 +1100100100100

View full report

@socket-security

socket-security Bot commented Aug 2, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn Medium
Low adoption: pypi pyldraw3

Location: Package overview

From: pyproject.tomlpypi/[email protected]

ℹ Read more on: This package | This alert | What are unpopular packages?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Unpopular packages may have less maintenance and contain other problems.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore pypi/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds instruction-mode model viewing to ModelView. It supports section-local step navigation, cumulative occurrences, PLI and BOM rendering, directive display, instruction details, and semantic issue reporting. New widgets render instruction data and expanded issue metadata. BOM and statistics rendering now accept explicit displayed state. Tests add instruction fixtures, interaction coverage, BOM export checks, reset behavior, and snapshots. The release version becomes 0.3.0.

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant ModelView
  participant InstructionDocument
  participant DisplayWidgets
  Operator->>ModelView: Select Instructions and a section-local step
  ModelView->>InstructionDocument: Resolve cumulative step state
  InstructionDocument-->>ModelView: Occurrences, BOM data, directives, and details
  ModelView->>DisplayWidgets: Update pieces, statistics, BOM, directives, and details
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a semantic instruction browser.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pyldraw-updates

Comment @coderabbitai help to get the list of available commands.

Model: GPT-5
Thread: 019fc2b0-5c19-75d0-bb00-4f97e318e787
Model: GPT-5
Thread: 019fc2b0-5c19-75d0-bb00-4f97e318e787
@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.59664% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.16%. Comparing base (f7ac01c) to head (5f31a3e).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
src/pyldraw3_tui/screens/model.py 91.08% 14 Missing ⚠️
src/pyldraw3_tui/app.py 33.33% 2 Missing ⚠️
src/pyldraw3_tui/commands.py 0.00% 2 Missing ⚠️
src/pyldraw3_tui/widgets/instruction_details.py 95.91% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #20      +/-   ##
==========================================
+ Coverage   86.09%   87.16%   +1.07%     
==========================================
  Files          28       30       +2     
  Lines        1215     1441     +226     
==========================================
+ Hits         1046     1256     +210     
- Misses        169      185      +16     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pyldraw3_tui/widgets/directives_table.py (1)

1-55: 📐 Maintainability & Code Quality | 🔴 Critical

Fix the Ruff CPY001 pipeline failure before merge.

The pipeline logs report uv run ruff check . failing with Ruff CPY001 — missing copyright notices in 21-22 Python files, including new files like this one. This blocks CI (exit code 1). Add the required copyright header to this file (and the other new/changed files) or adjust the Ruff flake8-copyright configuration to match project convention before merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pyldraw3_tui/widgets/directives_table.py` around lines 1 - 55, Add the
project-standard copyright header to the module containing DirectivesTable, and
apply the same header consistently to the other new or changed Python files
reported by Ruff CPY001. Preserve the existing flake8-copyright configuration
unless the repository convention requires updating it, and ensure uv run ruff
check . passes.

Source: Pipeline failures

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pyldraw3_tui/app.py`:
- Around line 204-205: Update the query_one calls in the model-tab focus logic
in src/pyldraw3_tui/app.py (lines 204-205) to pass PieceTable using the named
expect_type argument. Apply the same change in tests/test_snapshots.py (lines
91-92), passing Select and TabbedContent via expect_type; no other behavior
should change.

In `@src/pyldraw3_tui/screens/model.py`:
- Around line 261-275: Update _show_error to reset _view_mode to the default
model mode, reset `#view-mode-select` to the matching value, and call
_sync_mode_ui() after clearing the model state. Mirror the existing reset
behavior from load_model’s success path so the instructions CSS class and
section/step selectors are removed when loading fails.

In `@src/pyldraw3_tui/widgets/instruction_details.py`:
- Line 1: Add the project-required copyright header at the top of
instruction_details.py, before the existing module docstring, using the
repository’s established Ruff-compatible copyright notice format. Leave the
module content unchanged.

In `@src/pyldraw3_tui/widgets/stats_panel.py`:
- Around line 57-61: The whole-model render computes occurrences twice. In
src/pyldraw3_tui/widgets/stats_panel.py#L57-L61, update StatsPanel.show_model to
accept precomputed Sequence[ModelOccurrence] data or remove it in favor of
show_occurrences; in src/pyldraw3_tui/screens/model.py#L297-L317, update
ModelView._render_whole_model to pass its existing occurrences to
StatsPanel.show_occurrences with self._parts and steps=len(steps), preserving
the existing _render_instruction_step behavior.

In `@tests/test_snapshots.py`:
- Around line 86-94: Add type annotations to the
test_snapshot_model_instructions function parameters make_app and
instructions_mpd using the appropriate existing fixture types, and annotate the
function’s return type as None. Keep the test behavior unchanged.

---

Outside diff comments:
In `@src/pyldraw3_tui/widgets/directives_table.py`:
- Around line 1-55: Add the project-standard copyright header to the module
containing DirectivesTable, and apply the same header consistently to the other
new or changed Python files reported by Ruff CPY001. Preserve the existing
flake8-copyright configuration unless the repository convention requires
updating it, and ensure uv run ruff check . passes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 879325f1-51ba-4746-8d3b-43a4f2e34f0f

📥 Commits

Reviewing files that changed from the base of the PR and between af2f472 and 632912f.

⛔ Files ignored due to path filters (5)
  • tests/__snapshots__/model_bom.svg is excluded by !**/*.svg
  • tests/__snapshots__/model_instructions.svg is excluded by !**/*.svg
  • tests/__snapshots__/model_issues.svg is excluded by !**/*.svg
  • tests/__snapshots__/model_pieces.svg is excluded by !**/*.svg
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • README.md
  • pyproject.toml
  • src/pyldraw3_tui/app.py
  • src/pyldraw3_tui/commands.py
  • src/pyldraw3_tui/screens/model.py
  • src/pyldraw3_tui/widgets/bom_table.py
  • src/pyldraw3_tui/widgets/directives_table.py
  • src/pyldraw3_tui/widgets/instruction_details.py
  • src/pyldraw3_tui/widgets/issues_table.py
  • src/pyldraw3_tui/widgets/piece_table.py
  • src/pyldraw3_tui/widgets/stats_panel.py
  • tests/conftest.py
  • tests/fixtures/models/instructions.mpd
  • tests/test_pilot_model.py
  • tests/test_snapshots.py

Comment thread src/pyldraw3_tui/app.py
Comment on lines +204 to +205
if tab == "model":
self.query_one("#piece-table", PieceTable).focus()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Pass query_one expected types by name.

Textual names the type-selector parameter expect_type. (textual.textualize.io)

  • src/pyldraw3_tui/app.py#L204-L205: use expect_type=PieceTable.
  • tests/test_snapshots.py#L91-L92: use expect_type=Select and expect_type=TabbedContent.

As per coding guidelines, “Prefer named arguments when calling a method with more than one argument”.

📍 Affects 2 files
  • src/pyldraw3_tui/app.py#L204-L205 (this comment)
  • tests/test_snapshots.py#L91-L92
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pyldraw3_tui/app.py` around lines 204 - 205, Update the query_one calls
in the model-tab focus logic in src/pyldraw3_tui/app.py (lines 204-205) to pass
PieceTable using the named expect_type argument. Apply the same change in
tests/test_snapshots.py (lines 91-92), passing Select and TabbedContent via
expect_type; no other behavior should change.

Source: Coding guidelines

Comment on lines 261 to +275
def _show_error(self, message: str) -> None:
self._model = None
self._instruction_document = None
self._selected_instruction_section = None
self._selected_instruction_step = 1
self.add_class("errored")
self.query_one("#model-error", Static).update(f"[bold red]Error:[/] {message}")
self.query_one("#model-title", Static).update("No model open")
self.query_one("#piece-table", PieceTable).set_occurrences([], self._parts)
self.query_one("#stats-panel", StatsPanel).update("Model has no pieces.")
self.query_one("#instruction-details", InstructionDetails).update("")
self.query_one("#pli-table", BomTable).set_rows([], self._parts)
self.query_one("#bom-table", BomTable).set_rows([], self._parts)
self.query_one("#directives-table", DirectivesTable).set_directives([])
self._clear_instruction_selectors()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset the view mode when a model load fails.

_show_error clears the model and instruction state, but it does not reset self._view_mode, the #view-mode-select value, or call self._sync_mode_ui(). If the user was in instructions mode when a subsequent load fails, the widget keeps the instructions CSS class and the mode selector still shows "Instructions" and the section/step selectors, even though there is no model data. Reset the view mode the same way load_model's success path does.

🐛 Proposed fix to reset the view mode on error
     def _show_error(self, message: str) -> None:
         self._model = None
         self._instruction_document = None
         self._selected_instruction_section = None
         self._selected_instruction_step = 1
+        self._view_mode = MODEL_MODE
         self.add_class("errored")
         self.query_one("`#model-error`", Static).update(f"[bold red]Error:[/] {message}")
         self.query_one("`#model-title`", Static).update("No model open")
         self.query_one("`#piece-table`", PieceTable).set_occurrences([], self._parts)
         self.query_one("`#stats-panel`", StatsPanel).update("Model has no pieces.")
         self.query_one("`#instruction-details`", InstructionDetails).update("")
         self.query_one("`#pli-table`", BomTable).set_rows([], self._parts)
         self.query_one("`#bom-table`", BomTable).set_rows([], self._parts)
         self.query_one("`#directives-table`", DirectivesTable).set_directives([])
         self._clear_instruction_selectors()
+        mode_select = self.query_one("`#view-mode-select`", Select)
+        with mode_select.prevent(Select.Changed):
+            mode_select.value = MODEL_MODE
+        self._sync_mode_ui()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _show_error(self, message: str) -> None:
self._model = None
self._instruction_document = None
self._selected_instruction_section = None
self._selected_instruction_step = 1
self.add_class("errored")
self.query_one("#model-error", Static).update(f"[bold red]Error:[/] {message}")
self.query_one("#model-title", Static).update("No model open")
self.query_one("#piece-table", PieceTable).set_occurrences([], self._parts)
self.query_one("#stats-panel", StatsPanel).update("Model has no pieces.")
self.query_one("#instruction-details", InstructionDetails).update("")
self.query_one("#pli-table", BomTable).set_rows([], self._parts)
self.query_one("#bom-table", BomTable).set_rows([], self._parts)
self.query_one("#directives-table", DirectivesTable).set_directives([])
self._clear_instruction_selectors()
def _show_error(self, message: str) -> None:
self._model = None
self._instruction_document = None
self._selected_instruction_section = None
self._selected_instruction_step = 1
self._view_mode = MODEL_MODE
self.add_class("errored")
self.query_one("`#model-error`", Static).update(f"[bold red]Error:[/] {message}")
self.query_one("`#model-title`", Static).update("No model open")
self.query_one("`#piece-table`", PieceTable).set_occurrences([], self._parts)
self.query_one("`#stats-panel`", StatsPanel).update("Model has no pieces.")
self.query_one("`#instruction-details`", InstructionDetails).update("")
self.query_one("`#pli-table`", BomTable).set_rows([], self._parts)
self.query_one("`#bom-table`", BomTable).set_rows([], self._parts)
self.query_one("`#directives-table`", DirectivesTable).set_directives([])
self._clear_instruction_selectors()
mode_select = self.query_one("`#view-mode-select`", Select)
with mode_select.prevent(Select.Changed):
mode_select.value = MODEL_MODE
self._sync_mode_ui()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pyldraw3_tui/screens/model.py` around lines 261 - 275, Update _show_error
to reset _view_mode to the default model mode, reset `#view-mode-select` to the
matching value, and call _sync_mode_ui() after clearing the model state. Mirror
the existing reset behavior from load_model’s success path so the instructions
CSS class and section/step selectors are removed when loading fails.

@@ -0,0 +1,85 @@
"""Semantic state for a selected instruction step."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add the required copyright notice.

Ruff reports CPY001 for package files. The lint job cannot pass until this file has the required header.

As per coding guidelines, use “Ruff formatter and linter style conventions”.

🧰 Tools
🪛 GitHub Actions: Lint and Test / 2_lint-test (3.13).txt

[error] 1-1: Ruff CPY001: Missing copyright notices in 21 Python files under this package. Command 'uv run ruff check .' reported 46 total errors.

🪛 GitHub Actions: Lint and Test / lint-test (3.13)

[error] 1-1: Ruff CPY001: 22 Python files under this directory are missing copyright notices at the top of the file. Command 'uv run ruff check .' failed with exit code 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pyldraw3_tui/widgets/instruction_details.py` at line 1, Add the
project-required copyright header at the top of instruction_details.py, before
the existing module docstring, using the repository’s established
Ruff-compatible copyright notice format. Leave the module content unchanged.

Sources: Coding guidelines, Pipeline failures

Comment on lines +57 to +61
self.show_occurrences(
tuple(model.iter_occurrences()),
parts,
steps=steps,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid computing model occurrences twice per whole-model render. StatsPanel.show_model re-traverses the model via its own model.iter_occurrences() call, even though ModelView._render_whole_model already computed an equivalent occurrences list moments earlier. include_steps only changes whether occurrence.step is populated, not occurrence identity or count, so the second traversal is pure duplicated work on every whole-model render (submodel switch, catalog-ready, mode toggle).

  • src/pyldraw3_tui/widgets/stats_panel.py#L57-L61: change show_model to accept a pre-computed Sequence[ModelOccurrence] (or drop it and let callers use show_occurrences directly, as _render_instruction_step already does).
  • src/pyldraw3_tui/screens/model.py#L297-L317: update _render_whole_model to call self.query_one("#stats-panel", StatsPanel).show_occurrences(occurrences, self._parts, steps=len(steps)) with the already-computed occurrences, instead of calling show_model(model, self._parts, steps=len(steps)).
📍 Affects 2 files
  • src/pyldraw3_tui/widgets/stats_panel.py#L57-L61 (this comment)
  • src/pyldraw3_tui/screens/model.py#L297-L317
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pyldraw3_tui/widgets/stats_panel.py` around lines 57 - 61, The
whole-model render computes occurrences twice. In
src/pyldraw3_tui/widgets/stats_panel.py#L57-L61, update StatsPanel.show_model to
accept precomputed Sequence[ModelOccurrence] data or remove it in favor of
show_occurrences; in src/pyldraw3_tui/screens/model.py#L297-L317, update
ModelView._render_whole_model to pass its existing occurrences to
StatsPanel.show_occurrences with self._parts and steps=len(steps), preserving
the existing _render_instruction_step behavior.

Comment thread tests/test_snapshots.py
Comment on lines +86 to +94
async def test_snapshot_model_instructions(make_app, instructions_mpd):
app = make_app(model_path=instructions_mpd)
async with app.run_test(size=SIZE) as pilot:
await wait_for_catalog(app, pilot)
await pilot.press("i")
app.query_one("#instruction-step-select", Select).value = 3
app.query_one("#model-tabs", TabbedContent).active = "tab-summary"
await pilot.pause()
_check(app, "model_instructions")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add type hints to the new test.

Annotate make_app, instructions_mpd, and the None return value.

As per coding guidelines, “Always use or add type hints”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_snapshots.py` around lines 86 - 94, Add type annotations to the
test_snapshot_model_instructions function parameters make_app and
instructions_mpd using the appropriate existing fixture types, and annotate the
function’s return type as None. Keep the test behavior unchanged.

Source: Coding guidelines

@hbmartin
hbmartin merged commit 853faae into main Aug 2, 2026
12 checks passed
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.

1 participant