diff --git a/README.md b/README.md index 09f8697..2da4c78 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Live through COM on Windows, or headless through ezdxf anywhere — one typed co -> **v1.5 release snapshot:** 247 tools · 8 resources · 5 prompt templates · 4516 collected tests. +> **v1.5 release snapshot:** 247 tools · 8 resources · 5 prompt templates · 4942 collected tests. > 247 is the **registered** count; a default install advertises 242 over `tools/list`, > because `ENABLE_3D` is unset. `system_about` is the runtime authority. @@ -35,7 +35,7 @@ Live through COM on Windows, or headless through ezdxf anywhere — one typed co **A big MCP server is expensive to be connected to.** The full catalog costs a client **78,772 tokens** before it has asked for anything. Discovery mode replaces it with two tools and costs **356**. -**A drafter searches for `FILLET`, not `entity_fillet`.** Those command names appeared in no tool name or description — `df = 0` against a stock index, not badly ranked but *absent*. The fix was data: an authored corpus of **205 AutoCAD command names and 1318 synonym phrases** covering all 241 tools. A test refuses to let a tool exist without one. +**A drafter searches for `FILLET`, not `entity_fillet`.** Those command names appeared in no tool name or description — `df = 0` against a stock index, not badly ranked but *absent*. The fix was data: an authored corpus of **206 AutoCAD command names and 1439 synonym phrases** covering all 247 tools. A test refuses to let a tool exist without one. | Advertised surface | Tools seen | Idle cost | |---|---:|---:| @@ -108,7 +108,7 @@ Claude Desktop, Cursor, or any stdio MCP host. For HTTP: `autocad-mcp --transpor | Architecture | one plan model — walls with hosted doors and windows — and one engine: L / T / X junctions resolved, openings cut, poché by material; rooms measured from the faces the walls enclose (never typed) and read back from any plan with a confidence; door / window / room schedules as real TABLEs; a furniture and sanitary catalogue at nominal sizes; structural grid, north arrow, section / level / elevation marks; exterior dimension chains; English or Turkish labels (`lang="tr"`, decimal comma) | | P&ID | catalogue blocks with ports and tags (ISO 10628-2 / ISA-5.1), port-to-port lines with ISA-5.1 classes and line numbers, `pid_graph` reads any P&ID back with confidence, instrument index / line list / equipment list, `pid_from_spec` one-call sheets | | Styles & standards | ISO-25 / ANSI dimension styles, ISOCP / ROMANS text styles, ISO / ANSI leader styles from authored presets; `drawing_apply_standard("iso")` sets all of it plus units and the `mech` layers in one call; `changed` reports only what moved | -| Discovery | `search_tools` ranked over an AutoCAD command and synonym corpus — `FILLET`, `BPOLY`, `QSELECT`, `WBLOCK`, `OVERKILL`, `CHSPACE` each rank **#1** of the 236-tool advertised catalog | +| Discovery | `search_tools` ranked over an AutoCAD command and synonym corpus — `FILLET`, `BPOLY`, `QSELECT`, `WBLOCK`, `OVERKILL`, `CHSPACE` each rank **#1** of the 242-tool advertised catalog | | Batching | `cad_batch` runs a step list in one round trip; `fields=` projects 11 result-heavy tools | | Paper space | tab lifecycle, viewports, `entity_change_space` (CHSPACE), `page_setup_apply` (ISO 216 / ANSI Y14.1 paper, ctb, scale, device — on both engines), `batch_plot` with every sheet size read back from its PDF's `/MediaBox`, `drawing_export_pdf(layout=…)` | | Page setup & templates | ISO 216 / ANSI Y14.1 sheets, ctb catalog, `batch_plot` verified by the PDF's own `/MediaBox`, five bundled templates built by the server's own tools and pinned reproducible (`drawing_new(template="iso_a3_mech")`, `iso_a1_arch`, `iso_a3_pid`, `ansi_b_mech`, `ansi_d_arch`), save any drawing as a template (`.dwt` on live AutoCAD, `dwt_write` refused headlessly) | @@ -122,7 +122,7 @@ Claude Desktop, Cursor, or any stdio MCP host. For HTTP: `autocad-mcp --transpor | Quality loop | `drawing_preflight` → `drawing_plan` → `drawing_critique` → `drawing_refine` → `drawing_finalize` (0–100 score) | | Delivery | `drawing_deliver`: DXF/PDF/PNG + SHA-256 manifest + reopen-parity checks | -247 tools in 26 groups; TOOL_PACKS=core hides the nine pid_* tools, the 22 environment tools, the 14 mechanical tools and the 12 architectural tools (TOOL_PACKS=core,mech keeps the mechanical ones, TOOL_PACKS=core,arch the architectural ones) from a client that needs none of them. Plus 8 resources that cost nothing in the tool budget (autocad://drawing/info, layers, blocks, entities/stats, entities/{layer_name}, system/status, pid/symbols, standards/isa51) and 5 prompt templates. +247 tools in 28 groups; TOOL_PACKS=core hides the nine pid_* tools, the 22 environment tools, the 14 mechanical tools, the 12 architectural tools and the two plant takeoffs (TOOL_PACKS=core,mech keeps the mechanical ones, TOOL_PACKS=core,arch the architectural ones, TOOL_PACKS=core,plant the takeoffs) from a client that needs none of them. Plus 8 resources that cost nothing in the tool budget (autocad://drawing/info, layers, blocks, entities/stats, entities/{layer_name}, system/status, pid/symbols, standards/isa51) and 5 prompt templates. **Two rules worth knowing.** Every coordinate in and out of a tool is WCS on both engines — the one exception is TEXT `rotation`, which stays in the entity frame because a mirrored TEXT is mirror-imaged and no scalar angle expresses that. And never read vertices back and shoelace them: that loses **28.2%** of the area on a semicircular edge, silently. `analysis_measure_entity(handle)` reads the real geometry and states its own accuracy. @@ -234,8 +234,8 @@ Nothing loads a `.env` file — export these, or set them in your MCP client's ` |---|---|---| | `AUTOCAD_MCP_BACKEND` | `auto` | `auto`, `com`, or `ezdxf` | | `CAD_PROGID` | `AutoCAD.Application` | COM ProgID the live backend attaches to | -| `TOOL_PROFILE` | `full` | `lean` (64 curated tools, 54 with `TOOL_PACKS=core`) or `full` | -| `TOOL_PACKS` | `all` | Vertical packs to advertise: `core,pid,settings` (`core` always on) | +| `TOOL_PROFILE` | `full` | `lean` (65 curated tools, 55 with `TOOL_PACKS=core`) or `full` | +| `TOOL_PACKS` | `all` | Vertical packs to advertise: `core,pid,settings,mech,arch,plant` (`core` always on) | | `DISCOVERY_MODE` | `off` | `search` replaces the catalog with `search_tools` + `call_tool` | | `ENABLE_3D` | `false` | Expose the opt-in `solid_*` tools (COM) | | `LOG_LEVEL` | `INFO` | Python logging level | diff --git a/discovery/aliases.py b/discovery/aliases.py index 371dc96..83dc247 100644 --- a/discovery/aliases.py +++ b/discovery/aliases.py @@ -2580,6 +2580,150 @@ TOOL_ALIASES: dict[str, ToolAliases] = { } +# ── Track H: the five-language vocabulary of spec §12 ────────────────────── +# +# The six track-H records were written by three branches (SECTION 26 by groups +# U and D, SECTION 27 by group N). Spec §12's vocabulary - the words a plant +# engineer types when asking for a takeoff, a comparison or a scale check - is +# folded into them here, after the merge, so every branch's own phrases stay +# and nothing is written twice. A record missing here is a KeyError at import: +# a track-H tool without its branch's record is a merge that lost something. +# +# Turkish carries both spellings, diacritic and ASCII-folded, because the +# search tokenizer splits on a non-ASCII letter instead of folding it (the +# room records above say the same). The Russian terms are data for a client +# that searches this corpus itself: the BM25 tokenizer keeps only [a-z0-9], so +# it cannot index Cyrillic yet, and no golden case pretends it can. +# +# COMPARE is AutoCAD's drawing-compare command (the "DWG Compare" feature); +# "dwg compare" / "dwgcompare" are how drafters name the feature, so they are +# synonyms, not a command token. +_TRACK_H_VOCABULARY: dict[str, tuple[tuple[str, ...], tuple[str, ...]]] = { + "drawing_understand": ( + (), + ( + "what is in this drawing", + "what is in this drawing somebody sent me", + "understand a foreign drawing", + "read a drawing somebody else made", + "which units is this drawing in", + "drawing units check", + "birim", + "birim kontrolü", + "birim kontrolu", + "çizim birimi", + "cizim birimi", + "çizimi anla", + "cizimi anla", + "yabancı çizim", + "yabanci cizim", + ), + ), + "drawing_scale_check": ( + (), + ( + "scale check", + "is this drawing to scale", + "is the p&id to scale", + "schematic or to scale", + "compare distances between two drawings", + "ölçek", + "olcek", + "ölçek kontrolü", + "ölçek kontrolu", + "olcek kontrolu", + "ölçekli mi", + "olcekli mi", + "bu çizim ölçekli mi", + "bu cizim olcekli mi", + ), + ), + "drawing_diff": ( + ("COMPARE",), + ( + "compare", + "compare two revisions", + "compare two revisions of this drawing", + "what changed between revisions", + "drawing diff", + "dwg compare", + "dwgcompare", + "fark", + "revizyon farkı", + "revizyon farki", + "karşılaştır", + "karsilastir", + "iki revizyonu karşılaştır", + "iki revizyonu karsilastir", + "сравнить чертежи", # compare drawings + ), + ), + "drawing_topology_check": ( + (), + ( + "find dangling line ends", + "find dangling line ends and gaps", + "uçları açık çizgiler", + "uclari acik cizgiler", + "kopuk hat", + ), + ), + "pipe_takeoff": ( + (), + ( + "takeoff", + "pipe takeoff", + "pipe takeoff by diameter from the p&id", + "pipe quantities", + "bill of quantities", + "boq", + "metraj", + "metre cetveli", + "boru listesi", + "boru metrajı", + "boru metraji", + "boru metrajı çıkar", + "boru metraji cikar", + "спецификация", # specification: a bill of quantities + "ведомость", # statement: a quantity schedule + ), + ), + "cable_takeoff": ( + (), + ( + "takeoff", + "cable takeoff", + "cable schedule", + "cable lengths to the panel", + "bill of quantities", + "boq", + "metraj", + "metre cetveli", + "kablo listesi", + "kablo metrajı", + "kablo metraji", + "kablo metrajı çıkar", + "kablo metraji cikar", + "кабельная ведомость", # cable schedule + "спецификация", # specification: a bill of quantities + ), + ), +} + + +def _fold_in(records: dict[str, ToolAliases], extra) -> None: + """Append ``extra``'s commands and phrases to existing records, never twice.""" + for name, (acad, synonyms) in extra.items(): + record = records[name] + records[name] = ToolAliases( + acad=record.acad + tuple(c for c in acad if c not in record.acad), + synonyms=record.synonyms + tuple(s for s in synonyms if s not in record.synonyms), + ) + + +_fold_in(TOOL_ALIASES, _TRACK_H_VOCABULARY) + + def aliases_for(tool_name: str) -> ToolAliases | None: """Return the alias record for ``tool_name``, or ``None`` if it has none.""" return TOOL_ALIASES.get(tool_name) diff --git a/docs/tool-inventory.json b/docs/tool-inventory.json index 320abf3..566685c 100644 --- a/docs/tool-inventory.json +++ b/docs/tool-inventory.json @@ -6,10 +6,10 @@ "tools": 247, "resources": 8, "prompts": 5, - "groups": 26 + "groups": 28 }, "groups": { - "analysis": 18, + "analysis": 12, "architecture": 12, "batch": 3, "blocks": 9, @@ -26,6 +26,7 @@ "mechanical": 14, "page_setup": 6, "pid": 9, + "plant": 2, "premium": 12, "sheet": 11, "solids": 5, @@ -33,6 +34,7 @@ "system": 7, "templates": 2, "transactions": 3, + "understanding": 4, "validation": 1, "view": 4 }, @@ -545,7 +547,7 @@ }, { "name": "cable_takeoff", - "group": "analysis", + "group": "plant", "cost": "safe", "summary": "Cable takeoff: one row per load, Manhattan length to its panel, rounded up to the metre.", "tags": [ @@ -914,12 +916,13 @@ }, { "name": "drawing_diff", - "group": "analysis", + "group": "understanding", "cost": "mutate", "summary": "Compare two revisions of a drawing: what moved, changed, was added or removed.", "tags": [ "analysis", - "query" + "query", + "understand" ], "annotations": { "title": "Drawing: Diff Two Revisions", @@ -1147,12 +1150,13 @@ }, { "name": "drawing_scale_check", - "group": "analysis", + "group": "understanding", "cost": "read", "summary": "Is this drawing to scale against a reference? Tag distances, ratios, a verdict.", "tags": [ "analysis", - "query" + "query", + "understand" ], "annotations": { "title": "Understand: Scale Check", @@ -1206,12 +1210,13 @@ }, { "name": "drawing_topology_check", - "group": "analysis", + "group": "understanding", "cost": "read", "summary": "Find dangling ends, near misses and unjoined crossings in a pipe or cable network.", "tags": [ "analysis", - "query" + "query", + "understand" ], "annotations": { "title": "Drawing: Topology Check", @@ -1220,12 +1225,13 @@ }, { "name": "drawing_understand", - "group": "analysis", + "group": "understanding", "cost": "read", "summary": "Read a foreign drawing: units, extents, clusters, layers, tags, rooms, languages.", "tags": [ "analysis", - "query" + "query", + "understand" ], "annotations": { "title": "Understand: Describe Drawing", @@ -2597,7 +2603,7 @@ }, { "name": "pipe_takeoff", - "group": "analysis", + "group": "plant", "cost": "safe", "summary": "Pipe takeoff from a P&ID and its layout: rows by room, service and diameter.", "tags": [ diff --git a/server.py b/server.py index 994695c..cb99e45 100644 --- a/server.py +++ b/server.py @@ -692,6 +692,8 @@ _GROUP_TAG_PRIORITY = ( "pid", "mech", "arch", + "understand", + "plant", "sheet", "style", "pagesetup", @@ -723,6 +725,8 @@ _GROUP_TAG_LABELS = { "pid": "pid", "mech": "mechanical", "arch": "architecture", + "understand": "understanding", + "plant": "plant", "sheet": "sheet", "style": "styles", "pagesetup": "page_setup", @@ -973,6 +977,12 @@ LEAN_TOOL_NAMES = frozenset( "arch_opening", "arch_room", "arch_plan_from_spec", + # Understanding (track H) - the one read a lean client needs before it + # touches a drawing somebody else made: units, extents, clusters, + # layers and their services, tags and rooms in one call. A core-pack + # tool, so TOOL_PACKS=core keeps it; with the four architectural ones + # above, lean is 65. The takeoffs are `plant`-pack, full-profile work. + "drawing_understand", } ) @@ -986,7 +996,7 @@ SOLID_TOOL_NAMES = frozenset( # mechanically should not pay for the P&ID surface, and a client that never # talks to a live seat should not pay for the environment surface. Styles and # page setup (SECTION 18/19) are drafting essentials and stay in core. -TOOL_PACK_NAMES = ("core", "pid", "settings", "mech", "arch") +TOOL_PACK_NAMES = ("core", "pid", "settings", "mech", "arch", "plant") PACK_TOOL_NAMES: dict[str, frozenset[str]] = { "pid": frozenset( { @@ -1070,6 +1080,12 @@ PACK_TOOL_NAMES: dict[str, frozenset[str]] = { "arch_plan_from_spec", } ), + # SECTION 27 - the plant takeoffs: pipe and cable quantities whose topology + # comes from a P&ID and whose lengths come from its layout (track H spec + # §8-9, §12). SECTION 26's four readers stay in `core`: understanding, + # comparing and checking a drawing is not a vertical. Track D's Plant 3D + # readers join this pack. + "plant": frozenset({"pipe_takeoff", "cable_takeoff"}), } _active_tool_profile: dict | None = None @@ -10569,7 +10585,7 @@ async def arch_plan_from_spec( ) @mcp.tool( annotations={"title": "Understand: Describe Drawing", "readOnlyHint": True}, - tags={"analysis", "query"}, + tags={"understand", "analysis", "query"}, ) async def drawing_understand( path: Annotated[ @@ -10638,7 +10654,7 @@ async def drawing_understand( ) @mcp.tool( annotations={"title": "Understand: Scale Check", "readOnlyHint": True}, - tags={"analysis", "query"}, + tags={"understand", "analysis", "query"}, ) async def drawing_scale_check( reference: Annotated[ @@ -10800,7 +10816,7 @@ async def _diff_markup(backend, result: dict, new_snap, *, rev: str, description ) @mcp.tool( annotations={"title": "Drawing: Diff Two Revisions", "destructiveHint": False}, - tags={"analysis", "query"}, + tags={"understand", "analysis", "query"}, ) async def drawing_diff( old_path: Annotated[ @@ -10932,7 +10948,7 @@ async def drawing_diff( ) @mcp.tool( annotations={"title": "Drawing: Topology Check", "readOnlyHint": True}, - tags={"analysis", "query"}, + tags={"understand", "analysis", "query"}, ) async def drawing_topology_check( layers: Annotated[ diff --git a/tests/data/discovery_golden.py b/tests/data/discovery_golden.py index 7c285f6..54e7b3b 100644 --- a/tests/data/discovery_golden.py +++ b/tests/data/discovery_golden.py @@ -1,6 +1,6 @@ """Golden query set for tool discovery: English in, one right tool out. -Ninety-nine cases (84 tuning + 15 holdout) split into a **tuning** set and a +One hundred and seven cases (92 tuning + 15 holdout) split into a **tuning** set and a **holdout** set. The split is the point of the file. Ranking work is measured against :data:`TUNING_CASES` only; :data:`HOLDOUT_CASES` were written at the same time, @@ -204,6 +204,19 @@ TUNING_CASES: tuple[GoldenCase, ...] = ( GoldenCase("merdiven çiz", "arch_stair", "synonym"), GoldenCase("vaziyet planı çiz", "arch_plan_from_spec", "synonym"), GoldenCase("oda alanlarını hesapla", "arch_rooms_detect", "synonym"), + # v1.6 track H - reading drawings somebody else made. Written before the + # merged alias records were measured against them; a miss is fixed by + # adding vocabulary to that tool's record in discovery/aliases.py, never by + # moving the expectation. Half of them Turkish: a takeoff is asked for in + # the language of the shop that prices the job. + GoldenCase("boru metrajı çıkar", "pipe_takeoff", "synonym"), + GoldenCase("kablo metrajı", "cable_takeoff", "synonym"), + GoldenCase("iki revizyonu karşılaştır", "drawing_diff", "synonym"), + GoldenCase("bu çizim ölçekli mi", "drawing_scale_check", "synonym"), + GoldenCase("what is in this drawing somebody sent me", "drawing_understand", "paraphrase"), + GoldenCase("pipe takeoff by diameter from the p&id", "pipe_takeoff", "paraphrase"), + GoldenCase("compare two revisions of this drawing", "drawing_diff", "paraphrase"), + GoldenCase("find dangling line ends and gaps", "drawing_topology_check", "paraphrase"), ) diff --git a/tests/test_discovery_aliases.py b/tests/test_discovery_aliases.py index d1b5578..6a89563 100644 --- a/tests/test_discovery_aliases.py +++ b/tests/test_discovery_aliases.py @@ -208,3 +208,64 @@ def test_alias_text_contains_commands_and_synonyms(): for token in (*record.acad, *record.synonyms): assert token in text assert alias_text("no_such_tool") == "" + + +# ── track H: spec §12's five-language vocabulary ─────────────────────────── +# +# One row per term the spec names, in the record(s) a plant engineer means by +# it. Turkish comes in both spellings (diacritic and ASCII-folded): the search +# tokenizer splits on a non-ASCII letter instead of folding it. + +TRACK_H_TERMS = { + "pipe_takeoff": ( + "takeoff", + "boq", + "metraj", + "metre cetveli", + "boru listesi", + "boru metrajı", + "boru metraji", + "boru metrajı çıkar", + "boru metraji cikar", + "спецификация", # specification: a bill of quantities + "ведомость", # statement: a quantity schedule + ), + "cable_takeoff": ( + "takeoff", + "boq", + "metraj", + "metre cetveli", + "kablo listesi", + "kablo metrajı", + "kablo metraji", + ), + "drawing_diff": ( + "compare", + "dwgcompare", + "fark", + "karşılaştır", + "karsilastir", + "revizyon farkı", + "revizyon farki", + "iki revizyonu karşılaştır", + "iki revizyonu karsilastir", + ), + "drawing_scale_check": ( + "scale check", + "ölçek", + "olcek", + "bu çizim ölçekli mi", + "bu cizim olcekli mi", + ), + "drawing_understand": ("birim", "çizim birimi", "cizim birimi"), +} + + +def test_track_h_records_carry_the_five_language_vocabulary(): + missing = { + tool: [term for term in terms if term not in TOOL_ALIASES[tool].synonyms] + for tool, terms in TRACK_H_TERMS.items() + } + missing = {tool: terms for tool, terms in missing.items() if terms} + assert not missing, f"track-H records miss spec §12 vocabulary: {missing}" + assert "COMPARE" in TOOL_ALIASES["drawing_diff"].acad diff --git a/tests/test_token_suite.py b/tests/test_token_suite.py index ee85259..a65ab11 100644 --- a/tests/test_token_suite.py +++ b/tests/test_token_suite.py @@ -163,9 +163,9 @@ def test_the_cache_arithmetic_follows_the_published_multipliers(): def test_the_idle_lane_covers_the_five_advertised_surfaces(report): """Default, lean, search - and the two single-vertical clients: - TOOL_PACKS=core,mech (the default surface minus exactly the pid and settings - packs, and the arch pack) and TOOL_PACKS=core,arch (minus pid, settings and - mech).""" + TOOL_PACKS=core,mech (the default surface minus exactly the pid, settings, + arch and plant packs) and TOOL_PACKS=core,arch (minus pid, settings, mech + and plant).""" variants = {row["variant"]: row for row in report["idle"]} assert set(variants) == {"default", "lean", "packs_core_mech", "packs_core_arch", "search"} assert variants["default"]["advertised_tools"] > 100 @@ -177,12 +177,14 @@ def test_the_idle_lane_covers_the_five_advertised_surfaces(report): - len(packs["pid"]) - len(packs["settings"]) - len(packs["arch"]) + - len(packs["plant"]) ) assert variants["packs_core_arch"]["advertised_tools"] == ( variants["default"]["advertised_tools"] - len(packs["pid"]) - len(packs["settings"]) - len(packs["mech"]) + - len(packs["plant"]) ) for narrowed in ("packs_core_mech", "packs_core_arch"): assert ( diff --git a/tests/test_tool_packs.py b/tests/test_tool_packs.py index d8111e1..860b605 100644 --- a/tests/test_tool_packs.py +++ b/tests/test_tool_packs.py @@ -16,6 +16,13 @@ v1.6 track F adds the `arch` pack: SECTION 25's twelve architectural tools - the wall network, openings, stairs, rooms and their reader, schedules, the grid and symbols, the furniture and sanitary catalogue, the exterior chains and the whole plan from one spec. Lean gains four of them. + +v1.6 track H adds the `plant` pack: SECTION 27's two takeoffs (`pipe_takeoff`, +`cable_takeoff`) - quantities for a plant job, the vertical track D's Plant 3D +readers join later. SECTION 26's four readers (`drawing_understand`, +`drawing_scale_check`, `drawing_diff`, `drawing_topology_check`) stay in +`core`: understanding, comparing and checking a drawing is not a vertical. +Lean gains `drawing_understand` and reaches 65. """ from __future__ import annotations @@ -135,6 +142,20 @@ ARCH_TOOLS = { "arch_plan_from_spec", } +UNDERSTAND_TOOLS = { + "drawing_understand", + "drawing_scale_check", + "drawing_diff", + "drawing_topology_check", +} + +PLANT_TOOLS = { + "pipe_takeoff", + "cable_takeoff", +} + +LEAN_UNDERSTAND_ESSENTIALS = {"drawing_understand"} + LEAN_ARCH_ESSENTIALS = { "arch_wall", "arch_opening", @@ -159,7 +180,7 @@ LEAN_SETTINGS_ESSENTIALS = { } #: Declaration order - what `tool_packs["available"]` reports. -ALL_PACKS = ["core", "pid", "settings", "mech", "arch"] +ALL_PACKS = ["core", "pid", "settings", "mech", "arch", "plant"] #: `tool_packs["enabled"]` is sorted, which is no longer the same list. ALL_PACKS_ENABLED = sorted(ALL_PACKS) @@ -172,19 +193,26 @@ async def _restore(monkeypatch): def test_pack_registry_names_real_tools_and_only_them(): - assert server.TOOL_PACK_NAMES == ("core", "pid", "settings", "mech", "arch") + assert server.TOOL_PACK_NAMES == ("core", "pid", "settings", "mech", "arch", "plant") assert server.PACK_TOOL_NAMES["pid"] == frozenset(PID_TOOLS) assert server.PACK_TOOL_NAMES["mech"] == frozenset(MECH_TOOLS) assert server.PACK_TOOL_NAMES["settings"] == frozenset(SETTINGS_TOOLS) assert server.PACK_TOOL_NAMES["arch"] == frozenset(ARCH_TOOLS) + assert server.PACK_TOOL_NAMES["plant"] == frozenset(PLANT_TOOLS) assert len(SETTINGS_TOOLS) == 22 assert len(MECH_TOOLS) == 14 assert len(ARCH_TOOLS) == 12 + assert len(PLANT_TOOLS) == 2 assert not (server.PACK_TOOL_NAMES["settings"] & server.PACK_TOOL_NAMES["pid"]) assert not (server.PACK_TOOL_NAMES["mech"] & server.PACK_TOOL_NAMES["pid"]) assert not (server.PACK_TOOL_NAMES["mech"] & server.PACK_TOOL_NAMES["settings"]) for pack in ("pid", "settings", "mech"): assert not (server.PACK_TOOL_NAMES["arch"] & server.PACK_TOOL_NAMES[pack]), pack + for pack in ("pid", "settings", "mech", "arch"): + assert not (server.PACK_TOOL_NAMES["plant"] & server.PACK_TOOL_NAMES[pack]), pack + assert not (UNDERSTAND_TOOLS & set().union(*server.PACK_TOOL_NAMES.values())), ( + "SECTION 26 reads any drawing: core, not a vertical pack" + ) assert not (SHEET_TOOLS & set().union(*server.PACK_TOOL_NAMES.values())), ( "SECTION 24 is core drafting, not a vertical pack" ) @@ -205,7 +233,9 @@ async def test_core_only_hides_pid_and_settings_but_keeps_styles_and_page_setup( assert SETTINGS_TOOLS <= disabled assert MECH_TOOLS <= disabled assert ARCH_TOOLS <= disabled + assert PLANT_TOOLS <= disabled assert not (SHEET_TOOLS & disabled) + assert not (UNDERSTAND_TOOLS & disabled), "the readers of SECTION 26 are core" assert not (STYLE_TOOLS & disabled), "styles are core drafting essentials" assert not (PAGE_SETUP_TOOLS & disabled), "page setup and templates are core" assert "block_define" not in disabled and "entity_set_xdata" not in disabled @@ -221,11 +251,21 @@ async def test_settings_pack_alone_keeps_pid_hidden(monkeypatch): assert info["tool_packs"]["enabled"] == ["core", "settings"] +async def test_the_plant_pack_alone_adds_the_takeoffs_to_core(monkeypatch): + monkeypatch.setattr(config.settings, "tool_packs", "plant") + info = await server._apply_tool_profile("full") + disabled = set(info["disabled_tools"]) + assert not (PLANT_TOOLS & disabled) + assert not (UNDERSTAND_TOOLS & disabled) + assert PID_TOOLS <= disabled and ARCH_TOOLS <= disabled + assert info["tool_packs"]["enabled"] == ["core", "plant"] + + async def test_all_is_the_default_and_enables_every_pack(monkeypatch): monkeypatch.setattr(config.settings, "tool_packs", "all") info = await server._apply_tool_profile("full") disabled = set(info["disabled_tools"]) - assert not ((PID_TOOLS | SETTINGS_TOOLS | MECH_TOOLS | ARCH_TOOLS) & disabled) + assert not ((PID_TOOLS | SETTINGS_TOOLS | MECH_TOOLS | ARCH_TOOLS | PLANT_TOOLS) & disabled) assert info["tool_packs"]["enabled"] == ALL_PACKS_ENABLED @@ -254,22 +294,26 @@ async def test_lean_intersects_with_packs(monkeypatch): async def test_lean_carries_the_five_settings_essentials_and_nothing_from_the_settings_pack( monkeypatch, ): - """Spec §8.2: lean = 64 (55 + the five mechanical/sheet essentials + track - F's four architectural ones). The five settings essentials are core-pack - tools, so `TOOL_PACKS=core` leaves them on a lean surface; no environment - tool is lean.""" + """Spec §8.2: lean = 65 (55 + the five mechanical/sheet essentials + track + F's four architectural ones + track H's `drawing_understand`). The five + settings essentials and `drawing_understand` are core-pack tools, so + `TOOL_PACKS=core` leaves them on a lean surface; no environment tool and no + takeoff is lean.""" assert LEAN_SETTINGS_ESSENTIALS <= server.LEAN_TOOL_NAMES assert LEAN_MECH_ESSENTIALS <= server.LEAN_TOOL_NAMES assert LEAN_ARCH_ESSENTIALS <= server.LEAN_TOOL_NAMES assert server.LEAN_TOOL_NAMES & ARCH_TOOLS == LEAN_ARCH_ESSENTIALS + assert server.LEAN_TOOL_NAMES & UNDERSTAND_TOOLS == LEAN_UNDERSTAND_ESSENTIALS + assert not (PLANT_TOOLS & server.LEAN_TOOL_NAMES) assert not (SETTINGS_TOOLS & server.LEAN_TOOL_NAMES) - assert len(server.LEAN_TOOL_NAMES) == 64 + assert len(server.LEAN_TOOL_NAMES) == 65 monkeypatch.setattr(config.settings, "tool_packs", "core") info = await server._apply_tool_profile("lean") disabled = set(info["disabled_tools"]) assert not (LEAN_SETTINGS_ESSENTIALS & disabled) assert not ({"sheet_frame", "titleblock_apply"} & disabled), "sheet tools are core" - assert info["enabled_count"] == 64 - 10, ( + assert "drawing_understand" not in disabled, "the one-call report is core" + assert info["enabled_count"] == 65 - 10, ( "lean minus the three pid, three mech and four arch tools" ) @@ -284,7 +328,9 @@ async def test_the_three_track_e_sections_file_under_their_own_groups(): assert set(groups["mechanical"]) == MECH_TOOLS assert set(groups["sheet"]) == SHEET_TOOLS assert set(groups["architecture"]) == ARCH_TOOLS - assert len(groups) == 26 + assert set(groups["understanding"]) == UNDERSTAND_TOOLS + assert set(groups["plant"]) == PLANT_TOOLS + assert len(groups) == 28 async def test_system_about_reports_packs(): diff --git a/tests/test_tool_registry.py b/tests/test_tool_registry.py index 3e729d5..861c2c1 100644 --- a/tests/test_tool_registry.py +++ b/tests/test_tool_registry.py @@ -495,7 +495,7 @@ async def test_tool_groups_is_byte_identical_to_the_source_declared_grouping(): async def test_tool_group_sizes_are_unchanged(): - """Frozen snapshot of the surface (243 tools, 26 groups). + """Frozen snapshot of the surface (247 tools, 28 groups). Taken before @cad_tool landed at 131 tools; `batch` moved 2 -> 3 when v1.5.0's `cad_batch` joined `entity_batch_create`/`entity_batch_modify`, and @@ -592,41 +592,19 @@ async def test_tool_group_sizes_are_unchanged(): had grown while the tag was unranked on them returned to its pre-track-F value. - Track H group U opened SECTION 26 (Understanding & QA) with - `drawing_understand`, tagged `analysis` and `query`: `analysis` outranks - `query` in `_GROUP_TAG_PRIORITY`, so the reader files under `analysis` - (12 -> 13) beside the other whole-drawing queries, and - `drawing_scale_check`, tagged the same, followed it (13 -> 14). This - snapshot is branch-local: the track H merge task recomputes it once the - understand, network and diff branches have landed. - - Track H group D opened its copy of SECTION 26 (Understanding & QA) with - `drawing_diff` and `drawing_topology_check`, both tagged `analysis` and - `query`; `analysis` outranks `query` in `_GROUP_TAG_PRIORITY`, so both file - under `analysis` (12 -> 14). This snapshot is branch-local: the track H - merge recomputes it once the understand, network and diff branches have - landed. - - Merged, the two SECTION 26 halves file all four readers under `analysis` - (12 -> 16). - - `analysis` moved 12 -> 13 when track H's SECTION 27 opened with - `pipe_takeoff` on the network branch. It is tagged `plant` and - `analysis`, and `plant` is not in `_GROUP_TAG_PRIORITY` on that - branch, so it files under `analysis`. This snapshot is branch-local: - the track H integration task gives `plant` its pack and its group - once every branch has landed. - - It moved 13 -> 14 when `cable_takeoff` joined the same section, for - the same branch-local reason. - - With SECTION 27 merged beside them, the four readers and the two - takeoffs all file under `analysis` (12 -> 18) until the track H - integration task ranks `understand` and `plant`. + `understanding` and `plant` appeared with v1.6's track H: `understanding` + (4, SECTION 26: `drawing_understand`, `drawing_scale_check`, + `drawing_diff`, `drawing_topology_check` - the readers of drawings + somebody else made, core pack) and `plant` (2, SECTION 27: `pipe_takeoff` + and `cable_takeoff`, exactly `PACK_TOOL_NAMES["plant"]`). The three + track-H branches tagged the six `analysis` and left the priority list + alone, so `analysis` grew 12 -> 18 on them; the integration tagged + SECTION 26 `understand` and ranked `understand` and `plant` directly + after `arch` in `_GROUP_TAG_PRIORITY`, and `analysis` returned to 12. """ sizes = {label: len(names) for label, names in (await server._tool_groups()).items()} assert sizes == { - "analysis": 18, + "analysis": 12, "architecture": 12, "batch": 3, "blocks": 9, @@ -643,6 +621,7 @@ async def test_tool_group_sizes_are_unchanged(): "mechanical": 14, "page_setup": 6, "pid": 9, + "plant": 2, "premium": 12, "sheet": 11, "solids": 5, @@ -650,6 +629,7 @@ async def test_tool_group_sizes_are_unchanged(): "system": 7, "templates": 2, "transactions": 3, + "understanding": 4, "validation": 1, "view": 4, }