refactor(mcp): extract BOM table-merge helpers into shared bom_table module#857
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors the BOM modify-card “table merge” logic out of the UI layer (prefab_ui.py) into a new shared, non-UI domain module (foundation/bom_table.py) so both the rendering path and the tool-implementation path can reuse the same row-shaping helpers without foundation/bom.py importing UI internals.
Changes:
- Added
katana_mcp.tools.foundation.bom_tableto host BOM table-merge + status/outcome bucketing helpers with no Prefab/UI imports. - Updated
prefab_ui.pyto import the extracted helpers and removed the in-file implementations. - Updated
foundation/bom.py(apply path) andtest_prefab_ui.pyto reference the new module (including logger name for the warning test).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| katana_mcp_server/src/katana_mcp/tools/foundation/bom_table.py | New shared, non-UI module containing BOM row-merge + formatting + status/outcome helpers. |
| katana_mcp_server/src/katana_mcp/tools/prefab_ui.py | Removes BOM merge helpers from UI module; imports them from foundation.bom_table and keeps only rendering glue. |
| katana_mcp_server/src/katana_mcp/tools/foundation/bom.py | Switches apply-time helper imports from prefab_ui to foundation.bom_table to eliminate UI/impl coupling. |
| katana_mcp_server/tests/test_prefab_ui.py | Updates caplog expectations to the new logger namespace emitted by the extracted module. |
dougborg
left a comment
There was a problem hiding this comment.
Review Summary
Clean structural refactor that achieves its stated goal: BOM table-merge math is now in a self-contained domain module with no Prefab imports, and foundation/bom.py no longer reaches into prefab_ui for underscore helpers. All 258 tests survive unchanged. The behavior-fidelity check passes — every moved function is a verbatim copy. No generated files were touched.
Three issues found, none blocking.
BLOCKING (0)
None.
SUGGESTIONS (2)
1. katana_mcp_server/tests/test_prefab_ui.py:2264 and :3369-3372 — stale test imports of moved symbols still go through prefab_ui
These two call-sites import _summarize_apply_outcome, _merge_bom_rows_for_modify_card, _prepare_bom_table_rows from katana_mcp.tools.prefab_ui rather than from their new canonical home katana_mcp.tools.foundation.bom_table. They pass today only because prefab_ui.py incidentally re-exports all four symbols at module level (lines 111-115). If those top-level imports are ever narrowed to local/function scope (a routine refactoring step), the two test sites break silently without any indication the test was testing the wrong module.
The top-level test import at line 18 (from katana_mcp.tools.prefab_ui import _merge_bom_rows_for_modify_card) is used throughout TestMergeBomRowsForModifyCard — that class should be importing from bom_table given that the function now lives there.
Fix: update lines 2264 and 3369-3372 to from katana_mcp.tools.foundation.bom_table import .... Also update the top-level module import at line 18 to remove _merge_bom_rows_for_modify_card from the prefab_ui import block and import it from bom_table instead (either at the top of the file or at the test-class level).
2. katana_mcp_server/src/katana_mcp/tools/foundation/bom_table.py:82 — _BomMergedRowKind is defined but never referenced as a type annotation
The Literal type alias is moved over faithfully from prefab_ui.py but is unused within the new module: the row-shaper functions never annotate their kind writes with it (they use plain string literals). If ruff's F841 / PYI018 passes over this it may produce noise in future lint runs, and a reader scanning the module will reasonably wonder if it was meant to annotate something.
Fix: either annotate the "kind" dict values (e.g. declare a helper return type that includes kind: _BomMergedRowKind), or remove the dead alias and add it back when it is actually enforcing a type boundary.
NITPICKS (1)
3. katana_mcp_server/src/katana_mcp/tools/foundation/bom_table.py:110-142 — _summarize_apply_outcome is cross-entity generic but housed in a BOM-specific module
The PR notes call this out and make a pragmatic deferred-work call. That reasoning is sound for now. To prevent the deferred work from disappearing, recommend filing a GitHub issue (or adding a # TODO(#NNN) comment inline) so the "hoist to shared-modify-helpers if a third caller appears" intent is tracked rather than staying only in the PR description.
What Looks Good
- The new module has zero Prefab imports — verified with
grep -E 'from prefab_ui|PrefabApp|Badge\b'. Goal achieved. - All seven moved functions are verbatim copies of their originals — no dropped None-guards, no missing
ordefaults, no behavioral drift on any path. - The caplog logger-name assertion in
TestMergeBomRowsForModifyCard.test_unknown_operation_is_logged_and_droppedwas correctly updated tokatana_mcp.tools.foundation.bom_table(both theat_levelguard and therec.namefilter). - The two
_derive_status_labelvariants remain distinct: thedict-typed one is inbom_table, theActionResult-typed one stays in_modification.py— no collapse. foundation/bom.pylazy-import correctly re-targetsbom_tablewith no residualprefab_uiunderscore imports.- Test count is unchanged (258 → 258); no tests were silently deleted in the rename.
…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>
731afb0 to
d3f9957
Compare
|
Addressed all three review findings in d3f9957: Suggestion 1 — stale test imports of moved symbols ( Suggestion 2 — unused Nitpick 3 — Verification: |
Closes #850.
Summary
katana_mcp/tools/foundation/bom_table.py(pure domain, no Prefab imports) holds the BOM table-merge math.foundation/bom.pyno longer imports underscore helpers fromprefab_ui— the UI/impl coupling Copilot flagged on feat(mcp): build_bom_modify_ui — per-row diff-decorated BOM modify card (closes #811) #833 is gone.prefab_ui.build_bom_modify_uiimports the same helpers from the new module; rendering glue stays inprefab_ui.py.What moved
_merge_bom_rows_for_modify_cardand its row-shapers (_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_outcomeand_derive_status_label(dict version)What stayed in
prefab_ui.pybuild_bom_modify_ui+ all rendering glue (public render contract)._bom_row_summary— purely presentational summary line._BOM_MODIFY_COLUMNS+_resolve_bom_table_rows— UI-bound wiring.Judgment calls
_summarize_apply_outcome: Cross-entity-generic, but only directly called byfoundation/bomandprefab_ui.build_po_modify_ui. Housed inbom_table.pybecause that breaks the bom→prefab_ui underscore import without adding a second new module. If a third caller appears later, hoist to a dedicated shared-modify-helpers module — pragmatic call now to keep the diff focused._derive_status_label(dict version): Used by the BOM merge AND by_action_to_rowinprefab_ui. Moved with the BOM merge so the new module is self-contained;prefab_uiimports it back. TheActionResult-typed version in_modification.pyis untouched.Test plan
uv run poe agent-checkanduv run poe test(3647 passed, 7 skipped) both clean.🤖 Generated with Claude Code