feat(mcp): build_bom_modify_ui — per-row diff-decorated BOM modify card (closes #811)#833
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new modify-style UI card for manage_product_bom that renders the BOM itself as a diff-decorated table (per-row add/update/delete), and threads BOM-specific sidecar data through the modification response so the UI can render ingredient identity (SKU/display name) for planned rows—especially adds.
Changes:
- Introduce
ModificationResponse.extrasand plumbextrasthroughrun_modify_plan()to carry entity-specific renderer data. - Implement
build_bom_modify_ui(and_merge_bom_rows_for_modify_card) to render a merged BOM table with per-row kind/status decoration. - Add unit + browser-render tests covering BOM merge behavior, UI output contracts, and end-to-end iframe rendering scenarios.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Bumps katana-mcp-server version to 0.93.1. |
| katana_mcp_server/src/katana_mcp/tools/_modification.py | Adds ModificationResponse.extras and dispatches entity_type=="product_bom" to build_bom_modify_ui. |
| katana_mcp_server/src/katana_mcp/tools/_modification_dispatch.py | Extends run_modify_plan(..., extras=...) and persists extras on both preview and apply responses. |
| katana_mcp_server/src/katana_mcp/tools/foundation/bom.py | Collects ingredient IDs touched by the plan, resolves SKU/display name once, and threads resolved_ingredients via extras. |
| katana_mcp_server/src/katana_mcp/tools/prefab_ui.py | Implements BOM modify card builder + row-merge helpers and renders a DataTable bound to state.plan_rows. |
| katana_mcp_server/tests/tools/test_bom.py | Pins that _modify_product_bom_impl carries extras.resolved_ingredients for renderer use. |
| katana_mcp_server/tests/test_prefab_ui.py | Adds extensive unit tests for BOM row merging and BOM modify UI rendering. |
| katana_mcp_server/tests/browser/render_test_server.py | Adds BOM modify preview/applied scenarios for the browser render harness. |
| katana_mcp_server/tests/browser/test_modification_render.py | Adds Playwright assertions validating BOM card renders all kinds and per-row statuses. |
dougborg
left a comment
There was a problem hiding this comment.
Self-review (6D via code-reviewer agent)
CI green. No production-blocking issues. The merge logic is correct on all realistic inputs.
✨ What looks good
- Explicit anti-pattern guard test (
test_card_does_not_leak_internal_action_labels) pinning thefeedback-user-centric-card-contentregression — exactly the right way to prevent the original symptom from returning. - The
+/-/~/2-char gutter trick for visual diff decoration without per-row styling is a clean Prefab workaround. resolved_ingredientsstring-key coercion inbuild_bom_modify_uicorrectly anticipates the pydanticmodel_dump()round-trip stringingdict[int, ...]keys.- Good helper reuse from the shared PO modify pool (
_render_preview_footer,_summarize_apply_outcome,_init_modify_card_state,_render_warnings_block,_build_apply_action,_build_cancel_action). - Coverage shape mirrors PO modify card density (32 new tests including partial-failure, all-adds-to-empty-BOM, unresolved-ingredient degradation, browser scenarios for preview + applied).
⚠️ Suggestions (worth addressing)
_merge_bom_rows_for_modify_cardsilently drops unknownoperationstrings — currently exhaustive (add/update/delete) but a futurereorder_bom_rowsop would vanish from the rendered card with no warning. Add anelse:with an explicit comment, or a log line.- Missing unit test:
update_bom_row/delete_bom_rowtargeting an orphan row id (not in snapshot). The orphan path is exercised in the browser fixture but not pinned at unit level. AddTestMergeBomRowsForModifyCardcases for both. - Missing unit test: all-deletes plan — "clear the recipe" is a real user scenario. The symmetric inverse of
test_empty_prior_with_only_adds_renders_added_rows. ModificationResponse.extras: dict[str, Any]deserves a typed accessor — theint(key)coercion currently leaks into the renderer. Fine for one entity; before 2-3 more children add their ownextraskeys, encapsulate via a typed classmethod likebom_resolved_ingredients(self) -> dict[int, dict[str, str | None]]. Filing this as a follow-up issue rather than blocking this PR — none of the 5 remaining #721 children are in flight yet.
💬 Nitpicks
_bom_row_summaryusesr["kind"]direct-access; everything else in the file uses.get(). The contract guarantees"kind"is always present (post-merge), so this works — but breaks the defensive style.
Verdict
Comment / no-block. I'll address suggestions 1, 2, 3 + the nitpick as a small fixup on this PR; suggestion 4 goes to a follow-up issue.
e907768 to
b3ff561
Compare
Fixup pushed — all review findings addressedFollowed up on the deferred items from the self-review + both Copilot inline comments: Copilot inline (both resolved in-thread):
Self-review SUGGESTIONS:
Self-review NITPICK:
Bonus: added 3579 unit tests pass, agent-check clean. |
78c2bcf to
4ee2d0a
Compare
Round 2 fixup pushed — Copilot's second-pass findings addressedCopilot picked up two real findings on the first fixup that I missed:
3579 unit tests pass, agent-check clean. Both threads resolved. |
4ee2d0a to
a116a3b
Compare
Round 3 fixup pushed — rebase + apply-state morph bug fixRebased onto current main + addressed Copilot's third-round finding. The bug (Copilot caught it): After clicking Confirm in the iframe, the DataTable's Status column would stay on PLANNED forever, even though the card-header/footer correctly flipped to APPLIED. Because The fix: server-side precomputation per Copilot's suggestion.
3592 unit tests pass; agent-check clean. Branch rebased onto current main, all 3 round-3 threads resolved. |
a116a3b to
1bfe7f4
Compare
Round 4 fixup — apply-state morph hardeningCopilot caught two more morph-related gaps the round-3 fix didn't cover. The DataTable rows now repointed via Finding 1 (badge stuck on APPLIED on failure): Fixed. Server now packs Finding 2 (failed-row Alert unreachable from morph): Fixed. Server pre-formats Tests: extended |
0c7cc8d to
770e812
Compare
Round 5 fixup —
|
770e812 to
3c694e1
Compare
Round 6 fixup — comment-drift cleanup + helper-extraction follow-up filedPure documentation/architecture cleanup this round — no behavior changes. Comment drift (3 sites): three remaining Test docstring drift: the Helper-extraction coupling: filed as #850. The "BOM actions → merged row list / outcome summary" computation is domain logic that currently lives in 3593 unit tests pass; agent-check clean. All 5 round-6 threads resolved. |
3c694e1 to
0680d84
Compare
…rd (closes #811) Sixth modify-style card in the #721 umbrella, following the just-shipped PO modify card template (#722). BOM is the table-as-entity-view variant — there are no scalar parent fields to diff, so the card renders the BOM table itself with per-row add/update/delete decoration instead of header-field diff lines. Before: a 30-row ``manage_product_bom(add_bom_rows=[...])`` preview showed 30 identical rows reading ``Add Bom Row | — | 3 field(s) set | PLANNED`` — no ingredient SKUs, no quantities, no row identity. The user had to Confirm 30 add operations blind. After: the card renders the merged BOM table — existing rows unchanged, added rows prefixed ``+`` with ingredient SKU + quantity, updated rows show ``old → new`` decoration, deleted rows render with the ``deleted`` kind. Per-row status pills (PLANNED / APPLIED / APPLIED_VERIFIED / FAILED) replace the opaque internal ``ActionResult`` model surfacing. 1. **Impl (`bom.py`)** — extended `_modify_product_bom_impl` to resolve ingredient SKU + display_name for every variant_id touched by add / update / delete sub-payloads. Threaded the resolved lookup onto `ModificationResponse.extras` (new field, entity-agnostic dict — useful for the other 5 modify cards that need entity-specific sidecar data). 2. **Builder (`prefab_ui.py`)** — new `build_bom_modify_ui` alongside `build_po_modify_ui`. Helper `_merge_bom_rows_for_modify_card` projects `prior_state.rows + actions + resolved_ingredients` into a unified row list with `kind` (existing / added / updated / deleted) and per-row `status_label`. DataTable columns: Rank | Ingredient SKU | Display Name | Quantity (with old→new on updates) | Notes | Status. 3. **Dispatch (`_modification.py`)** — added ``entity_type == "product_bom"`` branch to `to_tool_result`, mirroring the PO branch. 4. **Tests** — 32 new tests pass alongside the existing 3543: - Unit (`test_prefab_ui.py`): ~15 tests covering each row kind, preview/applied/partial-failure states, edge cases (missing ingredient resolution, empty plan, all-deletes). - Unit (`test_bom.py`): pins the new `resolved_ingredients` resolution path and `extras` plumbing. - Browser (`test_modification_render.py`): full-render scenarios with mixed-op BOM responses pinning that the cards render end-to-end through the iframe. Closes #811 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0680d84 to
602ceb3
Compare
Round 8 fixup — two real morph/apply correctness bugsFinding 1 (fail-fast hides remaining actions): Fix: Finding 2 (footer verb frozen on morph): Fix: the impl now packs 3594 unit tests pass; agent-check clean. Both threads resolved. |
…module (closes #850) #833 left ``foundation/bom.py`` importing private helpers from ``prefab_ui`` to precompute ``extras["applied_plan_rows"]`` on the apply path — Copilot flagged this as UI/impl coupling. The "BOM actions → merged row list / outcome summary" computation is domain logic, not rendering. Move the table-merge helpers into ``katana_mcp/tools/foundation/bom_table.py`` (non-UI, no Prefab imports). The renderer (``prefab_ui.build_bom_modify_ui``) and the tool-impl (``foundation/bom._modify_product_bom_impl``) both import from the new module — no more underscore-helper crossing the UI/impl boundary. Moved: - ``_merge_bom_rows_for_modify_card`` + row-shaping helpers (``_bom_existing_row_from_snapshot``, ``_bom_synth_orphan_row``, ``_bom_add_action_to_row``, ``_apply_bom_update_to_row``, ``_apply_bom_delete_to_row``, ``_bom_change_lookup``) - ``_prepare_bom_table_rows`` - ``_format_bom_quantity`` / ``_format_bom_quantity_diff`` - ``_BOM_ROW_STATUS_VARIANTS`` + ``_BomMergedRowKind`` - ``_summarize_apply_outcome`` (cross-entity-generic, but only directly called by ``foundation/bom`` and ``prefab_ui.build_po_modify_ui``; housing it here lets ``bom.py`` import it without a UI dep — small pragmatic call, hoist to a dedicated shared-modify module later if a third caller appears) - ``_derive_status_label`` (dict version; the ``ActionResult`` version in ``_modification.py`` is untouched) Kept in ``prefab_ui.py``: - ``build_bom_modify_ui`` and all rendering glue (public render contract) - ``_bom_row_summary`` (compose the "+N added, ~M updated, -K deleted" display line — purely presentational) - ``_BOM_MODIFY_COLUMNS`` + ``_resolve_bom_table_rows`` (UI-bound configuration / wiring) Test impact: ``test_unknown_operation_is_logged_and_dropped`` updates the caplog logger name from ``katana_mcp.tools.prefab_ui`` to ``katana_mcp.tools.foundation.bom_table`` (the logger followed the code). All other tests pass unchanged — they import the moved symbols from ``prefab_ui`` which re-exports them transparently via its own import. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…module (closes #850) #833 left ``foundation/bom.py`` importing private helpers from ``prefab_ui`` to precompute ``extras["applied_plan_rows"]`` on the apply path — Copilot flagged this as UI/impl coupling. The "BOM actions → merged row list / outcome summary" computation is domain logic, not rendering. Move the table-merge helpers into ``katana_mcp/tools/foundation/bom_table.py`` (non-UI, no Prefab imports). The renderer (``prefab_ui.build_bom_modify_ui``) and the tool-impl (``foundation/bom._modify_product_bom_impl``) both import from the new module — no more underscore-helper crossing the UI/impl boundary. Moved: - ``_merge_bom_rows_for_modify_card`` + row-shaping helpers (``_bom_existing_row_from_snapshot``, ``_bom_synth_orphan_row``, ``_bom_add_action_to_row``, ``_apply_bom_update_to_row``, ``_apply_bom_delete_to_row``, ``_bom_change_lookup``) - ``_prepare_bom_table_rows`` - ``_format_bom_quantity`` / ``_format_bom_quantity_diff`` - ``_BOM_ROW_STATUS_VARIANTS`` + ``_BomMergedRowKind`` - ``_summarize_apply_outcome`` (cross-entity-generic, but only directly called by ``foundation/bom`` and ``prefab_ui.build_po_modify_ui``; housing it here lets ``bom.py`` import it without a UI dep — small pragmatic call, hoist to a dedicated shared-modify module later if a third caller appears) - ``_derive_status_label`` (dict version; the ``ActionResult`` version in ``_modification.py`` is untouched) Kept in ``prefab_ui.py``: - ``build_bom_modify_ui`` and all rendering glue (public render contract) - ``_bom_row_summary`` (compose the "+N added, ~M updated, -K deleted" display line — purely presentational) - ``_BOM_MODIFY_COLUMNS`` + ``_resolve_bom_table_rows`` (UI-bound configuration / wiring) Test impact: ``test_unknown_operation_is_logged_and_dropped`` updates the caplog logger name from ``katana_mcp.tools.prefab_ui`` to ``katana_mcp.tools.foundation.bom_table`` (the logger followed the code). All other tests pass unchanged — they import the moved symbols from ``prefab_ui`` which re-exports them transparently via its own import. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Closes #811 — sixth modify-style card under the #721 umbrella. Models on the just-shipped PO modify card (#722) but uses the table-as-entity-view variant: BOM has no scalar parent fields to diff, so the card renders the BOM table itself with per-row add/update/delete decoration instead of header-field diff lines.
Before / After
Before (the #811 symptom): a 30-row
manage_product_bom(add_bom_rows=[...])preview showed 30 identical rows readingAdd Bom Row | — | 3 field(s) set | PLANNED— no ingredient SKUs, no quantities, no row identity. The user had to Confirm 30 add operations blind.After: the card renders the merged BOM table — existing rows unchanged, added rows prefixed
+with ingredient SKU + quantity, updated rows showold → newdecoration, deleted rows render with thedeletedkind. Per-row status pills (PLANNED / APPLIED / APPLIED_VERIFIED / FAILED) replace the opaque internalActionResultmodel surfacing.Implementation
Impl side —
_modify_product_bom_implresolves ingredient SKU + display_name for every variant_id touched by add / update / delete sub-payloads, threading the lookup onto a newModificationResponse.extrasfield (entity-agnostic dict — useful for the other 5 modify cards needing entity-specific sidecar data).Builder —
build_bom_modify_uialongsidebuild_po_modify_ui. Helper_merge_bom_rows_for_modify_cardprojectsprior_state.rows + actions + resolved_ingredientsinto a unified row list withkindand per-rowstatus_label.Dispatch —
_modification.to_tool_resultgains anentity_type == "product_bom"branch mirroring the PO branch.Tests — 32 new tests added; 3543 → 3575 total. Browser scenarios pin the cards end-to-end through the iframe.
Test plan
uv run poe agent-check— lint + type-check passuv run poe test— 3575 unit tests passuv run poe check(full incl browser) — verifying in CImanage_product_bompreview to confirm the merged BOM table renders correctly with eachkind🤖 Generated with Claude Code