engineering/mech/features.py is the nineteen typed features of spec section 5,
each answering the same four questions - is my placement legal, what do I
contribute to this view, to a cut face, and what dimension do I want - plus the
five structural hooks the view engine needs: removal_box, radial_edit,
corner_edit, outline_edit and omission. Pure arithmetic; a test asserts emitted
primitive tuples, not a screenshot.
Nothing restates a table the repository already owns: Keyway takes its width
and depth from engineering.keyway (DIN 6885-1) and is refused by name outside
that table's bore range; GearTeeth takes its outline from engineering.gear's
involute generator and is refused when its tip diameter disagrees with the
segment it sits on.
standards/threads.py transcribes ISO 261 Table 1 and Table 2 - first-choice
M1.6-M64 plus second-choice M14/M18/M22/M27/M33/M39/M45/M52/M60 - and computes
ISO 724's basic minor and pitch diameters from ISO 68-1's own formula, pinned
against the published values (M20x2.5 -> 17.294 / 18.376). The remaining
second-choice diameters, every third-choice diameter and fine pitches below M8
are not transcribed and are refused by name, never interpolated. The drawing
rules are ISO 6410: minor at 0.8 x major, a thread-length limit line across the
major, a three-quarter circle in the end view, and an internal thread whose
section hatch runs to the major diameter.
standards/{undercuts,rings,centres,orings}.py ship DIN 509, DIN 471/472,
DIN 332-1 and ISO 3601-2 with their SOURCE, their row-key contract and an
**empty** row set. That is deliberate and is the track's table rule taken at its
word: a row that could not be verified against the standard is not shipped,
because a wrong relief depth or groove diameter is a wrong workshop drawing
while a narrow table is merely narrow. Every lookup is refused by name and says
what to transcribe and where; the features accept explicit dimensions so the
geometry is fully exercised, and DIN 332 form R is refused outright rather than
approximated by the straight 60 degree cone of forms A and B. Filling any of
these tables later is a data edit with no code change, and a shape test already
fails any row that lands without a column the drawing code reads.
Three deviations from the plan text, each measured rather than assumed
(the merge agent should keep exactly one copy of the first):
1. standards/__init__.py (Task 1, on main) refused a registration with no rows
("registered no rows"), which would have made the four not-transcribed
tables impossible to register at all - and registering them is what keeps a
later transcription a data edit. register() now refuses a rows argument that
is not a dict and accepts an empty one; two tests in tests/test_mech_standards.py
pin both halves. No existing test asserted the old refusal.
2. ORingGroove's housing kind is carried by `housing`, not `kind`. A dataclass
field named `kind` overwrites the discriminator _Base.to_dict() writes:
measured, Probe(id='og').to_dict() == {'kind': 'radial', 'id': 'og'}, so
feature_from_dict would then refuse its own payload as an unknown feature
kind and no part carrying an O-ring groove could round-trip through XDATA.
A new test pins the round trip.
3. The plan's end-chamfer test asserted after == (75.0, 10.0) - the corner
itself - and before == (72.0, 8.268), a point on neither surface. The real
edit is before (72.0, 10.0) on the cylinder and after (75.0, 8.268) on the
end face, consistent with the 45-degree start chamfer the same file pins.
The test was corrected to the measured geometry; the behaviour is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
161 lines
4.9 KiB
Python
161 lines
4.9 KiB
Python
"""The four tables this build does NOT transcribe, and how they refuse.
|
|
|
|
The track's table rule is absolute: a row that cannot be sourced against the
|
|
standard is not shipped, because a wrong relief depth or a wrong groove
|
|
diameter is a wrong workshop drawing, while a narrow table is merely narrow.
|
|
DIN 509, DIN 471/472, DIN 332-1 and ISO 3601-2 therefore ship with their
|
|
structure, their SOURCE and an empty row set, and every lookup is refused by
|
|
name and says what to transcribe.
|
|
|
|
These tests are the gate that keeps that honest in both directions: the
|
|
refusal must name the standard and the transcription route, and the shape gate
|
|
must fail the moment somebody adds a row that is missing a column the drawing
|
|
code reads.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from engineering.mech.standards import standards
|
|
from engineering.mech.standards.centres import (
|
|
CENTRE_ROW_KEYS,
|
|
ROWS_A,
|
|
ROWS_B,
|
|
centre_hole_dims,
|
|
)
|
|
from engineering.mech.standards.centres import (
|
|
SOURCE as SOURCE_CENTRES,
|
|
)
|
|
from engineering.mech.standards.orings import (
|
|
ORING_ROW_KEYS,
|
|
oring_groove_dims,
|
|
)
|
|
from engineering.mech.standards.orings import (
|
|
ROWS as ROWS_ORING,
|
|
)
|
|
from engineering.mech.standards.orings import (
|
|
SOURCE as SOURCE_ORING,
|
|
)
|
|
from engineering.mech.standards.rings import (
|
|
RING_ROW_KEYS,
|
|
ROWS_471,
|
|
ROWS_472,
|
|
ring_groove_dims,
|
|
)
|
|
from engineering.mech.standards.rings import (
|
|
SOURCE as SOURCE_RINGS,
|
|
)
|
|
from engineering.mech.standards.undercuts import (
|
|
ROWS_E,
|
|
ROWS_F,
|
|
UNDERCUT_ROW_KEYS,
|
|
undercut_profile,
|
|
)
|
|
from engineering.mech.standards.undercuts import (
|
|
SOURCE as SOURCE_UNDERCUTS,
|
|
)
|
|
|
|
|
|
def test_every_table_is_registered_so_a_later_transcription_is_a_data_edit():
|
|
registered = standards()
|
|
for name in (
|
|
"DIN 509-E",
|
|
"DIN 509-F",
|
|
"DIN 471",
|
|
"DIN 472",
|
|
"DIN 332-A",
|
|
"DIN 332-B",
|
|
"ISO 3601-2",
|
|
):
|
|
assert name in registered
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("source", "standard"),
|
|
[
|
|
(SOURCE_UNDERCUTS, "DIN 509"),
|
|
(SOURCE_RINGS, "DIN 471"),
|
|
(SOURCE_CENTRES, "DIN 332"),
|
|
(SOURCE_ORING, "ISO 3601-2"),
|
|
],
|
|
)
|
|
def test_every_source_names_its_standard_and_edition(source, standard):
|
|
assert standard in source
|
|
assert any(ch.isdigit() for ch in source.split(":")[-1])
|
|
|
|
|
|
def test_no_row_is_shipped_in_this_build():
|
|
assert ROWS_E == {} and ROWS_F == {}
|
|
assert ROWS_471 == {} and ROWS_472 == {}
|
|
assert ROWS_A == {} and ROWS_B == {}
|
|
assert ROWS_ORING == {}
|
|
|
|
|
|
def test_the_undercut_lookup_is_refused_by_name_and_says_what_to_transcribe():
|
|
with pytest.raises(ValueError) as excinfo:
|
|
undercut_profile("E", 40.0)
|
|
message = str(excinfo.value)
|
|
assert "DIN 509" in message
|
|
assert "form E" in message
|
|
assert "40" in message
|
|
assert "undercuts.py" in message
|
|
|
|
|
|
def test_an_unknown_undercut_form_is_refused_before_the_coverage_refusal():
|
|
with pytest.raises(ValueError, match="forms E and F"):
|
|
undercut_profile("G", 40.0)
|
|
|
|
|
|
def test_the_ring_groove_lookup_is_refused_by_name_for_shaft_and_bore():
|
|
for standard in ("DIN 471", "DIN 472"):
|
|
with pytest.raises(ValueError) as excinfo:
|
|
ring_groove_dims(standard, 30.0)
|
|
assert standard in str(excinfo.value)
|
|
assert "rings.py" in str(excinfo.value)
|
|
with pytest.raises(ValueError, match="DIN 471 or DIN 472"):
|
|
ring_groove_dims("DIN 6799", 30.0)
|
|
|
|
|
|
def test_the_centre_hole_lookup_is_refused_by_name_and_form_r_is_refused_outright():
|
|
with pytest.raises(ValueError) as excinfo:
|
|
centre_hole_dims("A", 2.5)
|
|
assert "DIN 332" in str(excinfo.value)
|
|
assert "centres.py" in str(excinfo.value)
|
|
with pytest.raises(ValueError) as excinfo:
|
|
centre_hole_dims("R", 2.5)
|
|
assert "form R" in str(excinfo.value)
|
|
|
|
|
|
def test_the_oring_lookup_is_refused_by_name():
|
|
with pytest.raises(ValueError) as excinfo:
|
|
oring_groove_dims(3.53)
|
|
assert "ISO 3601-2" in str(excinfo.value)
|
|
assert "orings.py" in str(excinfo.value)
|
|
|
|
|
|
def test_the_row_key_contracts_are_the_ones_the_drawing_code_reads():
|
|
assert UNDERCUT_ROW_KEYS == ("profile",)
|
|
assert RING_ROW_KEYS == ("m", "d2")
|
|
assert CENTRE_ROW_KEYS == ("d1", "d2", "d3", "t")
|
|
assert ORING_ROW_KEYS == ("b", "h")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("rows", "keys"),
|
|
[
|
|
(ROWS_E, ("profile",)),
|
|
(ROWS_F, ("profile",)),
|
|
(ROWS_471, ("m", "d2")),
|
|
(ROWS_472, ("m", "d2")),
|
|
(ROWS_A, ("d1", "d2", "t")),
|
|
(ROWS_B, ("d1", "d2", "d3", "t")),
|
|
(ROWS_ORING, ("b", "h")),
|
|
],
|
|
)
|
|
def test_any_row_that_is_ever_added_must_carry_every_key_the_drawing_code_reads(rows, keys):
|
|
"""Empty today; this gate fires the moment a row lands without a column."""
|
|
for size, row in rows.items():
|
|
missing = [key for key in keys if key not in row]
|
|
assert not missing, f"row {size} is missing {missing}"
|