From 6002a9afe5d50c9d0705debb75a46a278fdaf50f Mon Sep 17 00:00:00 2001 From: kopecn Date: Wed, 26 Aug 2026 21:52:39 -0700 Subject: [PATCH 01/12] docs(specs): separate durable contracts from findings and plans --- .claude/CLAUDE.md | 9 +- .../2026-08-26-spherical-spatial-packaging.md | 413 ++++++++++++++++++ .claude/specs/mathToolsArchitecture.md | 30 +- .claude/specs/spatialMath.md | 107 ++++- .claude/specs/sphericalGeometry.md | 141 ++++++ .claude/specs/templateConformance.md | 174 ++++---- .claude/specs/waveformCore.md | 54 ++- requirements.txt | 2 +- 8 files changed, 789 insertions(+), 141 deletions(-) create mode 100644 .claude/findings/2026-08-26-spherical-spatial-packaging.md create mode 100644 .claude/specs/sphericalGeometry.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index f1ef708..b27913f 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -54,15 +54,20 @@ and every module's behavior. Consult the relevant spec before extending a module; if implementation forces a contract change, the spec is updated in the same change and its `semver` bumped. +Reproduced defect evidence, measurements, source locations, and +implementation material live in `.claude/findings/` — never in a spec. + - [mathToolsArchitecture.md](specs/mathToolsArchitecture.md) — umbrella: layering, package layout, dependency policy, shared conventions, error semantics -- [templateConformance.md](specs/templateConformance.md) — template - migration: packaging, Makefile/CI parity, rename, governance docs +- [templateConformance.md](specs/templateConformance.md) — packaging, + dependency resolvability, CI install parity, governance artifacts - [precisionTimeMath.md](specs/precisionTimeMath.md) — `PrecisionTimeInterval`, `PrecisionTimestamp` - [spatialMath.md](specs/spatialMath.md) — `Position`, `Quaternion`, `SpatialPose` +- [sphericalGeometry.md](specs/sphericalGeometry.md) — unit-sphere arcs and + small circles, transforms, the `orient` bearing convention - [waveformCore.md](specs/waveformCore.md) — `Waveform1D` + aggregate waveform containers - [waveformDsp.md](specs/waveformDsp.md) — DSP families, scipy mapping, diff --git a/.claude/findings/2026-08-26-spherical-spatial-packaging.md b/.claude/findings/2026-08-26-spherical-spatial-packaging.md new file mode 100644 index 0000000..459c178 --- /dev/null +++ b/.claude/findings/2026-08-26-spherical-spatial-packaging.md @@ -0,0 +1,413 @@ +--- +version: 1.0 +type: findings +name: spherical-spatial-packaging-defects +purpose: Reproduced defect evidence, measurements, and implementation material displaced from the specs during the 2026-08-26 normalization +scope: project +status: open +applies_to: src/math_tools/spherical/, src/math_tools/spatial/, src/math_tools/waveforms/, requirements.txt, Makefile, README.md +last_updated: 2026-08-26 +semver: 0.1.1 +author: Nicholas Bergantz +--- + +# Findings — spherical geometry, spatial math, packaging + +Companion to [sphericalGeometry.md](../specs/sphericalGeometry.md), +[spatialMath.md](../specs/spatialMath.md), +[waveformCore.md](../specs/waveformCore.md), and +[templateConformance.md](../specs/templateConformance.md). + +This artifact holds what those specs must not: current-defect evidence, +reproductions, measured values, source locations, and implementation material. +The specs state the durable expected outcomes; this states what was observed on +2026-08-26 and what an implementer needs in hand. + +Nothing here is normative. Recording an observation does not authorize a +requirement. + +## Source + +Eight findings produced by an audit pass on 2026-08-26 and then presented to +the human for decisions. All eight were independently reproduced before any +spec was drafted. Two additional defects were found during that reproduction +and are marked as such. None of these findings is human-authorized; they are +observations, and the decisions taken on them are recorded separately. + +## F1 — `arc_from_two_points` bearing uses transposed colatitude terms + +**Location:** `src/math_tools/spherical/constructors.py:84` + +**Observed:** the `x` component is written +`cos θ₁ · sin θ₂ − sin θ₁ · cos θ₂ · cos Δφ`; substituting `lat = π/2 − θ` into +the standard initial-bearing formula gives `sin θ₁ · cos θ₂ − cos θ₁ · sin θ₂ · cos Δφ`. +The two terms are transposed. + +| input `(φ₁,θ₁) → (φ₂,θ₂)` | requested endpoint | actual endpoint | +| --- | --- | --- | +| `(0.5, 0.8) → (1.2, 1.0)` | `(1.2000, 1.0000)` | `(1.3506, 0.6944)` | +| `(0.0, 0.01) → (π/2, 1.0)` | `(1.5708, 1.0000)` | `(2.3516, 0.9930)` | + +**Why it survived review:** the transposed form is not wrong everywhere. +Measured over 200 random pairs per family, correct and transposed agree +*exactly* on the equator, on any `θ₁ = θ₂` pair, and on the `Δφ = π` +antimeridian pair. They disagree on all 200 of `Δφ = π/2`, and differ by +exactly `π` (opposite bearings) at `Δφ = 0`. Equal-colatitude pairs are the +easiest examples to write by hand, which is why hand-checked spot tests pass. + +**Corrected form validated:** round-trip through +`compute_spherical_arc_endpoint` over 4,000 seeded random cases, worst chordal +error **4.1e-13**. + +## F1a — near-pole bearing regression vector (compliance item withdrawn) + +A "pole-start bearing" compliance item was drafted requiring the constructed +arc to reach the requested target azimuth for start points down to +`abs(sin θ₁) ≥ 1e-6`. It was withdrawn from the spec: it restated the +constructor round-trip item without adding an observable, its "at any distance +from a pole" wording contradicted its own lower bound, and target azimuth is +undefined when the *target* is at a pole — an endpoint the item left unbounded. + +The vector it was built from is still worth exercising, with both endpoints +held away from the poles. The near-pole start row of the F1 table above is that +vector: `(0.0, 0.01) → (π/2, 1.0)`, requested `(1.5708, 1.0000)`, actual on the +defective code `(2.3516, 0.9930)`. The round-trip item covers it, since +`abs(sin 0.01) = 0.0099 ≥ 1e-6`. + +## F2 — arc generator builds a reversed local-north tangent + +**Location:** `src/math_tools/spherical/spherical_generators.py:217` + +**Observed:** `north_tangent = cross(east, start_point)`, which equals `−n̂`. +The initial direction is therefore `−cos(orient)·n̂ + sin(orient)·ê`. + +From the equator with `orient = 0` and `arc_length = π/4`, generated points +travel **south** to polar `2.3562` (`3π/4`), while +`compute_spherical_arc_endpoint` correctly predicts `0.7854` (`π/4`). + +`generate_spherical_small_circle_points:112` already uses the correct +`cross(center, east)` form — the two functions in the same module disagree. + +**Corrected form validated:** `cross(start_point, east)`, with the pole +threshold shared with `compute_spherical_arc_endpoint`, agrees with the +endpoint function to worst chordal **2.1e-14** over 4,000 seeded arcs at +`num_points=33`. + +**Why it survived review:** `tests/spherical/` contains only +`test_public_surface.py`, which pins exports and identity. No geometry test +exists for the subpackage. + +## F2a — pole-threshold mismatch (found during reproduction, not reported) + +`compute_spherical_arc_endpoint:189` tests `abs(sin(theta_start)) < 1e-10`. +The generator's branch tests `abs(z_start) < 0.999`, i.e. colatitude within +~2.56° of a pole — a band roughly 8 orders of magnitude wider. + +Measured consequence of leaving them mismatched: raising only the generator's +threshold to `1e-6` while the endpoint function stays at `1e-10` produces a +chordal error of **1.075 radians** across the whole intervening band. A +proposal to widen one side as a fix was evaluated and rejected on this +measurement. + +## F2b — near-pole conditioning limit (found during reproduction) + +After both fixes, agreement is not machine-precision arbitrarily close to a +pole. `compute_spherical_arc_endpoint` recovers `Δazimuth` through a quotient +whose denominator is `sin(θ_start)·sin(θ_end)`; that division loses +significance as `sin(θ_start) → 0`. The generator rotates a Cartesian vector +and does not. The residual is the endpoint function's conditioning. + +Measured worst-case chordal disagreement, shared threshold at `1e-10`: + +| `abs(sin(polar_start))` | worst chordal error | +| --- | --- | +| `1e-4` | 1.4e-13 | +| `1e-5` | 5.8e-12 | +| `1e-6` | 1.3e-10 | +| `1e-7` | 7.0e-10 | +| `1e-8` | 9.9e-09 | +| `1e-9` | 3.4e-08 | +| `1.1e-10` | 4.2e-07 | +| below threshold | exact (shared pole convention) | + +The spec states the resulting bands as normative outcomes. Narrowing the +sliver would require reformulating the endpoint function's `Δazimuth` +computation — not requested, not authorized. + +## F2c — azimuth range split defeats naive comparison + +`compute_spherical_arc_endpoint` normalizes to `[0, 2π)`; the generators +return raw `arctan2` output in `(-π, π]`. For `orient = -2.0` a correct +implementation yields `4.283185…` and `-2.0` — exactly `2π` apart. Any +equality or `assertAlmostEqual` comparison fails on correct code. Verified +live. + +## F2d — a monotonicity test has no power against F2 + +A monotone-traversal check was drafted as a compliance item on the reasoning +that a tangent sign error produces fold-back at the midpoint. Measured against +the live F2 defect: **0 of 200** random arcs produced any fold-back. A sign +flip reverses direction without folding the path. The item was replaced with a +midpoint-agreement check, which does discriminate. + +## F3 — `angular_distance` saturates for non-unit quaternions + +**Location:** `src/math_tools/spatial/spatial_pose.py:242` + +Clips an unnormalized dot product. Two quaternions separated by 60° and scaled +by 3 report **0.0** rather than `1.047198`. Clipping does not sanitize an +unnormalized dot — it saturates it. + +## F3a — `Quaternion.angle` has the same defect (found during review) + +**Location:** `src/math_tools/spatial/quaternion.py:317` + +`2.0 * arccos(clip(self.w, -1.0, 1.0))` reads a raw `w` with no norm division. +A 60° rotation scaled by 5 reports **0.0°**. + +`Quaternion.axis` was checked and is already scale-invariant. + +Authorized by the human on 2026-08-26 to become normative for `angle` only; +`axis` and `interpolate` explicitly left unchanged. + +## F4 — zero-magnitude normalization yields NaN + +| Location | Observed | +| --- | --- | +| `quaternion.py:684` (`from_axis_angle`) | `quaternion(0.8775825618903728, nan, nan, nan)` — finite `w`, three NaN | +| `quaternion.py:293` (`normalized`) | `quaternion(nan, nan, nan, nan)` | +| `quaternion.py` (`inverse`) | `1.0 / q` on the zero quaternion is NaN | + +The `from_axis_angle` case is the worse of the two: a plausible-looking finite +`w` makes the result read as valid at a glance. + +`Position.normalize()` already raises `ValueError` on the zero vector, so the +sibling contract exists and `Quaternion` diverges from it. + +## F5 — the foundation dependency pin is dead + +`requirements.txt:22` pins +`pyFoundationTools @ git+https://github.com/kopecn/py-foundationTools.git@feat/switch-to-new-template`. + +That branch **does not exist on the remote**. Verified: + +``` +$ git ls-remote --heads https://github.com/kopecn/py-foundationTools.git +ada8247… refs/heads/dev +1082ccb… refs/heads/feat/new-features +1216008… refs/heads/main +fc22d72… refs/heads/prod + +$ git ls-remote --tags … +b476033… refs/tags/v0.0.2 +efb630e… refs/tags/v0.0.3 +fc22d72… refs/tags/v0.0.4 +``` + +A clean `uv pip install -r requirements.txt` therefore **fails outright +today**. The repo builds only because the existing `.venv` holds an install of +commit `323e4b09` (per its `direct_url.json`), reachable from a local clone but +from nothing on the remote. The reported green gate — 1,550 tests passing — is +evidence about a stale environment, not about a fresh checkout. + +**Selected pin: `v0.0.4`.** It resolves to `fc22d72`, is tag-reachable, is +identical to `refs/heads/prod`, and carries the post-template +`foundation_abc/math/` layout including `sphericalABCs.py` and +`waveformABCs.py`. `git diff 323e4b09 v0.0.4` restricted to +`src/foundation_abc/` and `src/foundationTypes/mathTypes/` is **empty** — +adopting it is behaviour-preserving, not an upgrade. + +A commit-SHA fallback is unavailable: the only SHA that would serve +(`323e4b09`) is the unreachable object that caused the problem. + +## F6 — the two CI jobs install from different sources + +`Makefile:306` (`uv-test-all`) runs `uv pip install -q -e ".[dev]"`, bypassing +`requirements.txt`. It is the only install path in the Makefile that does so; +`uv-bootstrap`, `uv-sync`, `uv-sync-headless`, `uv-sync-dev`, `uv-refresh`, and +the pip fallback all install `-r requirements.txt` first. + +Consequence: the `compatibility` job resolves `pyFoundationTools` from the +configured index (where it is not published) while `quality` resolves it from +the git pointer. The two jobs can test different code, and post-F5 the +compatibility job cannot resolve the dependency at all. + +## F7 — waveform insertion rejects indices `list.insert` clamps + +**Locations:** `waveform1d.py:812`, `waveform_position.py:324`, +`waveform_quaternion.py:352`, `waveform_spatial_pose.py:620` — all bare +`np.insert`. + +`np.insert` raises `IndexError` outside `[-n, n]`; `list.insert` clamps. +Measured on a 3-sample waveform, `insert(99, x)` raises on all four classes, +where `[0,1,2].insert(99, 9)` appends. + +None of the four `insert` methods carries a docstring, unlike `pop()` beside +each, which documents its `IndexError`. + +## F8 — spatial deserializers rely on assertions + +| Location | `from_dict()` raises | +| --- | --- | +| `position.py:149` | `AssertionError` | +| `spatial_pose.py:118` | `AssertionError` | +| `quaternion.py:829` | `AttributeError` (no `isinstance` check at all) | + +Under `python -O` the asserts are stripped and `Position.from_dict` degrades to +`AttributeError` — verified live. Two duplicate assert-based numeric coercers +exist: `Position._from_float` (`position.py:53`) and `Quaternion.from_float` +(`quaternion.py:47`). + +--- + +# Material for the implementation plan + +Displaced from the specs during normalization. This is plan input, not +contract. + +## Index-clamping recipe (F7) + +Clamp before delegating to the array helper, in each of the four `insert` +methods: + +```python +n = len(self._values) +if index < 0: + index = max(0, n + index) +else: + index = min(index, n) +``` + +`WaveformSpatialPose.insert` clamps **once** and applies the same index to +both parallel arrays, so `len(positions) == len(quaternions)` is preserved. A +per-array clamp would desynchronize them. + +## Shared deserializer helper (F8) + +New private module `src/math_tools/spatial/_serde.py`, replacing +`Position._from_float` and `Quaternion.from_float`. Private: leading +underscore, no `__all__` entry, no public-surface change. + +While editing the three `from_dict` methods, unify their signatures: +`Position` and `SpatialPose` use `from_dict(cls: type[T], obj: Any) -> T`; +`Quaternion.from_dict(cls, obj: Any) -> Quaternion` hardcodes the return type +and breaks subclass covariance. `Quaternion` adopts the `TypeVar` form. + +## Docstring edit targets (spherical) + +`spherical_generators.py` describes `orient` as a right-hand-rule rotation +about the radial vector at lines 169–171, 181–182, 202, 209, and 226 — replace, +do not append. Line 227 already reads "orient=0 points north, positive orient +rotates towards east", so the file contradicts itself; the compass wording is +the one that stays. The module docstring (lines 7–36) documents θ and φ but +never defines `orient` at all. + +`arc_from_two_points`' docstring claims the `isPositive=False` arc "reaches +toward (azimuth2, polar2)". It does not. + +Suggested check after the edit: +`grep -niE "right.hand|thumb|fingers curl" src/math_tools/spherical/` returns +nothing. + +## Shared-constant check (spherical) + +`spherical_generators.py:132` carries an unrelated, legitimate +`axis_norm > 1e-10` (a Rodrigues axis-degeneracy guard in the small-circle +loop). A bare `grep 1e-10` cannot distinguish it from a pole literal, so the +conformance check must be line-scoped to lines involving `sin(`. + +## Test material + +- Regression case for F1: `(0.5, 0.8) → (1.2, 1.0)`. +- Non-discriminating control worth asserting as such: a `θ₁ = θ₂` pair, which + both the correct and transposed formulas pass. +- Mandatory case for F2c: `orient = -2.0`, which exposes a naive `==`. +- Literal scale cases: 60°/scale-3 for `angular_distance`, 60°/scale-5 for + `Quaternion.angle`. +- `tests/spherical/` currently holds only `test_public_surface.py`; geometry + test files do not yet exist. +- Gate: `make uv-fullCheck`. + +## Downstream consumers to re-check after the spherical fixes + +`examples/sphericalPlotting/plotArcs.py` chains +`compute_spherical_arc_endpoint` into `arc_from_two_points`. Note it is **not** +a discriminating check: its inputs land on `Δφ = -π/2` with `θ₂ = 0`, where +both bearing formulas return 0. + +`src/math_plot_helpers/plot_unit_spherical.py:878-892` holds demo arcs whose +expected directions are only consistent with the compass reading of `orient`. + +## Recorded, not actioned + +- `Makefile` `uv-sync-release` installs `requirements-release.txt`, which does + not exist; the target fails if run. Human decision 2026-08-26: leave it, + report only. +- `Quaternion` keeps `fromNumpyQuaternion` / `to_unitSphericalSmallCircle` as + permanent aliases with no `DeprecationWarning` and no sunset path. +- The Tier-2 ABC docstring `UnitSphericalArcABC.orient` ("Rotated orientation + about the origin->start vector") carries the right-hand-rule wording that + conflicts with the compass convention. Fixing it is a py-foundationTools + change; worth filing upstream. + +## Test vectors and verification procedure + +Displaced from spec compliance sections, which state outcomes rather than the +inputs that demonstrate them. + +### Waveform insertion (F7) + +Differential vector against stdlib. On a 3-sample waveform, for +`i ∈ {-99, -4, -3, -1, 0, 1, 2, 3, 4, 99}`, the resulting order matches the +identical `list.insert` call on `[0, 1, 2]` and nothing raises. Boundary +illustrations: + +``` +[1, 2, 3].insert(99, 9) → [1, 2, 3, 9] +[1, 2, 3].insert(-99, 0) → [0, 1, 2, 3] +``` + +`WaveformSpatialPose` is additionally checked for equal parallel-array lengths +after a clamped insertion. + +### Spherical geometry + +- Randomized items use an explicitly seeded generator (`np.random.default_rng` + with a literal seed) so failures reproduce. +- Constructor round-trip coverage must include a pair with `Δφ ∉ {0, π}` and + `θ₁ ≠ θ₂`. Coincident-value families do not discriminate the contracted + formula from the transposed one (F1), so a suite built only from + equal-colatitude or antimeridian pairs passes against the defect. +- Generator/endpoint agreement is exercised in both accuracy bands: the + guaranteed band and the near-pole sliver. +- Pole agreement is exercised with at least one negative `orient`, which is + where the azimuth-range split (F2c) makes a naive equality comparison fail + on correct code. +- `POLE_EPS` conformance is checked line-scoped to bearing-resolving code, not + by a bare literal grep — see the `spherical_generators.py:132` caveat above. + +### Spatial math + +- Scale invariance: literal cases 60°/scale-3 for `angular_distance` and + 60°/scale-5 for `Quaternion.angle`, plus scale factors below 1. +- Deserializer suite runs twice, once under `python -O`. + +### Packaging + +Pin and install-path conformance is verified against a **fresh** environment. +An existing working environment is not evidence: it can hold an install of a +ref that is no longer remotely reachable, which is precisely what masked F5. + +## Implementation notes displaced from spec prose + +- **Scale invariance (F3, F3a):** divide by the operand norms, or normalize, + *before* the range-limiting step. Clipping an unnormalized dot product or a + raw `w` component to `[-1, 1]` saturates rather than sanitizes. +- **Near-pole bands (F2b):** the bound originates in + `compute_spherical_arc_endpoint`'s `Δazimuth` recovery, whose denominator + carries a factor of `sin(θ_start)`. The generator rotates a Cartesian vector + and does not share the conditioning. +- **`assert` on deserializer paths (F8):** prohibited, because assertions are + stripped under `python -O`, turning a rejected payload into a silently + constructed object. The spec states the equivalent outcome (behaviour + identical with assertions disabled) rather than the mechanism ban. diff --git a/.claude/specs/mathToolsArchitecture.md b/.claude/specs/mathToolsArchitecture.md index 6bac934..948740d 100644 --- a/.claude/specs/mathToolsArchitecture.md +++ b/.claude/specs/mathToolsArchitecture.md @@ -5,10 +5,10 @@ name: mathToolsArchitecture purpose: Layering, package layout, dependency policy, and shared conventions for py-MathTools spec: MathToolsArchitecture scope: project -status: accepted +status: draft applies_to: src/, tests/, pyproject.toml -last_updated: 2026-07-11 -semver: 0.0.2 +last_updated: 2026-08-26 +semver: 0.1.0 author: Nicholas Bergantz --- @@ -70,9 +70,8 @@ per the template BKM (see [templateConformance.md](templateConformance.md)). ## Packages and layering -Two top-level snake_case packages under `src/` (replacing `pyMathTools` and -`pyMathToolsPlotHelpers` — see the rename contract in -[templateConformance.md](templateConformance.md)): +Two top-level snake_case packages under `src/` (naming rules in +[templateConformance.md](templateConformance.md) §Package and module naming): ``` math_plot_helpers → math_tools → pyFoundationTools → stdlib @@ -87,11 +86,11 @@ No reverse imports. `matplotlib` never appears in `math_tools`. src/math_tools/ __init__.py # curated public re-exports (see Public surface) py.typed - hints.py # shared type aliases (migrated from pyMathTools/hints.py) + hints.py # shared type aliases errors.py # exception hierarchy (see Error semantics) precision_time/ # PrecisionTimeInterval, PrecisionTimestamp → precisionTimeMath.md spatial/ # Position, Quaternion, SpatialPose → spatialMath.md - spherical/ # migrated spherical arc/circle utilities (behavior unchanged) + spherical/ # unit-sphere arcs/circles/transforms → sphericalGeometry.md waveforms/ # Waveform1D + aggregates → waveformCore.md dsp/ # DSP mixin per family → waveformDsp.md support.py # DSP descriptor dataclasses/enums → waveformDsp.md @@ -99,8 +98,7 @@ src/math_tools/ otg/ # online trajectory generation (Ruckig port) → otg.md ``` -`src/math_plot_helpers/` holds the migrated plotting module(s); rename-only, -no behavioral change in this effort. +`src/math_plot_helpers/` holds the plotting module(s). ## Shared conventions (inherited by every sibling spec) @@ -135,8 +133,7 @@ no behavioral change in this effort. - **Error semantics:** see below. - **Testing:** `unittest.TestCase` style run under pytest (repo convention, `tests/test*.py`); tests mirror the package layout - (`tests/spatial/test_position.py`, …). TDD per chunk. Gate: - `make uv-fullCheck`. + (`tests/spatial/test_position.py`, …). - **Docstrings:** every public symbol; module docstrings state conventions (e.g. the ISO spherical convention doc in the existing `spherical/` module stays canonical). @@ -171,17 +168,18 @@ class PolynomialSolveError(MathToolsError): ... # unsolvable/ill-posed r 4. **Swift stubs are not ported:** `Waveform1D+Custom`, the empty `SpatialWaveforms/Operators/*+Arithmetic` files, and the empty `PrecisionTime/Extensions` files define no behavior. -5. **Serialization file I/O helpers** (Swift's CSV/JSON `load/save`) are not - part of this effort; wire interop is `to_dict`/`from_dict` (foundation's - `DataModelHelper` owns file I/O patterns). +5. **No serialization file I/O helpers.** Wire interop is + `to_dict`/`from_dict`; foundation's `DataModelHelper` owns file I/O + patterns. ## Sibling spec index | Spec | Contract | |---|---| -| [templateConformance.md](templateConformance.md) | template migration: packaging, Makefile/CI parity, rename, governance | +| [templateConformance.md](templateConformance.md) | packaging, dependency resolvability, CI parity, governance artifacts | | [precisionTimeMath.md](precisionTimeMath.md) | `PrecisionTimeInterval`, `PrecisionTimestamp` | | [spatialMath.md](spatialMath.md) | `Position`, `Quaternion`, `SpatialPose` | +| [sphericalGeometry.md](sphericalGeometry.md) | unit-sphere arcs/small circles, transforms, `orient` convention | | [waveformCore.md](waveformCore.md) | `Waveform1D` + aggregate waveform containers | | [waveformDsp.md](waveformDsp.md) | DSP families, scipy mapping, support types | | [polynomials.md](polynomials.md) | polynomial type + analytic root solvers | diff --git a/.claude/specs/spatialMath.md b/.claude/specs/spatialMath.md index 5467715..6311158 100644 --- a/.claude/specs/spatialMath.md +++ b/.claude/specs/spatialMath.md @@ -5,10 +5,10 @@ name: spatialMath purpose: Behavioral contract for the Position, Quaternion, and SpatialPose math types spec: SpatialMath scope: project -status: accepted +status: draft applies_to: src/math_tools/spatial/, tests/spatial/ -last_updated: 2026-07-11 -semver: 0.0.2 +last_updated: 2026-08-26 +semver: 0.1.1 author: Nicholas Bergantz --- @@ -24,7 +24,7 @@ author: Nicholas Bergantz | Class | Module | Subclasses | Storage | |---|---|---|---| | `Position` | `spatial/position.py` | `PositionABC` | `np.ndarray` shape `(3,)` float64 | -| `Quaternion` | `spatial/quaternion.py` (migrated existing class) | `QuaternionABC` (already) | `numpy-quaternion` (already) | +| `Quaternion` | `spatial/quaternion.py` | `QuaternionABC` | `numpy-quaternion` | | `SpatialPose` | `spatial/spatial_pose.py` | `SpatialTransformABC` | composed `Position` + `Quaternion` | `Quaternion` is the **existing, tested implementation** (~90 tests). Its @@ -59,6 +59,54 @@ Beyond that, this spec only adds the members under "Quaternion additions". - `__array__(dtype=None)` on `Position` (→ `(3,)` vector) and `Quaternion` (→ `[w, x, y, z]`), so `np.asarray(x)` / `plt.plot(...)` work directly. +### Deserializer error semantics (normative) + +`from_dict` is public API reachable from untrusted wire data, so its input +validation is behavior rather than a developer-time check. Its outcomes SHALL +be identical whether or not assertions are enabled. + +Per [mathToolsArchitecture.md](mathToolsArchitecture.md) §Error semantics +(programmer errors raise stdlib `TypeError`/`ValueError`), every `from_dict` on +`Position`, `Quaternion`, and `SpatialPose` SHALL: + +- raise `TypeError` when the payload is not a `dict`, or when a field's value + is not a real number (`bool` is rejected: it is an `int` subclass and is not + a coordinate); +- raise `ValueError` when a required key is absent; +- carry a message naming the class, the key, and the offending type. + +All three classes SHALL apply the same numeric-field rule and raise the same +exception type, with a message of the same form, for the same malformed +payload. Validation behavior SHALL NOT vary by class, and the rule SHALL NOT +appear on any public surface. + +All three SHALL expose `from_dict` as `(cls: type[T], obj: Any) -> T`, so a +subclass deserializes to its own type. + +### Non-unit quaternion inputs (normative) + +Quaternion magnitude is not part of the rotation a quaternion represents. +Two APIs return a rotation *angle* derived from quaternion components, and +both MUST be invariant to a positive rescaling of their inputs: + +- `SpatialPose.angular_distance(to)` — `f(αq₁, βq₂) == f(q₁, q₂)` for all + `α, β > 0`. +- `Quaternion.angle` — `(αq).angle == q.angle` for all `α > 0`. + +This section governs those two members only. `Quaternion.axis` is +scale-invariant by construction and `interpolate` is outside this contract; +neither is governed here. + +### Degenerate (zero) magnitude (normative) + +A zero vector has no direction and a zero quaternion has no rotation. Neither +is representable, so producing `nan` components is prohibited: the operation +MUST raise `ValueError` at the point of the invalid input, before a corrupt +value can propagate into a pose, a waveform, or serialized output. This applies +uniformly to `Position` and `Quaternion`: the zero-vector rule stated for +`Position.normalize()` under §`Position` and the zero-quaternion rule under +§`Quaternion` are one contract, not two. + ## `Position` Mutable components (`p.x = 1.0` allowed), matching the existing `Quaternion` @@ -102,7 +150,9 @@ Swift's custom `•`/`×` operators map to the named methods `dot`/`cross` plus ## `Quaternion` additions -Added to the existing class (plus the ABC re-parent above; no other change): +The members below are part of the `Quaternion` contract. Items 1–4 are +established and unchanged by this revision; items 5–6 are the requirements this +revision adds. 1. `dot(other) -> float` — 4-component dot product. 2. `rotation_matrix_elements -> RotationMatrixElements` — frozen dataclass @@ -112,11 +162,20 @@ Added to the existing class (plus the ABC re-parent above; no other change): `rotate_vector`, typed for `Position` (`q * p` NOT overloaded; explicit method only, to avoid ambiguity with the existing quaternion `*`). 4. `__array__(dtype=None)` per the cross-cutting conventions. -5. snake_case aliases for the two camelCase slips on the public surface: +5. **Degenerate-input guards** (§Degenerate (zero) magnitude). Each of + `normalized()`, `normalize()`, `inverse`, and + `from_axis_angle(axis, angle)` SHALL raise `ValueError` on a zero-magnitude + operand — a zero quaternion for the first three, a zero axis for the last. + None SHALL return a non-finite component. + + The condition is exact equality with zero, not a tolerance: a very small + but nonzero quaternion has a well-defined direction and SHALL be accepted. + + Each of the four docstrings SHALL carry a `Raises:` section. +6. snake_case aliases for the two camelCase slips on the public surface: `from_numpy_quaternion` (= `fromNumpyQuaternion`) and - `to_unit_spherical_small_circle` (= `to_unitSphericalSmallCircle`); the - camelCase originals remain as deprecated aliases (docstring note, no - removal in this effort). + `to_unit_spherical_small_circle` (= `to_unitSphericalSmallCircle`). The + camelCase spellings remain available and are documented as deprecated. ## `SpatialPose` @@ -159,7 +218,9 @@ robotics reading) parity pinned by test. - `position_distance(to)`, `position_distance_squared(to)`, `angular_distance(to) -> float` (radians, double-cover safe: uses - `min(θ, 2π−θ)` via `|dot|`). + `min(θ, 2π−θ)` via `|dot|`). Scale-invariant per §Non-unit quaternion inputs. + Raises `ValueError` if either orientation has zero norm (§Degenerate (zero) + magnitude). - `normalize()` / `normalized()` — normalize orientation only. - `==` exact, `isclose(other, rtol, atol)` (double-cover aware via `Quaternion.isclose`), `repr`, `to_dict`/`from_dict`. @@ -168,11 +229,10 @@ robotics reading) 1. All three classes satisfy `isinstance` of their ABC and round-trip `to_dict`/`from_dict` against the matching `foundationTypes` generated - Type's wire output — exact target names on the pinned branch: - `PositionType`, `QuaternionType`, `SpatialTransformType` - (NOT the pre-template `PositionVectorType`/`SpatialPoseType`; refresh the - venv to the branch pin first, per - [templateConformance.md](templateConformance.md)). + Type's wire output. The target names are `PositionType`, + `QuaternionType`, and `SpatialTransformType`; verification is meaningful + only against an environment matching the current foundation pin + ([templateConformance.md](templateConformance.md) §Environment currency). 2. Coordinate inits/accessors are mutual inverses (property-style tests over a grid of radii/angles, both spherical conventions, atol 1e-12). 3. `cross` follows the right-hand rule (`unit_x × unit_y == unit_z` pinned). @@ -190,3 +250,20 @@ robotics reading) 10. `__hash__ is None` pinned for all three classes; `np.asarray(x)` returns the documented array for `Position` and `Quaternion`. 11. mypy strict clean. +12. **Scale invariance.** For any positive scale factor applied to either + operand, `SpatialPose.angular_distance` and `Quaternion.angle` are + unchanged to atol 1e-12, as is `Quaternion.axis`. Compliance 7 (`q` vs + `-q` → 0) is unaffected. +13. **No NaN escape.** `Quaternion.normalized()`, `normalize()`, `inverse`, + and `from_axis_angle` raise `ValueError` on a zero-magnitude operand, and + each carries a `Raises:` docstring section. For finite non-degenerate + input, none of the four returns a non-finite component. +14. **Deserializer errors.** For each of `Position`, `Quaternion`, + `SpatialPose`: a non-`dict` payload raises `TypeError`; a payload missing a + required key raises `ValueError`; a field carrying a string or a `bool` + raises `TypeError`. The three classes raise the same type for the same + malformed payload, and every outcome is unchanged when assertions are + disabled. All three `from_dict` docstrings carry a `Raises:` section. +15. **Wire round-trip preserved.** `to_dict` → `from_dict` remains exact for + all three classes: the validation required by 13–14 rejects malformed + input without narrowing well-formed input. diff --git a/.claude/specs/sphericalGeometry.md b/.claude/specs/sphericalGeometry.md new file mode 100644 index 0000000..ab66667 --- /dev/null +++ b/.claude/specs/sphericalGeometry.md @@ -0,0 +1,141 @@ +--- +version: 1.0 +type: specification +name: sphericalGeometry +purpose: Behavioral contract for math_tools.spherical — arc/small-circle construction, generation, and coordinate transforms on the unit sphere +spec: SphericalGeometry +scope: project +status: draft +applies_to: src/math_tools/spherical/, tests/spherical/ +last_updated: 2026-08-26 +semver: 0.1.1 +author: Nicholas Bergantz +--- + +# Spherical Geometry + +> Sibling of [mathToolsArchitecture.md](mathToolsArchitecture.md), which owns layering, dependency policy, error semantics, and the shared API idioms this spec inherits. Reproduced defect evidence and implementation material are held in [the 2026-08-26 findings](../findings/2026-08-26-spherical-spatial-packaging.md). + +## Role + +`math_tools.spherical` is the Tier-3 implementation of the unit-sphere primitives whose Tier-1 carriers are `foundationTypes.mathTypes.MathTypes.UnitSphericalArcType` / `UnitSphericalSmallCircleType` and whose Tier-2 contracts are `foundation_abc.math.sphericalABCs.UnitSphericalArcABC` / `UnitSphericalSmallCircleABC`. It owns no storage type: every public entry point takes or returns an ABC-typed arc/circle, a scalar coordinate pair, or a numpy array of them. + +## Coordinate convention + +ISO physics convention. The `spherical_generators` module docstring is the canonical statement; other modules reference it rather than restating it. + +- **θ (polar)** — colatitude from `+z`, range `[0, π]`. `0` = north pole, `π/2` = equator, `π` = south pole. +- **φ (azimuth)** — angle in the xy-plane from `+x`, counterclockwise viewed from `+z`. +- `x = sin θ cos φ`, `y = sin θ sin φ`, `z = cos θ`. + +Azimuth output ranges differ by function and are part of each function's contract: `compute_spherical_arc_endpoint` returns `[0, 2π)`; `cartesian_to_spherical`, `generate_spherical_arc_points`, and `generate_spherical_small_circle_points` return `arctan2` range `(-π, π]`. Consumers comparing azimuths across these functions SHALL compare modulo `2π` (§Compliance 0). + +## The `orient` convention + +`orient` SHALL be an initial great-circle bearing measured from local north, positive toward local east (the direction of increasing azimuth): + +- `orient = 0` → travel toward the north pole (decreasing θ). +- `orient = +π/2` → travel due east (increasing φ). +- `orient = ±π` → travel toward the south pole (increasing θ). + +With `p̂` the outward radial at the start point, `ê = ẑ × p̂ / ‖ẑ × p̂‖` (local east) and `n̂ = p̂ × ê` (local north), the initial tangent direction SHALL be + +``` +d̂ = cos(orient) · n̂ + sin(orient) · ê +``` + +This convention governs every function in this subpackage, and public docstrings state it at the call site (§Compliance 10). + +It diverges in sign from the wording of the inherited Tier-2 `UnitSphericalArcABC.orient` docstring. Tier-2 states no formula, so no implemented upstream contract is contradicted; reconciling the wording is owned by py-foundationTools. + +## Degeneracy handling + +Bearing is undefined at a pole. Every function resolving a bearing SHALL treat the start point as degenerate under one shared predicate: + +``` +POLE_EPS = 1e-10 +degenerate ⇔ abs(sin(polar)) < POLE_EPS +``` + +- Every bearing-resolving code path SHALL classify a point as degenerate under exactly this predicate, at exactly this threshold. `spherical_generators` and `spherical_transforms` SHALL agree on the classification of every input: no point is degenerate to one and non-degenerate to the other. +- At a degenerate start point the azimuth reference SHALL be the fixed `x̂`/`ŷ` basis, yielding `azimuth_end == orient` at both poles. This is definitional, not approximate. +- `generate_spherical_small_circle_points` resolves no bearing: a small circle is rotationally symmetric about its center, so its output does not depend on a choice of in-plane reference direction. It is not governed by `POLE_EPS`, and §Compliance 7 holds for every center position. + +### Accuracy bands near the pole + +Agreement between `generate_spherical_arc_points` and `compute_spherical_arc_endpoint` is bounded by the start point's proximity to a pole. The following bounds are normative: + +| Start point | Guaranteed chordal agreement | +| --- | --- | +| `abs(sin(polar_start)) ≥ 1e-6` | `< 1e-9` | +| `POLE_EPS ≤ abs(sin(polar_start)) < 1e-6` | `< 1e-6` | +| `abs(sin(polar_start)) < POLE_EPS` | exact (shared pole convention) | + +## Public surface + +Seven functions, re-exported from `math_tools/spherical/__init__.py` with `__all__` per the umbrella's Public surface rule. + +### `spherical_transforms` + +- `plate_carree_transform(azimuth, polar) -> (x, y)` — `x = azimuth`, `y = π/2 − polar`. Accepts float or ndarray via `FloatOrNDArray`. +- `spherical_to_cartesian(azimuth, polar) -> (x, y, z)` — unit-sphere point per §Coordinate convention. +- `cartesian_to_spherical(point) -> (azimuth, polar)` — normalizes `point`, then `azimuth = arctan2(y, x)`, `polar = arccos(clip(z, −1, 1))`. +- `compute_spherical_arc_endpoint(arc) -> (azimuth_end, polar_end)` — the reference implementation of the arc contract (§Cross-function consistency). Spherical law of cosines for `polar_end`; `arctan2` for `Δazimuth`; degenerate start → `azimuth_end = orient`; degenerate end → `azimuth_end = arc.azimuth`; result in `[0, 2π)`. + +### `spherical_generators` + +- `generate_spherical_small_circle_points(circle, num_points=120) -> (azimuths, polars)` — `num_points` samples over `t ∈ [−π, π]`, each at exactly `circle.radius_angle` from the center. +- `generate_spherical_arc_points(arc, num_points=120) -> (azimuths, polars)` — `num_points` samples over `t ∈ [0, arc.arc_length]`, inclusive at both ends, along the great circle leaving `(arc.azimuth, arc.polar)` on bearing `arc.orient` per §The `orient` convention. Negative `arc_length` traverses the opposite bearing. + +### `constructors` + +- `arc_from_two_points(azimuth1, polar1, azimuth2, polar2, isPositive=True) -> UnitSphericalArcABC` — an arc rooted at point 1 whose bearing and length carry it to point 2: + + ``` + arc_length = arccos(clip(cos θ₁ cos θ₂ + sin θ₁ sin θ₂ cos Δφ, −1, 1)) + orient = atan2( sin Δφ · sin θ₂ , + sin θ₁ · cos θ₂ − cos θ₁ · sin θ₂ · cos Δφ ) + ``` + + Both expressions are part of the contract, including the order of terms in the `orient` denominator. + + `isPositive=False` negates `arc_length` and leaves `orient` unchanged, producing the reverse traversal from point 1. That arc does not reach point 2, and its docstring SHALL say so. + +## Cross-function consistency + +`compute_spherical_arc_endpoint` is the single source of truth for where an arc ends. The other two arc functions are defined by agreement with it: + +1. **Generator ≡ endpoint.** `generate_spherical_arc_points(arc, n)`'s final sample SHALL equal `compute_spherical_arc_endpoint(arc)`, compared per §Compliance 0 and bounded by §Accuracy bands near the pole. +2. **Constructor round-trip.** `compute_spherical_arc_endpoint(arc_from_two_points(φ₁, θ₁, φ₂, θ₂))` SHALL equal `(φ₂ mod 2π, θ₂)` on the same terms. + +A change that breaks either invariant is a regression regardless of how the individual formula reads. + +## Error semantics + +Inherited from [mathToolsArchitecture.md](mathToolsArchitecture.md) §Error semantics. This subpackage raises no domain errors: inputs are floats or ABC-typed carriers, every `arccos` argument is clipped to `[−1, 1]`, and every normalization divisor is guarded by §Degeneracy handling. No `MathToolsError` subtype applies. + +## Compliance requirements + +Stable observable outcomes of a conforming implementation. + +0. **Comparison metric.** Agreement between two `(azimuth, polar)` results is measured as chordal error, with the azimuth difference taken modulo `2π` as `abs(((a − b + π) % (2π)) − π)` and weighted by `sin(polar)`. Every tolerance below is expressed in this metric. +1. **Constructor round-trip.** For any `(φ₁, θ₁, φ₂, θ₂)` with `abs(sin θ) ≥ 1e-6`, `compute_spherical_arc_endpoint(arc_from_two_points(φ₁, θ₁, φ₂, θ₂))` recovers `(φ₂, θ₂)` to `< 1e-9`. +2. **Generator ≡ endpoint.** `generate_spherical_arc_points`' final sample matches `compute_spherical_arc_endpoint` for the same arc, within the bounds of §Accuracy bands near the pole, for `arc_length ∈ [−π, π]`. +3. **North-tangent orientation.** For an equatorial start, `orient = 0`, and `0 < L ≤ π/2`, `polar(s) = π/2 − s` for `s ∈ [0, L]`. +4. **Pole agreement.** For start `polar ∈ {0, π}`, the generator's final sample matches `compute_spherical_arc_endpoint` exactly, and `azimuth_end ≡ orient (mod 2π)`, for both positive and negative `orient`. +5. **Uniform degeneracy predicate.** For any point, `spherical_generators` and `spherical_transforms` return the same degeneracy classification, and that classification is the §Degeneracy handling predicate at its stated threshold. No bearing-resolving path applies a different threshold. +6. **Midpoint agreement.** The generator's sample at parameter `L/2` matches `compute_spherical_arc_endpoint` of the same arc with `arc_length = L/2`, to `< 1e-9`. +7. **Small-circle radius.** Every sample of `generate_spherical_small_circle_points` sits at `radius_angle` from the center to atol `1e-12`, for centers anywhere on the sphere including at a pole. +8. **Transform inverses.** `cartesian_to_spherical ∘ spherical_to_cartesian` is the identity away from the poles to atol `1e-12`. `plate_carree_transform` maps `polar = 0 → y = π/2` and `polar = π → y = −π/2`. +9. **Public surface.** `math_tools.spherical.__all__` pins exactly the seven names, each resolving by identity to its defining module. +10. **Docstrings carry the convention.** The `spherical_generators` module docstring defines `orient` per §The `orient` convention. Each arc function's docstring states that convention and notes its divergence from the inherited `UnitSphericalArcABC.orient` wording. No docstring or comment in the subpackage describes `orient` as a rotation about the radial vector. +11. mypy strict clean. + +## Non-goals + +1. **No new spherical types.** Tier-1 carriers stay upstream; this subpackage defines no arc/circle class. +2. **No azimuth-range harmonization.** The per-function ranges in §Coordinate convention are contract, not drift. +2. **No upstream ABC docstring change.** Owned by py-foundationTools. +3. **No small-circle degeneracy rework.** See §Degeneracy handling. +4. **No reformulation of `compute_spherical_arc_endpoint`.** The bands in §Accuracy bands near the pole are the contract; tightening them is a separate governed change. +5. **No `isPositive` redesign.** Reverse-traversal semantics are contract. diff --git a/.claude/specs/templateConformance.md b/.claude/specs/templateConformance.md index 942f8cc..3b97cf8 100644 --- a/.claude/specs/templateConformance.md +++ b/.claude/specs/templateConformance.md @@ -5,107 +5,97 @@ name: templateConformance purpose: Bring py-MathTools into full conformance with the py-foundationTools template conventions spec: TemplateConformance scope: project -status: accepted +status: draft applies_to: pyproject.toml, requirements.txt, Makefile, .env, .github/, .claude/, src/, README.md -last_updated: 2026-08-14 -semver: 0.0.4 +last_updated: 2026-08-26 +semver: 0.1.1 author: Nicholas Bergantz --- # Template Conformance -> Sibling of [mathToolsArchitecture.md](mathToolsArchitecture.md). The -> authoritative template reference is py-foundationTools at branch -> `feat/switch-to-new-template` -> (`/Users/nbergantz/__Workspaces__/pythonWorkspaces/py-foundationTools`); -> this spec states what "conformant" means for this repo, not a copy of that -> repo's content. - -## Already conformant (verify, don't re-do) - -The current branch already carries: the uv Makefile (with `uv-fullCheck` -gate), `.env` (`PY_SRC=src`), `.bumpversion.cfg`, the `ci-cd.yml` workflow -(quality checks + Python compatibility matrix on PRs to `dev`/`prod`), -names-only `pyproject.toml` policy, and pinned `requirements.txt`. -Conformance work MUST NOT rewrite these wholesale; only close the specific -gaps below. - -## Gap 1 — package rename (breaking, approved) - -| Current | Target | -|---|---| -| `src/pyMathTools/` | `src/math_tools/` | -| `src/pyMathToolsPlotHelpers/` | `src/math_plot_helpers/` | - -Requirements: - -1. `git mv` the trees; module layout inside follows the umbrella's module map - (existing `spatial/`, `spherical/`, `hints.py` migrate; `Quaternion.py` → - `spatial/quaternion.py`; `plotUnitSpherical.py` → - `math_plot_helpers/plot_unit_spherical.py`). Module **filenames** become - snake_case; class names are unchanged. -2. Behavior of migrated code is unchanged in the rename chunk — imports and - paths only ([stay-in-scope]). Extensions to migrated classes come later - under their own sibling specs. -3. All imports updated: `tests/`, `examples/`, intra-package. -4. Every package dir (`math_tools`, each subpackage with public API, - `math_plot_helpers`) contains `py.typed`. -5. `pyproject.toml` needs no `packages` edit (`package-dir = {"" = "src"}` - auto-discovers), but the Makefile's mypy package list (`MYPY_PKGS` pattern - from the template) and any name references MUST resolve to the new names. -6. Distribution name stays `py_math_tools`; only import packages rename. - -## Gap 2 — dependencies - -1. `pyproject.toml` `dependencies`: names only — - `pyFoundationTools`, `numpy`, `scipy`, `numpy-quaternion`, `matplotlib`. -2. `requirements.txt`: keep the `pyFoundationTools @ git+...@feat/switch-to-new-template` - pin until foundation tags a release, then move to a tag pin (tracked as a - known follow-up, not part of this effort). -3. No `uv.lock` committed (repo BKM). -4. **Hard prerequisite for every other chunk:** the installed `.venv` may - hold a pre-template `pyfoundationtools` (old per-type ABC layout, - `foundationTypes.mathTypes.quaternionABC` etc.). The first chunk MUST - `make uv-refresh` (or equivalent) so the environment matches the branch - pin's layout (`foundation_abc.math.*`, `PositionType`, - `SpatialTransformType`, `ScalarWaveformType`); the gate is meaningless - against the stale install. - -## Gap 3 — `.claude/` governance - -1. `.claude/CLAUDE.md` authored for this repo: role (Tier 3 of foundation's - math tiers), package map, gate command, pointer to `.claude/specs/`. - It references specs — never duplicates their content. -2. `.claude/specs/` — this spec set. -3. `.claude/archive/math-tools-port/` — the completed chunk set produced - from these specs. - -## Gap 4 — README and metadata - -1. `README.md` replaces the "Python Boilerplate" stub, following the - template's section shape: title → Features → Installation → Quick Start → - Development Workflows → Requirements. Content MUST describe what actually - exists at the time the chunk runs (no aspirational feature lists). -2. `pyproject.toml` `description` updated from boilerplate text. -3. `examples/sphericalPlotting/*` imports fixed to real module paths (note: - `plotArcs.py` currently imports a nonexistent - `foundationTypes.mathTypes.UnitSphericalArc` path). - -## Gap 5 — layering test - -Port the template's layering-enforcement pattern -(py-foundationTools `tests/test_package_layering.py`) as -`tests/test_package_layering.py` asserting, via import/AST scan of `src/`: +> Sibling of [mathToolsArchitecture.md](mathToolsArchitecture.md). This spec states what "conformant to the py-foundationTools template" means for this repo; it does not copy that repo's content. The authoritative template reference is the py-foundationTools release selected by `requirements.txt`. +> +> The migration that first brought this repo into conformance is recorded in [`../archive/math-tools-port/`](../archive/math-tools-port/00-overview.md). Reproduced dependency-resolution evidence is held in [the 2026-08-26 findings](../findings/2026-08-26-spherical-spatial-packaging.md). + +## Package and module naming + +- Import packages are snake_case: `math_tools` and `math_plot_helpers` under `src/`. +- Module filenames are snake_case; class names are not affected by this rule. +- The distribution name is `py_math_tools` and is independent of the import package names. +- Every package directory exposing a public API ships `py.typed`. +- Package discovery is driven by `package-dir = {"" = "src"}`; no explicit `packages` enumeration is maintained. +- Tooling configuration that names packages or paths resolves to the names above. + +## Dependency declaration model + +This model is the repo BKM: only the application layer pins, so module-level pins and version-control URLs in `pyproject.toml` are prohibited. + +1. `pyproject.toml` `dependencies` declares **names only** — no version specifier, no `@`, no `git+` URL. +2. `requirements*.txt` carries every pin and version-control pointer. It is the authoritative resolution source for `pyFoundationTools`. +3. No lockfile is committed. + +## Dependency resolvability + +### Pin immutability + +The `pyFoundationTools` pointer in `requirements.txt` SHALL name an immutable, remotely reachable ref — a release tag or a full 40-character commit SHA. A branch name SHALL NOT be used. + +The pinned ref SHALL supply the full import surface this repo depends on: + +- `foundation_abc.math.{mathEnums, precisionTimeABC, spatialABCs, sphericalABCs, waveformABCs}` +- `foundationTypes.mathTypes.MathTypes.{PositionType, QuaternionWaveformType, PositionWaveformType, PrecisionTimeIntervalType, PrecisionTimestampType, ScalarWaveformType, SpatialTransformType, SpatialTransformWaveformType, UnitSphericalArcType, UnitSphericalSmallCircleType}` + +The declared environment SHALL be reproducible from `requirements.txt` alone, independently of any environment already installed. + +### Install-path parity + +Every install path that runs tests SHALL resolve `pyFoundationTools` from `requirements.txt`, installing the requirements file before the editable self-install. No target that installs the package's own extras without `requirements.txt` may run tests. + +Where two CI jobs install by different paths, both SHALL resolve `pyFoundationTools` to the same version. + +### Distribution consumption + +Built artifacts carry only the names-only requirement from `pyproject.toml` metadata; `requirements.txt` is not part of wheel metadata. Resolving `pyFoundationTools` is therefore the consumer's responsibility — a consequence of §Dependency declaration model, not an exception to it. + +`README.md` §Installation SHALL state that installing the distribution requires supplying a `pyFoundationTools` source — the `requirements.txt` pointer, a private index, or an already-installed foundation — and SHALL give a working command. Whether py-foundationTools is published to an index is an upstream decision this spec does not make. + +## Environment currency + +Verification results are evidence only about the environment that produced them. An environment SHALL match the current `requirements.txt` pin for its results to bear on this repo's conformance. + +## Governance artifacts + +1. `.claude/CLAUDE.md` states this repo's role, package map, and gate command, and links every spec under `.claude/specs/`. It references specs and does not duplicate their content. Publishing a new spec includes adding its link here. +2. `.claude/specs/` holds the spec set. +3. `.claude/findings/` holds reproduced defect evidence and other non-normative observations. +4. `.claude/archive/` holds completed chunk sets. + +## README and metadata + +1. `README.md` follows the template's section shape: title → Features → Installation → Quick Start → Development Workflows → Requirements, and describes only what exists. +2. `pyproject.toml` `description` describes this package, not boilerplate. +3. Everything under `examples/` imports real module paths and runs against the installed package. + +## Layering enforcement + +`tests/test_package_layering.py` asserts, by AST scan of `src/`: - `math_tools` never imports `math_plot_helpers` or `matplotlib`. - `math_plot_helpers` may import `math_tools`. - Only `math_plot_helpers` imports `matplotlib`. -## Compliance checklist (mechanically verifiable) +The test fails if a `matplotlib` import is added to any `math_tools` module. + +## Conformance criteria + +Observable properties of a conformant repo: -- [x] `grep -r "pyMathTools" src/ tests/ examples/ Makefile pyproject.toml` → no hits -- [x] `find src -name py.typed` covers every public package -- [x] `make uv-fullCheck` passes after rename -- [x] `.claude/CLAUDE.md` exists and links every spec in `.claude/specs/` -- [x] `README.md` contains no "Boilerplate" text -- [x] `tests/test_package_layering.py` passes and fails if `import matplotlib` is added to any `math_tools` module +1. Import package names match §Package and module naming, and every public package ships `py.typed`. +2. `.claude/CLAUDE.md` links every `*.md` under `.claude/specs/`. +3. `README.md` carries no boilerplate stub text, and its §Installation satisfies §Distribution consumption. +4. The layering test passes, and fails when a `matplotlib` import is introduced into `math_tools`. +5. `pyproject.toml` `dependencies` contains no version specifier, `@`, or `git+` URL. +6. The `requirements.txt` `pyFoundationTools` pointer names a tag or full commit SHA that resolves against the remote. +7. An environment built from `requirements.txt` alone supplies every symbol in §Pin immutability. +8. All test-running install paths resolve `pyFoundationTools` to the same version. diff --git a/.claude/specs/waveformCore.md b/.claude/specs/waveformCore.md index f4e0e34..38ec50b 100644 --- a/.claude/specs/waveformCore.md +++ b/.claude/specs/waveformCore.md @@ -5,10 +5,10 @@ name: waveformCore purpose: Behavioral contract for Waveform1D and the aggregate spatial waveform containers spec: WaveformCore scope: project -status: accepted +status: draft applies_to: src/math_tools/waveforms/, tests/waveforms/ -last_updated: 2026-07-23 -semver: 0.0.4 +last_updated: 2026-08-26 +semver: 0.1.0 author: Nicholas Bergantz --- @@ -50,14 +50,14 @@ The inherited ABC `to_dict` therefore emits the correct wire keys and the DSP mixins use the numpy accessors; the ABC accessors exist for the contract and serialization. -## Instantiability / DSP phasing (load-bearing) +## Instantiability -The core chunk ships `class Waveform1D(Waveform1dABC)` with **no DSP mixin -bases**. The mixin base list is added in a single later "compose" chunk after -every mixin exists ([waveformDsp.md](waveformDsp.md)); until then the class -is complete and the gate stays green. A test pins that `Waveform1D` is -concrete (`Waveform1D.__abstractmethods__ == frozenset()` and a bare -construction succeeds). +`Waveform1D` SHALL be concrete and directly constructible: +`Waveform1D.__abstractmethods__` is empty and a bare construction succeeds. + +It composes the DSP families defined in [waveformDsp.md](waveformDsp.md) as +mixin bases ahead of `Waveform1dABC` in its MRO. Adding a DSP family SHALL NOT +change this class's own contract. ## Time axis (shared by all four) @@ -144,6 +144,27 @@ Python list vocabulary, not Swift's: `pop(i=-1) -> float` (`IndexError` on empty/out-of-range — stdlib semantics, not Swift's Optional), `clear()`. +**`insert` index semantics.** `insert(i, x)` SHALL follow `list.insert` index +semantics, including its out-of-range rule: the index is clamped, never +rejected. `insert` SHALL NOT raise for any integer `i`. + +| `i` | Insertion point | +| --- | --- | +| `i > n` | end | +| `0 <= i <= n` | `i` | +| `-n <= i < 0` | `n + i` | +| `i < -n` | front | + +This governs `Waveform1D.insert` and `insert` on all three aggregate +containers. `WaveformSpatialPose` resolves a single index for both of its +parallel arrays, so its length and `sample_count` invariants hold after any +insertion. + +`insert` is the only mutation verb with clamping semantics: `pop` and +`replace` raise on out-of-range indices, matching stdlib `list`. + +Each `insert` docstring SHALL state that the index clamps and never raises. + ## Aggregate containers All three share (with `Element` = `Position` / `Quaternion` / `SpatialPose`): @@ -175,7 +196,7 @@ All three share (with `Element` = `Position` / `Quaternion` / `SpatialPose`): `sample_count == min(len(positions), len(quaternions))` — both subtle Swift behaviors, kept. - `==`, `repr`, `to_dict`/`from_dict` per the ABC wire shapes. -- **Shared API idioms** (mathToolsArchitecture.md §API idioms, chunk 50): all +- **Shared API idioms** (mathToolsArchitecture.md §API idioms): all three aggregates additionally implement `__array__` and `isclose(other, rtol, atol)`, mirroring the umbrella idiom already required of `Waveform1D`, `Position`, and `Quaternion`. @@ -200,8 +221,8 @@ All three share (with `Element` = `Position` / `Quaternion` / `SpatialPose`): rotation). - `WaveformSpatialPose.from_components(position_x, position_y, position_z, quaternion_w, quaternion_x, quaternion_y, quaternion_z)`: the third - aggregate's `from_components`, dropped in chunk 17 and restored in chunk - 50 — seven per-component `Waveform1D`s (three position, four quaternion), + aggregate's `from_components` — seven per-component `Waveform1D`s + (three position, four quaternion), following `WaveformPosition.from_components`/ `WaveformQuaternion.from_components`'s shape; `ValueError` unless all seven share length and `dt`. @@ -210,7 +231,7 @@ All three share (with `Element` = `Position` / `Quaternion` / `SpatialPose`): 1. All four classes: `isinstance` of their ABC; `to_dict` output loads via the matching `foundationTypes` generated Type and round-trips equal — - exact target names on the pinned branch: `Waveform1D` ↔ + target names: `Waveform1D` ↔ `ScalarWaveformType`, `WaveformPosition` ↔ `PositionWaveformType`, `WaveformQuaternion` ↔ `QuaternionWaveformType`, `WaveformSpatialPose` ↔ `SpatialTransformWaveformType`. @@ -226,7 +247,10 @@ All three share (with `Element` = `Position` / `Quaternion` / `SpatialPose`): choice with a literal expected value). 6. `value_at_time` midpoint between two samples returns their average. 7. Mutation verbs match stdlib list semantics (pin `prepend` t0 shift; - `pop()` on empty raises `IndexError`; `clear()` empties). + `pop()` on empty raises `IndexError`; `clear()` empties). For all four + classes and any integer index, `insert` produces the same resulting order + as the equivalent `list.insert` and raises nothing. + `WaveformSpatialPose.insert` leaves its parallel arrays equal in length. 8. Aggregate `component_waveforms` → `from_components` round-trips exactly (quaternion tuple order `(w, x, y, z)` pinned). 9. `WaveformSpatialPose` with unequal arrays: `is_valid` False, diff --git a/requirements.txt b/requirements.txt index de5e654..c017f1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,4 +19,4 @@ # Editable: -e git+https://github.com/example-org/some-package.git@develop#egg=some-package -pyFoundationTools @ git+https://github.com/kopecn/py-foundationTools.git@feat/switch-to-new-template \ No newline at end of file +pyFoundationTools @ git+https://github.com/kopecn/py-foundationTools.git@dev \ No newline at end of file From a391e9e66c8d7984e288e871393a2446bdb25b94 Mon Sep 17 00:00:00 2001 From: kopecn Date: Tue, 8 Sep 2026 13:29:06 -0700 Subject: [PATCH 02/12] adding notice... --- MANIFEST.in | 1 + NOTICE | 39 ++++++++++++++++++++++++++++++++++ src/math_tools/otg/__init__.py | 4 ++++ 3 files changed, 44 insertions(+) create mode 100644 NOTICE diff --git a/MANIFEST.in b/MANIFEST.in index 3a51146..40b8f95 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include CONTRIBUTING.md include HISTORY.md include LICENSE +include NOTICE include README.md recursive-exclude * __pycache__ diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..e8acd83 --- /dev/null +++ b/NOTICE @@ -0,0 +1,39 @@ +py-MathTools +Copyright (c) 2026, Nicholas Bergantz + +This product includes software developed by third parties. Their copyright +notices and license terms are retained below as required by their licenses. + +-------------------------------------------------------------------------------- +Ruckig — Online Trajectory Generation +-------------------------------------------------------------------------------- + +The `math_tools.otg` package (src/math_tools/otg/) is a Python port of Ruckig, +the community (open-source) edition of the Ruckig online trajectory generator: + + https://github.com/pantor/ruckig + +Ruckig is distributed under the MIT License. Its original copyright notice and +license terms are reproduced in full below and apply to the ported material: + +MIT License + +Copyright (c) 2021 Lars Berscheid + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/math_tools/otg/__init__.py b/src/math_tools/otg/__init__.py index 19ffc92..de3c898 100644 --- a/src/math_tools/otg/__init__.py +++ b/src/math_tools/otg/__init__.py @@ -1,5 +1,9 @@ """Online trajectory generation (OTG): a faithful Ruckig port. +Ported from Ruckig (community edition, MIT-licensed; Copyright (c) 2021 Lars +Berscheid), https://github.com/pantor/ruckig. The upstream copyright notice and +license text are retained in the ``NOTICE`` file at the repository root. + See ``.claude/specs/otg.md``. Pure Python + stdlib in the per-cycle path -- this package (and everything under it) never imports numpy (real-time control-loop constraint, enforced by ``tests/test_package_layering.py``). From 2736780184875b7226117543b847f7e4286b390d Mon Sep 17 00:00:00 2001 From: kopecn Date: Tue, 8 Sep 2026 14:29:10 -0700 Subject: [PATCH 03/12] cont... --- .bumpversion.cfg | 2 +- .claude/specs/precisionTimeMath.md | 21 ++- .env | 4 +- .flake8 | 25 +++ CONTRIBUTING.md | 2 +- Makefile | 149 +++++++++++++----- README.md | 2 +- pyproject.toml | 5 +- src/math_tools/otg/profile.py | 31 +++- .../precision_time/precision_time_interval.py | 18 +++ .../precision_time/precision_timestamp.py | 37 +++++ src/math_tools/spatial/position.py | 8 + src/math_tools/spatial/quaternion.py | 8 + src/math_tools/spatial/spatial_pose.py | 12 ++ src/math_tools/waveforms/support.py | 1 + src/math_tools/waveforms/waveform1d.py | 13 ++ src/math_tools/waveforms/waveform_position.py | 12 ++ .../waveforms/waveform_quaternion.py | 12 ++ .../waveforms/waveform_spatial_pose.py | 14 ++ 19 files changed, 318 insertions(+), 58 deletions(-) create mode 100644 .flake8 diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 54b6c97..175d563 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.0.2 +current_version = 0.0.3 commit = true tag = false tag_name = v{new_version} diff --git a/.claude/specs/precisionTimeMath.md b/.claude/specs/precisionTimeMath.md index 4e00aab..5ae6c42 100644 --- a/.claude/specs/precisionTimeMath.md +++ b/.claude/specs/precisionTimeMath.md @@ -7,8 +7,8 @@ spec: PrecisionTimeMath scope: project status: accepted applies_to: src/math_tools/precision_time/, tests/precision_time/ -last_updated: 2026-07-11 -semver: 0.0.2 +last_updated: 2026-09-08 +semver: 0.0.3 author: Nicholas Bergantz --- @@ -69,10 +69,13 @@ noted; `NotImplemented` for foreign types so Python falls back correctly): - `==`, `<`, `<=`, `>`, `>=` (total ordering on signed total), `__hash__`. - `bool(x)` is `not x.is_zero`. -**Serialization**: `to_dict` inherited from the ABC; `from_dict` accepts the -ABC wire shape `{"seconds", "attoseconds", "sign"}` — exactly what -`foundationTypes` `PrecisionTimeIntervalType.to_dict()` emits (wire-format -interop is the Tier-1↔Tier-3 contract). +**Serialization**: `to_dict` and `from_dict` are both implemented on this +class (the Tier-2 ABC declares them abstract and, per `mathTypeTiers.md`, must +stay free of concrete wire mappings — there is nothing to inherit). Both use +the ABC wire shape `{"attoseconds", "seconds", "sign"}` (`sign` as its string +value) — exactly what `foundationTypes` `PrecisionTimeIntervalType.to_dict()` +emits, so payloads round-trip through either carrier (wire-format interop is +the Tier-1↔Tier-3 contract). ## `PrecisionTimestamp` @@ -110,8 +113,10 @@ epoch), `days_since_epoch: int`, `seconds_of_day: int`, rendering of Swift's `Result` in `PrecisionTimestamp+Comparable.swift`. -**Serialization**: ABC `to_dict` (camelCase optional keys); `from_dict` -accepts the same wire shape (parity with `PrecisionTimestampType`). +**Serialization**: `to_dict` and `from_dict` are both implemented on this +class (camelCase optional keys `referenceFrame`/`timescale`/`uncertainty` +emitted only when present); the wire shape matches `PrecisionTimestampType` +for round-trip parity. ## Compliance requirements (test-checkable) diff --git a/.env b/.env index 5bd627c..0d75f3e 100644 --- a/.env +++ b/.env @@ -10,8 +10,8 @@ DEFAULT_PYTHON=3.13 # Versions to test against. SPACE-SEPARATED, UNQUOTED on purpose: # Make expands `$(PYTHONS)` straight into `uv python install ...` as separate args. # Do NOT quote or comma-separate — that collapses it into one bogus argument. -# Canonical set: default 3.13, support floor 3.10 (matches requires-python>=3.10). -PYTHONS=3.10 3.11 3.12 3.13 +# Canonical set: default 3.13, support floor 3.11 (matches requires-python>=3.11). +PYTHONS=3.11 3.12 3.13 3.14 3.15 # Throwaway clean-room venv for `make testInEnv` (deleted on teardown). # Kept DISTINCT from uv's `.venv` so the clean-room never nukes your dev env. diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..865b6e5 --- /dev/null +++ b/.flake8 @@ -0,0 +1,25 @@ +# flake8 config (flake8 does not read pyproject.toml). Formatting is owned by black +# ([tool.black]); this file only lints. flake8-bugbear supplies the `B` checks. +[flake8] +max-line-length = 100 + +# E (pycodestyle errors), F (pyflakes), B (bugbear). The `W` warning series is +# intentionally not enforced. +select = E, F, B + +# E203, W503: disabled for black compatibility. +# E704: `def f(): ...` one-liners are the idiomatic form for @overload / stub bodies. +extend-ignore = E203, E704, W503, F401 + +# Repo-specific relaxations go here, e.g.: +# per-file-ignores = +# tests/test_something.py: E501 + +exclude = + .git, + __pycache__, + build, + dist, + .venv, + .venvs, + .cleanroom-venv diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 27082ba..6e5f270 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -107,7 +107,7 @@ Before submitting a pull request, make sure: - [ ] Tests are included for new logic - [ ] Documentation is updated if needed -- [ ] The project supports Python 3.10 through 3.13 +- [ ] The project supports Python 3.11 through 3.13 - [ ] All tests pass (CI checks will run on PRs) --- diff --git a/Makefile b/Makefile index 16cc5f0..c6b6669 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,9 @@ uv-flush-cache uv-flush-envs uv-flush-pythons uv-flush-everything uv-nuke \ uv-lifecycle-test \ dev setup \ - installDev e refresh \ - test cleanRoomCleanup cleanRoomBootstrap cleanRoomPytest testInEnv \ + installDev e refresh pip-bootstrap \ + lint format typecheck fullCheck \ + test check-pip cleanRoomCleanup cleanRoomBootstrap cleanRoomPytest testInEnv \ build validateBuild release-test release \ nuke list @@ -25,24 +26,30 @@ ifneq (,$(wildcard .env)) endif # Defaults (overridable via .env — the user-editable surface). Keep in sync with .env. -PYTHONS ?= 3.10 3.11 3.12 3.13 +PYTHONS ?= 3.11 3.12 3.13 3.14 3.15 DEFAULT_PYTHON ?= 3.13 PYTHON ?= python3 VENV ?= .cleanroom-venv -# Quality-target paths. ROOT half has NO src/ — its Python lives in hooks/ + tests/ -# (see GAPS.md §6). The template half overrides these to src/. Overridable via .env. PY_SRC ?= hooks PY_TESTS ?= tests -PY_EXAMPLES ?= examples +PY_EXAMPLES ?= PY_ALL ?= $(PY_SRC) $(PY_TESTS) $(PY_EXAMPLES) +# mypy cannot CRAWL a src/ layout that is editable-installed: the editable .pth puts +# src/ on sys.path, so each module resolves under both `pkg` and `src.pkg` → mypy's +# "Source file found twice under different module names" error. Drive mypy by package +# NAME instead (resolved via the single src/ root). Top-level packages = src/ subdirs +# that have an __init__.py. Empty for the hooks/ root variant (no src/), which crawls +# normally. See uv-typecheck. +MYPY_PKGS := $(patsubst src/%/,-p %,$(sort $(dir $(wildcard src/*/__init__.py)))) + # Derived # Tool runner for uv- quality/test recipes. `--extra dev` ensures ruff/mypy/pytest are # resolved (and installed if missing) from the "[dev]" extra even on a FRESH checkout — # no reliance on a pre-existing .venv, rather than the ambient PATH. -UV := uv run --no-project +UV := uv run --no-project PIP := $(PYTHON) -m pip BUMPVERSION := bumpversion --allow-dirty REPO := $(notdir $(CURDIR)) @@ -95,7 +102,7 @@ endef # MARK: - HELP # ============================================================================ help: ## Show this help - @echo "$(REPO) — make targets (bare = pip · uv-… = uv path)" + @echo "$(REPO) — make targets (bare = pip · FIRST-CLASS · uv-… = uv runner · second-class)" @echo "config: DEFAULT_PYTHON=$(DEFAULT_PYTHON) PYTHONS=$(PYTHONS)" @echo "" @awk 'BEGIN {FS = ":.*?## "} \ @@ -266,19 +273,20 @@ uv-refresh: check-uv ## Clean cache + reinstall from requirements + upgrade edi # ============================================================================ # MARK: - UV · QUALITY # ============================================================================ -##@ UV · Quality -uv-lint: check-uv ## Run ruff linter (read-only; non-zero exit for CI) - $(UV) ruff check $(PY_ALL) +##@ UV · Quality (second-class uv RUNNER for the first-class flake8/black/mypy tools) +uv-lint: check-uv ## Run flake8 via uv (read-only; non-zero exit for CI) + $(UV) flake8 $(PY_ALL) -uv-format: check-uv ## Format code with ruff - $(UV) ruff format $(PY_ALL) -# KEEP --unsafe-fixes: intentional, added after findings on 2026-06-25. -# Do NOT strip in cleanup/refactor passes — the unsafe autofixes here are relied -# upon deliberately. Removing it has regressed this target before. - $(UV) ruff check --fix --unsafe-fixes $(PY_ALL) +uv-format: check-uv ## Format code with black via uv + $(UV) black $(PY_ALL) uv-typecheck: check-uv ## Strict type check with mypy +ifeq ($(strip $(MYPY_PKGS)),) $(UV) mypy $(PY_SRC) $(PY_TESTS) $(PY_EXAMPLES) +else + $(UV) mypy $(MYPY_PKGS) + $(UV) mypy $(PY_TESTS) $(PY_EXAMPLES) +endif # ty (Astral's preview type-checker) is intentionally OUT for now (decision D1): # it's pre-release and not wired into uv-fullCheck. Revisit when it stabilizes. @@ -379,6 +387,41 @@ refresh: ## Refresh pip packages: reinstall from requirements + upgrade editabl $(PIP) install -r requirements.txt $(PIP) install --upgrade -e ".[dev]" +# ============================================================================ +# MARK: - PIP · QUALITY (FIRST-CLASS) +# ============================================================================ +##@ PIP · Quality (FIRST-CLASS: flake8 + black + mypy, run on ambient $(PYTHON)) +lint: ## Run flake8 (read-only; non-zero exit for CI) — first-class + $(PYTHON) -m flake8 $(PY_ALL) + +format: ## Format code with black — first-class + $(PYTHON) -m black $(PY_ALL) + +typecheck: ## Strict type check with mypy — first-class +ifeq ($(strip $(MYPY_PKGS)),) + $(PYTHON) -m mypy $(PY_SRC) $(PY_TESTS) $(PY_EXAMPLES) +else + $(PYTHON) -m mypy $(MYPY_PKGS) + $(PYTHON) -m mypy $(PY_TESTS) $(PY_EXAMPLES) +endif + +fullCheck: lint typecheck test ## FIRST-CLASS gate: flake8 + mypy + pytest + +# nuke's inverse (see the comment on `nuke`). Rebuilds the build backend +# (setuptools/wheel) that `ensurepip` never bundles on Python >= 3.12 (E1). +# Deliberately NOT wired as a prereq of installDev/e/refresh/build (D1) — those +# targets keep failing loudly on their own terms rather than growing a guard +# layer; check-pip is scoped only to the clean-room target (D2, see C2 comment +# on cleanRoomBootstrap below). +pip-bootstrap: ## Rebuild the ambient build backend after `nuke` (NETWORK REQUIRED) + @echo "Bootstrapping ambient pip + build backend (setuptools, wheel)..." + $(PYTHON) -m ensurepip --upgrade + $(PIP) install --upgrade setuptools wheel + @echo "" + @echo "If this fails with 'externally-managed-environment' (Homebrew/Debian" + @echo "Python), this interpreter refuses ambient installs by design — use" + @echo "'make uv-bootstrap' instead (offline-capable via uv's cache)." + # ============================================================================ # MARK: - PIP · TEST # ============================================================================ @@ -390,6 +433,28 @@ test: ## Run tests using the current Python environment cleanRoomCleanup: ## Delete the clean-room venv ($(VENV)) rm -rf $(VENV) || true +# check-pip guard. Assert ONLY what the clean room actually needs: that the +# ambient interpreter can build a working venv, i.e. `ensurepip` is present. +# +# It deliberately does NOT assert that `setuptools.build_meta` imports on the +# ambient interpreter. The clean room installs into $(VENV) under PEP-517 build +# isolation, which provisions its own setuptools from PyPI — the ambient +# interpreter's build backend is never consulted. Guarding on it produced a +# false negative that blocked a clean room which then succeeded when run by +# hand. That assertion is only meaningful for --no-build-isolation ambient +# installs (`e`, `installDev`), which D1 deliberately leaves ungated. +# +# NETWORK REQUIRED for the recipe below: since Python 3.12 ensurepip seeds pip +# only (E1), so a fresh venv has no build backend and PEP-517 isolation must +# reach PyPI. A backend-less ambient interpreter does not change that either +# way, which is precisely why it is not worth guarding here. +check-pip: ## Check the ambient interpreter can create the clean-room venv + @$(PYTHON) -m ensurepip --version >/dev/null 2>&1 || { \ + echo "ERROR: ensurepip unavailable on $(PYTHON) — cannot create $(VENV)."; \ + echo " Run: make pip-bootstrap"; \ + echo " Or use the uv path: make uv-sync"; \ + exit 1; } + # The clean room is an install path, so it obeys the same BKM rule as every other # one (see the dependency-model comment above ##@ UV · Bootstrap): pyproject.toml # declares dependency NAMES ONLY, and requirements.txt carries the pins and the @@ -398,7 +463,7 @@ cleanRoomCleanup: ## Delete the clean-room venv ($(VENV)) # (`No matching distribution found`). Install the requirements file FIRST, then the # package. Keep ".[dev]" NON-editable here — validating the real packaging path is # this target's entire purpose. -cleanRoomBootstrap: cleanRoomCleanup ## Bootstrap the clean-room venv + deps (runs NO tests) +cleanRoomBootstrap: cleanRoomCleanup check-pip ## Bootstrap the clean-room venv + deps (runs NO tests) $(PYTHON) -m venv $(VENV) . $(VENV)/bin/activate && \ which python3 && \ @@ -418,30 +483,35 @@ testInEnv: clean cleanRoomBootstrap cleanRoomPytest cleanRoomCleanup ## Full cl # MARK: - PIP · BUILD & RELEASE # ============================================================================ ##@ PIP · Build & Release -build: clean-build ## Build sdist + wheel ($(PYTHON) -m build) +# build/twine were previously invoked against ambient $(PYTHON), but both are +# declared in [project.optional-dependencies].dev, which installs into .venv — +# not the ambient interpreter (E4, a live bug independent of the FA this track +# is fixing). Route them through `uv run --with` instead so they resolve +# correctly on a checkout whose only setup was `make uv-sync`. +build: check-uv clean-build ## Build sdist + wheel (uv run --with build python -m build) @echo "Building package..." - $(PYTHON) -m build + $(UV) --with build python -m build -validateBuild: build ## Validate build artifacts with twine +validateBuild: check-uv build ## Validate build artifacts with twine @echo "Validating dist/ with twine..." - $(PYTHON) -m twine check dist/* + $(UV) --with twine twine check dist/* release-test: checkCleanGit validateBuild ## Dry-run publish to TestPyPI (clean tree only) @echo "Uploading $(REPO) v$$($(MAKE) -s version) to TestPyPI..." - @$(PYTHON) -m twine upload --repository testpypi dist/* - -# No CI-driven publish path exists: `.github/workflows/ci-cd.yml` runs quality -# checks and the Python compatibility matrix on PRs to dev/prod only — it does not -# tag or upload anything. Release is therefore a manual, human-run procedure; -# `make release` refuses to upload from an unreviewed working tree and prints that -# procedure instead of performing it. -release: validateBuild ## Refuse local upload; print the manual release procedure - @echo "No CI-driven publish path exists — release is a manual procedure." + @$(UV) --with twine twine upload --repository testpypi dist/* + +# PyPI publishing is owned by CI, not this Makefile. Per the ci-cd spec, the +# pipeline is the single authoritative path to production — no manual, out-of-band +# uploads. `.github/workflows/tag-on-prod.yml` tags v on push to `prod`; +# a publish-on-tag workflow promotes that artifact. `make release` therefore +# refuses to upload and prints the release procedure instead. +release: validateBuild ## Refuse local upload; print the CI-driven release procedure + @echo "Local PyPI upload is disabled — the pipeline is the authoritative publish path." @echo "" @echo "To release $(REPO) v$$($(MAKE) -s version):" @echo " 1. Bump the version (make bump-patch|bump-minor|bump-major) and merge to prod." - @echo " 2. Build + validate: make validateBuild." - @echo " 3. Upload by hand: python3 -m twine upload dist/*" + @echo " 2. Push to prod → tag-on-prod.yml creates the v tag." + @echo " 3. The publish-on-tag workflow uploads to PyPI." @echo "" @echo "For a local pre-flight, use: make release-test (TestPyPI)." @exit 1 @@ -454,14 +524,22 @@ release: validateBuild ## Refuse local upload; print the manual release procedu # the AMBIENT interpreter ($(PIP)). Prefer `make uv-flush-envs` — deleting the # venv dir is the reliable flush primitive. Use this only when you're stuck in a # non-deletable (e.g. system) env. Non-editable URL/VCS installs are skipped. +# +# --exclude setuptools --exclude wheel: at Python >= 3.12, pip/_internal/commands/ +# freeze.py:12-20 stopped suppressing the build backend from `pip freeze` +# (_should_suppress_build_backends() is version-gated below 3.12), so an +# unqualified `freeze --exclude-editable | pip uninstall` now removes the very +# build backend the interpreter needs to install anything afterward — including +# itself. Keep these exclusions; do not "clean up" them in a later refactor. nuke: ## Per-package uninstall from ambient env (inferior — prefer uv-flush-envs) @echo "Uninstalling regular packages (skipping system-managed)..." - $(call uninstall_package_list,$(PIP) freeze --exclude-editable | grep -v ' @ ') + $(call uninstall_package_list,$(PIP) freeze --exclude-editable --exclude setuptools --exclude wheel | grep -v ' @ ') @echo "Uninstalling editable packages by name..." $(call uninstall_package_list,$(PIP) list --editable --format=freeze | cut -d= -f1) @echo "pip-nuke complete." + @echo "Run 'make pip-bootstrap' (network) or 'make uv-bootstrap' (offline-capable) to rebuild." list: ## List pip packages in available environments $(call print_packages,SYSTEM PYTHON PACKAGES,$(PIP)) @@ -481,5 +559,4 @@ list: ## List pip packages in available environments echo "$(HR)"; \ $$venv/bin/pip list 2>/dev/null || echo "No packages or pip not available"; \ echo; \ - done - \ No newline at end of file + done \ No newline at end of file diff --git a/README.md b/README.md index fe8c6e6..8dda9d2 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ make uv-format # ruff format + autofix ## Requirements -- Python >= 3.10 +- Python >= 3.11 - `pyFoundationTools`, `numpy`, `scipy`, `numpy-quaternion`, `matplotlib` (see [`pyproject.toml`](pyproject.toml) for the names-only dependency declaration and [`requirements.txt`](requirements.txt) for the pinned diff --git a/pyproject.toml b/pyproject.toml index 2cfe65b..39b4462 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,10 +5,10 @@ build-backend = "setuptools.build_meta" [project] name = "py_math_tools" -version = "0.0.2" +version = "0.0.3" description = "Tier 3 numpy-backed math implementation layer for pyFoundationTools (quaternions, spatial/spherical math, waveforms, polynomials, trajectory generation)." readme = {file = "README.md", content-type = "text/markdown"} -requires-python = ">=3.10" +requires-python = ">=3.11" authors = [ {name = "Nicholas Bergantz", email = "nicholas.bergantz@gmail.com"} ] @@ -20,7 +20,6 @@ classifiers = [ "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", diff --git a/src/math_tools/otg/profile.py b/src/math_tools/otg/profile.py index 46dd855..4ee2cf9 100644 --- a/src/math_tools/otg/profile.py +++ b/src/math_tools/otg/profile.py @@ -55,7 +55,9 @@ _P_PRECISION = 1e-8 _V_PRECISION = 1e-8 _A_PRECISION = 1e-10 -_T_PRECISION = 1e-12 # Declared but unused in the Swift source too; ported for fidelity. +_T_PRECISION = ( + 1e-12 # Declared but unused in the Swift source too; ported for fidelity. +) _T_MAX = 1e12 #: Swift ``Double.ulpOfOne`` (machine epsilon for float64) -- used (not @@ -271,7 +273,15 @@ def check_for_second_order_velocity( if self.t[1] < 0.0: return False - self.t_sum = [0.0, self.t[1], self.t[1], self.t[1], self.t[1], self.t[1], self.t[1]] + self.t_sum = [ + 0.0, + self.t[1], + self.t[1], + self.t[1], + self.t[1], + self.t[1], + self.t[1], + ] if self.t_sum[-1] > _T_MAX: return False @@ -457,6 +467,7 @@ def check_with_timing( @overload def set_boundary(self, profile: Profile, /) -> None: ... + @overload def set_boundary( self, @@ -602,7 +613,9 @@ def check_for_second_order_with_timing( return False if not (a_min - _A_EPS < a_down < a_max + _A_EPS): return False - return self.check_for_second_order(a_up, a_down, v_max, v_min, control_signs, limits) + return self.check_for_second_order( + a_up, a_down, v_max, v_min, control_signs, limits + ) # MARK: - First-order position interface @@ -624,7 +637,9 @@ def check_for_first_order( self.v = [0.0, 0.0, 0.0, v_up if self.t[3] > 0 else 0.0, 0.0, 0.0, 0.0, self.vf] for i in range(7): - self.p[i + 1], _, _ = integrate_jerk(self.t[i], self.p[i], self.v[i], self.a[i], 0.0) + self.p[i + 1], _, _ = integrate_jerk( + self.t[i], self.p[i], self.v[i], self.a[i], 0.0 + ) self.control_signs = control_signs self.limits = limits @@ -699,5 +714,9 @@ def check_step_for_position_extremum( Profile.check_position_extremum(-a / j, t_sum, t, p, v, a, j, ext) elif d > 0.0: d_sqrt = math.sqrt(d) - Profile.check_position_extremum((-a - d_sqrt) / j, t_sum, t, p, v, a, j, ext) - Profile.check_position_extremum((-a + d_sqrt) / j, t_sum, t, p, v, a, j, ext) + Profile.check_position_extremum( + (-a - d_sqrt) / j, t_sum, t, p, v, a, j, ext + ) + Profile.check_position_extremum( + (-a + d_sqrt) / j, t_sum, t, p, v, a, j, ext + ) diff --git a/src/math_tools/precision_time/precision_time_interval.py b/src/math_tools/precision_time/precision_time_interval.py index 9c3cbe5..6c67d97 100644 --- a/src/math_tools/precision_time/precision_time_interval.py +++ b/src/math_tools/precision_time/precision_time_interval.py @@ -140,6 +140,19 @@ def from_dict(cls, obj: Any) -> PrecisionTimeInterval: sign=sign, ) + def to_dict(self) -> dict[str, Any]: + """Serialize to the ABC wire shape ``{"attoseconds", "seconds", "sign"}``. + + Emits exactly what ``foundationTypes`` ``PrecisionTimeIntervalType.to_dict()`` + emits (``sign`` as its string value), so the payload round-trips through + either carrier. + """ + return { + "attoseconds": self.attoseconds, + "seconds": self.seconds, + "sign": self.sign.value, + } + # MARK: - Properties (ABC-required) @property @@ -168,6 +181,11 @@ def seconds_as_float(self) -> float: """This interval's duration in seconds, as a (lossy) float.""" return self._total_atto / ATTOSECONDS_PER_SECOND + @property + def is_zero(self) -> bool: + """Whether this interval is exactly zero.""" + return self._total_atto == 0 + # MARK: - Arithmetic def __add__(self, other: PrecisionTimeInterval) -> PrecisionTimeInterval: diff --git a/src/math_tools/precision_time/precision_timestamp.py b/src/math_tools/precision_time/precision_timestamp.py index edbd18a..01ea901 100644 --- a/src/math_tools/precision_time/precision_timestamp.py +++ b/src/math_tools/precision_time/precision_timestamp.py @@ -211,6 +211,28 @@ def from_dict(cls, obj: Any) -> PrecisionTimestamp: uncertainty=uncertainty, ) + def to_dict(self) -> dict[str, Any]: + """Serialize to the ABC wire shape (camelCase optional keys). + + Emits exactly what ``foundationTypes`` ``PrecisionTimestampType.to_dict()`` + emits: the ``{"attoseconds", "seconds", "sign"}`` core, plus + ``referenceFrame``/``timescale``/``uncertainty`` only when present. Enum + values serialize to their string value, so the payload round-trips + through either carrier. + """ + result: dict[str, Any] = { + "attoseconds": self.attoseconds, + "seconds": self.seconds, + "sign": self.sign.value, + } + if self._reference_frame is not None: + result["referenceFrame"] = self._reference_frame.value + if self._timescale is not None: + result["timescale"] = self._timescale.value + if self._uncertainty is not None: + result["uncertainty"] = self._uncertainty + return result + # MARK: - Properties (ABC-required) @property @@ -239,6 +261,21 @@ def uncertainty(self) -> int | None: # MARK: - Accessors (beyond the ABC) + @property + def is_epoch(self) -> bool: + """Whether this timestamp is exactly the Unix epoch.""" + return self.sign == NumericSign.ZERO + + @property + def is_after_epoch(self) -> bool: + """Whether this timestamp is strictly after the Unix epoch.""" + return self.sign == NumericSign.POSITIVE + + @property + def is_before_epoch(self) -> bool: + """Whether this timestamp is strictly before the Unix epoch.""" + return self.sign == NumericSign.NEGATIVE + @property def interval(self) -> PrecisionTimeInterval: """This timestamp's signed offset from the Unix epoch.""" diff --git a/src/math_tools/spatial/position.py b/src/math_tools/spatial/position.py index eb1d934..70222d1 100644 --- a/src/math_tools/spatial/position.py +++ b/src/math_tools/spatial/position.py @@ -154,6 +154,14 @@ def from_dict(cls: type[T], obj: Any) -> T: z = _from_float(obj.get("z")) return cls(x, y, z) + def to_dict(self) -> dict[str, Any]: + """Serialize to the ABC wire shape ``{"x", "y", "z"}``. + + Emits exactly what ``foundationTypes`` ``PositionType.to_dict()`` emits, + so the payload round-trips through either carrier. + """ + return {"x": self.x, "y": self.y, "z": self.z} + # MARK: - Properties @property diff --git a/src/math_tools/spatial/quaternion.py b/src/math_tools/spatial/quaternion.py index 870ac4b..bd33fc0 100644 --- a/src/math_tools/spatial/quaternion.py +++ b/src/math_tools/spatial/quaternion.py @@ -843,6 +843,14 @@ def from_dict(cls, obj: Any) -> Quaternion: # cls ensures the correct subclass type is returned return cls.from_components(w, x, y, z) + def to_dict(self) -> dict[str, Any]: + """Serialize to the ABC wire shape ``{"w", "x", "y", "z"}``. + + Emits exactly what ``foundationTypes`` ``QuaternionType.to_dict()`` emits, + so the payload round-trips through either carrier. + """ + return {"w": self.w, "x": self.x, "y": self.y, "z": self.z} + def to_unitSphericalSmallCircle( q: Quaternion, radius_angle: float = np.pi / 4 ) -> UnitSphericalSmallCircleType: diff --git a/src/math_tools/spatial/spatial_pose.py b/src/math_tools/spatial/spatial_pose.py index 9babe5e..17ebe3e 100644 --- a/src/math_tools/spatial/spatial_pose.py +++ b/src/math_tools/spatial/spatial_pose.py @@ -122,6 +122,18 @@ def from_dict(cls: type[T], obj: Any) -> T: orientation = Quaternion.from_dict(obj.get("orientation")) return cls(position, orientation) + def to_dict(self) -> dict[str, Any]: + """Serialize to the ABC wire shape ``{"orientation", "position"}``. + + Emits exactly what ``foundationTypes`` ``SpatialTransformType.to_dict()`` + emits (nested ``QuaternionType``/``PositionType`` payloads), so the + payload round-trips through either carrier. + """ + return { + "orientation": self.orientation.to_dict(), + "position": self.position.to_dict(), + } + # MARK: - Properties @property diff --git a/src/math_tools/waveforms/support.py b/src/math_tools/waveforms/support.py index 0c34632..83460e1 100644 --- a/src/math_tools/waveforms/support.py +++ b/src/math_tools/waveforms/support.py @@ -88,6 +88,7 @@ # MARK: - Enums + class WaveformFilterType(str, enum.Enum): """Filter family selector for ``FilteringMixin.filtered``.""" diff --git a/src/math_tools/waveforms/waveform1d.py b/src/math_tools/waveforms/waveform1d.py index a952d2e..eff5f6d 100644 --- a/src/math_tools/waveforms/waveform1d.py +++ b/src/math_tools/waveforms/waveform1d.py @@ -179,6 +179,19 @@ def from_dict(cls, obj: Any) -> Waveform1D: t0 = PrecisionTimestamp.from_dict(obj["t0"]) return cls(obj.get("waveform", []), dt=dt, t0=t0) + def to_dict(self) -> dict[str, Any]: + """Serialize to the ABC wire shape ``{"waveform", "dt", "t0"}``. + + Emits exactly what ``foundationTypes`` ``ScalarWaveformType.to_dict()`` + emits (``waveform`` as a list of floats), so the payload round-trips + through either carrier. + """ + return { + "waveform": [float(v) for v in self.values], + "dt": self.dt.to_dict(), + "t0": self.t0.to_dict(), + } + # MARK: - Generators (sinusoidal) @classmethod diff --git a/src/math_tools/waveforms/waveform_position.py b/src/math_tools/waveforms/waveform_position.py index 617e50f..9a0b2c3 100644 --- a/src/math_tools/waveforms/waveform_position.py +++ b/src/math_tools/waveforms/waveform_position.py @@ -127,6 +127,18 @@ def from_dict(cls, obj: Any) -> WaveformPosition: positions = [Position.from_dict(p) for p in obj.get("positions", [])] return cls(positions, dt=dt, t0=t0) + def to_dict(self) -> dict[str, Any]: + """Serialize to the ABC wire shape ``{"dt", "positions", "t0"}``. + + Emits exactly what ``foundationTypes`` ``PositionWaveformType.to_dict()`` + emits, so the payload round-trips through either carrier. + """ + return { + "dt": self.dt.to_dict(), + "positions": [p.to_dict() for p in self.positions], + "t0": self.t0.to_dict(), + } + @classmethod def from_components(cls, x: Waveform1D, y: Waveform1D, z: Waveform1D) -> WaveformPosition: """Build from three per-axis ``Waveform1D``s (``dt``/``t0`` taken from ``x``). diff --git a/src/math_tools/waveforms/waveform_quaternion.py b/src/math_tools/waveforms/waveform_quaternion.py index 3554851..bf71529 100644 --- a/src/math_tools/waveforms/waveform_quaternion.py +++ b/src/math_tools/waveforms/waveform_quaternion.py @@ -140,6 +140,18 @@ def from_dict(cls, obj: Any) -> WaveformQuaternion: quaternions = [Quaternion.from_dict(q) for q in obj.get("quaternions", [])] return cls(quaternions, dt=dt, t0=t0) + def to_dict(self) -> dict[str, Any]: + """Serialize to the ABC wire shape ``{"dt", "quaternions", "t0"}``. + + Emits exactly what ``foundationTypes`` ``QuaternionWaveformType.to_dict()`` + emits, so the payload round-trips through either carrier. + """ + return { + "dt": self.dt.to_dict(), + "quaternions": [q.to_dict() for q in self.quaternions], + "t0": self.t0.to_dict(), + } + @classmethod def from_components( cls, w: Waveform1D, x: Waveform1D, y: Waveform1D, z: Waveform1D diff --git a/src/math_tools/waveforms/waveform_spatial_pose.py b/src/math_tools/waveforms/waveform_spatial_pose.py index 5512370..40473b2 100644 --- a/src/math_tools/waveforms/waveform_spatial_pose.py +++ b/src/math_tools/waveforms/waveform_spatial_pose.py @@ -215,6 +215,20 @@ def from_dict(cls, obj: Any) -> WaveformSpatialPose: quaternions = [Quaternion.from_dict(q) for q in obj.get("quaternions", [])] return cls(positions, quaternions, dt=dt, t0=t0) + def to_dict(self) -> dict[str, Any]: + """Serialize to the ABC wire shape ``{"dt", "positions", "quaternions", "t0"}``. + + Emits exactly what ``foundationTypes`` + ``SpatialTransformWaveformType.to_dict()`` emits (parallel position and + quaternion arrays), so the payload round-trips through either carrier. + """ + return { + "dt": self.dt.to_dict(), + "positions": [p.to_dict() for p in self.positions], + "quaternions": [q.to_dict() for q in self.quaternions], + "t0": self.t0.to_dict(), + } + @classmethod def from_poses( cls, From 9c196ec59e7c493b0608acd750e8770ee25be4a6 Mon Sep 17 00:00:00 2001 From: kopecn Date: Tue, 8 Sep 2026 14:42:06 -0700 Subject: [PATCH 04/12] cont... --- .claude/specs/precisionTimeMath.md | 7 +++++-- .claude/specs/spatialMath.md | 6 ++++-- .claude/specs/waveformCore.md | 6 ++++-- .../test_precision_time_interval.py | 12 ++++++++--- .../test_precision_timestamp.py | 12 ++++++++--- tests/spatial/test_position.py | 8 +++++-- tests/spatial/test_spatial_pose.py | 8 +++++-- tests/test_quaternion.py | 21 +++++++++++++------ tests/waveforms/test_waveform1d_core.py | 9 ++++++-- tests/waveforms/test_waveform_position.py | 9 ++++++-- tests/waveforms/test_waveform_quaternion.py | 9 ++++++-- tests/waveforms/test_waveform_spatial_pose.py | 9 ++++++-- 12 files changed, 86 insertions(+), 30 deletions(-) diff --git a/.claude/specs/precisionTimeMath.md b/.claude/specs/precisionTimeMath.md index 5ae6c42..4603a4e 100644 --- a/.claude/specs/precisionTimeMath.md +++ b/.claude/specs/precisionTimeMath.md @@ -1,5 +1,5 @@ --- -version: 1.0 +version: 1.1 type: specification name: precisionTimeMath purpose: Behavioral contract for the Tier-3 PrecisionTimeInterval and PrecisionTimestamp math types @@ -129,4 +129,7 @@ for round-trip parity. 7. `timestamp - timestamp` across the epoch (one pre-1970 operand) is exact. 8. `compare_validated` raises `TimestampComparisonError` on differing timescale (both set), differing frame (both set), and overlapping uncertainty (both set, delta ≤ combined — pin the boundary case delta == combined as raising); returns -1/0/1 otherwise. `can_compare` is True when either side's metadata is `None` and ignores uncertainty. 9. `as_datetime`/`from_datetime` round-trips to microsecond precision, including a pre-epoch instant. -10. mypy strict clean; all classes pass `isinstance(x, )`. +10. mypy strict clean; all classes structurally conform to their Tier-2 + ``. The ABC is a non-runtime_checkable `typing.Protocol`, so + conformance is verified by member presence (`hasattr` over + `.__abstractmethods__`), not `isinstance`. diff --git a/.claude/specs/spatialMath.md b/.claude/specs/spatialMath.md index 6311158..6c88555 100644 --- a/.claude/specs/spatialMath.md +++ b/.claude/specs/spatialMath.md @@ -1,5 +1,5 @@ --- -version: 1.0 +version: 1.1 type: specification name: spatialMath purpose: Behavioral contract for the Position, Quaternion, and SpatialPose math types @@ -227,7 +227,9 @@ robotics reading) ## Compliance requirements (test-checkable) -1. All three classes satisfy `isinstance` of their ABC and round-trip +1. All three classes structurally conform to their ABC — member presence over + the ABC's `__abstractmethods__`; the Tier-2 ABCs are non-runtime_checkable + `typing.Protocol`s, so this is not `isinstance`-checkable — and round-trip `to_dict`/`from_dict` against the matching `foundationTypes` generated Type's wire output. The target names are `PositionType`, `QuaternionType`, and `SpatialTransformType`; verification is meaningful diff --git a/.claude/specs/waveformCore.md b/.claude/specs/waveformCore.md index 38ec50b..8819492 100644 --- a/.claude/specs/waveformCore.md +++ b/.claude/specs/waveformCore.md @@ -1,5 +1,5 @@ --- -version: 1.0 +version: 1.1 type: specification name: waveformCore purpose: Behavioral contract for Waveform1D and the aggregate spatial waveform containers @@ -229,7 +229,9 @@ All three share (with `Element` = `Position` / `Quaternion` / `SpatialPose`): ## Compliance requirements (test-checkable) -1. All four classes: `isinstance` of their ABC; `to_dict` output loads via +1. All four classes: structural conformance to their ABC (member presence over + the ABC's `__abstractmethods__`; the Tier-2 ABCs are non-runtime_checkable + `typing.Protocol`s, not `isinstance`-checkable); `to_dict` output loads via the matching `foundationTypes` generated Type and round-trips equal — target names: `Waveform1D` ↔ `ScalarWaveformType`, `WaveformPosition` ↔ `PositionWaveformType`, diff --git a/tests/precision_time/test_precision_time_interval.py b/tests/precision_time/test_precision_time_interval.py index 1022844..e090312 100644 --- a/tests/precision_time/test_precision_time_interval.py +++ b/tests/precision_time/test_precision_time_interval.py @@ -375,10 +375,16 @@ def test_division_by_foreign_type_raises_type_error(self) -> None: class TestIsinstanceAndAbc(unittest.TestCase): - def test_isinstance_of_abc(self) -> None: - """Compliance 10: isinstance(x, PrecisionTimeIntervalABC).""" + def test_structural_conformance_to_abc(self) -> None: + """Compliance 10: PrecisionTimeInterval structurally conforms to + PrecisionTimeIntervalABC. + + The Tier-2 ABC is a non-runtime_checkable ``typing.Protocol``, so + conformance is verified by member presence rather than ``isinstance``. + """ interval = PrecisionTimeInterval.from_seconds(1) - self.assertIsInstance(interval, PrecisionTimeIntervalABC) + for member in PrecisionTimeIntervalABC.__abstractmethods__: + self.assertTrue(hasattr(interval, member), member) class TestSerializationRoundTrip(unittest.TestCase): diff --git a/tests/precision_time/test_precision_timestamp.py b/tests/precision_time/test_precision_timestamp.py index ae03350..de2aecc 100644 --- a/tests/precision_time/test_precision_timestamp.py +++ b/tests/precision_time/test_precision_timestamp.py @@ -417,10 +417,16 @@ def test_subtraction_with_foreign_type_raises_type_error(self) -> None: class TestIsinstanceAndAbc(unittest.TestCase): - def test_isinstance_of_abc(self) -> None: - """Compliance 10: isinstance(x, PrecisionTimestampABC).""" + def test_structural_conformance_to_abc(self) -> None: + """Compliance 10: PrecisionTimestamp structurally conforms to + PrecisionTimestampABC. + + The Tier-2 ABC is a non-runtime_checkable ``typing.Protocol``, so + conformance is verified by member presence rather than ``isinstance``. + """ ts = PrecisionTimestamp(seconds=1) - self.assertIsInstance(ts, PrecisionTimestampABC) + for member in PrecisionTimestampABC.__abstractmethods__: + self.assertTrue(hasattr(ts, member), member) class TestSerializationRoundTrip(unittest.TestCase): diff --git a/tests/spatial/test_position.py b/tests/spatial/test_position.py index f98db91..1f56f57 100644 --- a/tests/spatial/test_position.py +++ b/tests/spatial/test_position.py @@ -466,9 +466,13 @@ def test_asarray_is_independent_copy(self) -> None: class TestIsinstanceAndAbc(unittest.TestCase): - def test_isinstance_of_abc(self) -> None: + def test_structural_conformance_to_abc(self) -> None: + """Position structurally conforms to PositionABC (a non-runtime_checkable + ``typing.Protocol``), verified by member presence rather than + ``isinstance``.""" p = Position(1.0, 2.0, 3.0) - self.assertIsInstance(p, PositionABC) + for member in PositionABC.__abstractmethods__: + self.assertTrue(hasattr(p, member), member) class TestSerializationRoundTrip(unittest.TestCase): diff --git a/tests/spatial/test_spatial_pose.py b/tests/spatial/test_spatial_pose.py index 8f5a391..b3e850b 100644 --- a/tests/spatial/test_spatial_pose.py +++ b/tests/spatial/test_spatial_pose.py @@ -55,9 +55,13 @@ def test_from_components_explicit(self) -> None: self.assertEqual((pose.x, pose.y, pose.z), (1.0, 2.0, 3.0)) self.assertEqual((pose.qw, pose.qx, pose.qy, pose.qz), (0.0, 1.0, 0.0, 0.0)) - def test_isinstance_of_abc(self) -> None: + def test_structural_conformance_to_abc(self) -> None: + """SpatialPose structurally conforms to SpatialTransformABC (a + non-runtime_checkable ``typing.Protocol``), verified by member presence + rather than ``isinstance``.""" pose = SpatialPose() - self.assertIsInstance(pose, SpatialTransformABC) + for member in SpatialTransformABC.__abstractmethods__: + self.assertTrue(hasattr(pose, member), member) class TestFromHomogeneous(unittest.TestCase): diff --git a/tests/test_quaternion.py b/tests/test_quaternion.py index 9fd97e8..f65a1c2 100644 --- a/tests/test_quaternion.py +++ b/tests/test_quaternion.py @@ -804,19 +804,28 @@ def test_dict_modification_doesnt_affect_quaternion(self) -> None: self.assertEqual(q.w, 1.0) self.assertEqual(q.x, 2.0) - def test_inheritance_from_quaternion_type(self) -> None: - """Test that Quaternion properly inherits from the shared QuaternionABC.""" + def test_structural_conformance_to_abc(self) -> None: + """Quaternion structurally conforms to QuaternionABC. + + The Tier-2 ABC is a non-runtime_checkable ``typing.Protocol``, so + conformance is verified by member presence rather than ``isinstance``. + """ from foundation_abc.math.spatialABCs import QuaternionABC q = Quaternion.from_components(1.0, 2.0, 3.0, 4.0) - self.assertIsInstance(q, QuaternionABC) + for member in QuaternionABC.__abstractmethods__: + self.assertTrue(hasattr(q, member), member) - def test_inheritance_from_data_model_helper(self) -> None: - """Test that Quaternion inherits from QuaternionABC (ABC-only, not DataModelHelper).""" + def test_conforms_to_abc_without_carrier_inheritance(self) -> None: + """Quaternion conforms to QuaternionABC structurally and does not inherit + the Tier-1 ``DataModelHelper`` carrier base (ABC-only conformance).""" from foundation_abc.math.spatialABCs import QuaternionABC q = Quaternion.from_components(1.0, 2.0, 3.0, 4.0) - self.assertIsInstance(q, QuaternionABC) + for member in QuaternionABC.__abstractmethods__: + self.assertTrue(hasattr(q, member), member) + mro_names = {base.__name__ for base in type(q).__mro__} + self.assertNotIn("DataModelHelper", mro_names) def test_has_serialization_methods(self) -> None: """Test that Quaternion has the required serialization methods.""" diff --git a/tests/waveforms/test_waveform1d_core.py b/tests/waveforms/test_waveform1d_core.py index d05f763..e253486 100644 --- a/tests/waveforms/test_waveform1d_core.py +++ b/tests/waveforms/test_waveform1d_core.py @@ -75,10 +75,15 @@ def test_from_dict_reconstructs_equal_waveform(self) -> None: w2 = Waveform1D.from_dict(w.to_dict()) self.assertEqual(w, w2) - def test_isinstance_of_abc(self) -> None: + def test_structural_conformance_to_abc(self) -> None: + """Waveform1D structurally conforms to Waveform1dABC (a + non-runtime_checkable ``typing.Protocol``), verified by member presence + rather than ``isinstance``.""" from foundation_abc.math.waveformABCs import Waveform1dABC - self.assertIsInstance(Waveform1D([1.0]), Waveform1dABC) + w = Waveform1D([1.0]) + for member in Waveform1dABC.__abstractmethods__: + self.assertTrue(hasattr(w, member), member) class TestConstruction(unittest.TestCase): diff --git a/tests/waveforms/test_waveform_position.py b/tests/waveforms/test_waveform_position.py index 9dc37bb..04fe76b 100644 --- a/tests/waveforms/test_waveform_position.py +++ b/tests/waveforms/test_waveform_position.py @@ -38,8 +38,13 @@ def test_from_dict_reconstructs_equal_waveform(self) -> None: w2 = WaveformPosition.from_dict(w.to_dict()) self.assertEqual(w, w2) - def test_isinstance_of_abc(self) -> None: - self.assertIsInstance(_make(), PositionWaveformABC) + def test_structural_conformance_to_abc(self) -> None: + """WaveformPosition structurally conforms to PositionWaveformABC (a + non-runtime_checkable ``typing.Protocol``), verified by member presence + rather than ``isinstance``.""" + wp = _make() + for member in PositionWaveformABC.__abstractmethods__: + self.assertTrue(hasattr(wp, member), member) def test_no_abstract_methods_remain(self) -> None: self.assertEqual(WaveformPosition.__abstractmethods__, frozenset()) diff --git a/tests/waveforms/test_waveform_quaternion.py b/tests/waveforms/test_waveform_quaternion.py index 23423b7..ec928d9 100644 --- a/tests/waveforms/test_waveform_quaternion.py +++ b/tests/waveforms/test_waveform_quaternion.py @@ -41,8 +41,13 @@ def test_from_dict_reconstructs_equal_waveform(self) -> None: w2 = WaveformQuaternion.from_dict(w.to_dict()) self.assertEqual(w, w2) - def test_isinstance_of_abc(self) -> None: - self.assertIsInstance(_make(), QuaternionWaveformABC) + def test_structural_conformance_to_abc(self) -> None: + """WaveformQuaternion structurally conforms to QuaternionWaveformABC (a + non-runtime_checkable ``typing.Protocol``), verified by member presence + rather than ``isinstance``.""" + wq = _make() + for member in QuaternionWaveformABC.__abstractmethods__: + self.assertTrue(hasattr(wq, member), member) def test_no_abstract_methods_remain(self) -> None: self.assertEqual(WaveformQuaternion.__abstractmethods__, frozenset()) diff --git a/tests/waveforms/test_waveform_spatial_pose.py b/tests/waveforms/test_waveform_spatial_pose.py index fbe0d8f..67fe3c2 100644 --- a/tests/waveforms/test_waveform_spatial_pose.py +++ b/tests/waveforms/test_waveform_spatial_pose.py @@ -55,8 +55,13 @@ def test_from_dict_reconstructs_equal_waveform(self) -> None: w2 = WaveformSpatialPose.from_dict(w.to_dict()) self.assertEqual(w, w2) - def test_isinstance_of_abc(self) -> None: - self.assertIsInstance(_make(), WaveformSpatialABC) + def test_structural_conformance_to_abc(self) -> None: + """WaveformSpatialPose structurally conforms to WaveformSpatialABC (a + non-runtime_checkable ``typing.Protocol``), verified by member presence + rather than ``isinstance``.""" + wsp = _make() + for member in WaveformSpatialABC.__abstractmethods__: + self.assertTrue(hasattr(wsp, member), member) def test_no_abstract_methods_remain(self) -> None: self.assertEqual(WaveformSpatialPose.__abstractmethods__, frozenset()) From 37e12c52c3ab02880ebc82ac51162199f434eb38 Mon Sep 17 00:00:00 2001 From: kopecn Date: Tue, 8 Sep 2026 14:45:33 -0700 Subject: [PATCH 05/12] missing dev deps --- pyproject.toml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 39b4462..a701c85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,12 +43,16 @@ dependencies = [ dev = [ "pytest", "pytest-asyncio", - "ruff", + "flake8", + "flake8-bugbear", + "black", + "mypy", "build", "bump2version", - "mypy", - "ty", "twine", + # second-class (available, not in the gate): + "ruff", + "ty", ] [project.urls] From 3c372887065231e03ac24a3ac3c6dc511a0ba2d5 Mon Sep 17 00:00:00 2001 From: kopecn Date: Tue, 8 Sep 2026 14:50:31 -0700 Subject: [PATCH 06/12] makefile bug... --- Makefile | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c6b6669..7034ef3 100644 --- a/Makefile +++ b/Makefile @@ -26,11 +26,13 @@ ifneq (,$(wildcard .env)) endif # Defaults (overridable via .env — the user-editable surface). Keep in sync with .env. -PYTHONS ?= 3.11 3.12 3.13 3.14 3.15 +PYTHONS ?= 3.11 3.12 3.13 DEFAULT_PYTHON ?= 3.13 PYTHON ?= python3 VENV ?= .cleanroom-venv +# Quality-target paths. ROOT half has NO src/ — its Python lives in hooks/ + tests/ +# (see GAPS.md §6). The template half overrides these to src/. Overridable via .env. PY_SRC ?= hooks PY_TESTS ?= tests PY_EXAMPLES ?= @@ -311,6 +313,7 @@ uv-test-all: check-uv ## Run tests across all configured Python versions (.venv venv=".venvs/$$py"; \ [ -d "$$venv" ] || uv venv --python $$py "$$venv"; \ if ( . "$$venv/bin/activate" && \ + uv pip install -q -r requirements.txt && \ uv pip install -q -e ".[dev]" && \ python -m pytest ); then \ echo "PASS: Python $$py"; \ @@ -559,4 +562,24 @@ list: ## List pip packages in available environments echo "$(HR)"; \ $$venv/bin/pip list 2>/dev/null || echo "No packages or pip not available"; \ echo; \ - done \ No newline at end of file + done + +# ============================================================================ +# MARK: - Codegen +# ============================================================================ + +# Base dir for generated Python types; mirrors _PYTHON_TYPES_BASE in +# schema/scripts/reuse/codegen.sh. Kept in sync so the fleet-wide normalization +# sweep targets every generated model regardless of its generate script. +_PYTHON_TYPES_BASE := src/foundationTypes + +codegen-all: check-uv ## Run all schema codegen scripts in schema/scripts/ + @for script in schema/scripts/*.sh; do \ + echo "Generating: $$script"; \ + bash "$$script"; \ + done + @echo "Normalizing all generated models (fleet-wide DataModelHelper contract)..." + @bash schema/scripts/reuse/normalize_generated.sh $(_PYTHON_TYPES_BASE) + @echo "Formatting the generated tree with black (first-class formatter)..." + $(UV) black $(_PYTHON_TYPES_BASE) + @echo "-- fini --" \ No newline at end of file From 1da942d0e69caeb41c311ccb70bb0f232d76a051 Mon Sep 17 00:00:00 2001 From: kopecn Date: Tue, 8 Sep 2026 21:34:43 -0700 Subject: [PATCH 07/12] known broken for 3.15 --- .env | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.env b/.env index 0d75f3e..8aa2253 100644 --- a/.env +++ b/.env @@ -11,6 +11,14 @@ DEFAULT_PYTHON=3.13 # Make expands `$(PYTHONS)` straight into `uv python install ...` as separate args. # Do NOT quote or comma-separate — that collapses it into one bogus argument. # Canonical set: default 3.13, support floor 3.11 (matches requires-python>=3.11). +# KNOWN BROKEN — 3.15: `uv-test-all`/`uv-test-matrix` segfault on import of the +# scipy and numpy-quaternion C-extensions under cpython-3.15.0b3. Root cause is +# upstream Cython, not this repo: 3.15 makes PyObject/exception-state internals +# opaque and the currently-published cp315 wheels' Cython-generated code +# (e.g. scipy `_cyutility` `__Pyx__ExceptionSave`) still dereferences them -> +# null SIGSEGV. numpy/matplotlib (non-Cython-core) are fine. Tracking: Cython +# cython/cython#7399. 3.15 is kept here intentionally as an early-warning canary; +# it clears once Cython lands #7399 and scipy/numpy-quaternion republish wheels. PYTHONS=3.11 3.12 3.13 3.14 3.15 # Throwaway clean-room venv for `make testInEnv` (deleted on teardown). From 3cb0d5d8e56dddba789fa5696eddc60579c27323 Mon Sep 17 00:00:00 2001 From: kopecn Date: Thu, 24 Sep 2026 09:47:03 -0700 Subject: [PATCH 08/12] bumping env and makefile --- .env | 28 +++------- Makefile | 160 ++++++++++++++++++------------------------------------- 2 files changed, 60 insertions(+), 128 deletions(-) diff --git a/.env b/.env index 8aa2253..b1b0169 100644 --- a/.env +++ b/.env @@ -1,31 +1,17 @@ -# This file is parsed by GNU Make (`include .env` in the Makefile), not a shell. -# Values are Make variables expanded UNQUOTED into recipes — keep that in mind below. +# Development environment settings used by the Makefile. -# Python interpreter for pip-based targets (single token — used as `$(PYTHON) -m pip`). +# Python interpreter used by pip-based targets. PYTHON=python3 -# Default version for single-version uv operations. MUST appear in PYTHONS below. +# Default Python version. This version must also be listed in PYTHONS. DEFAULT_PYTHON=3.13 -# Versions to test against. SPACE-SEPARATED, UNQUOTED on purpose: -# Make expands `$(PYTHONS)` straight into `uv python install ...` as separate args. -# Do NOT quote or comma-separate — that collapses it into one bogus argument. -# Canonical set: default 3.13, support floor 3.11 (matches requires-python>=3.11). -# KNOWN BROKEN — 3.15: `uv-test-all`/`uv-test-matrix` segfault on import of the -# scipy and numpy-quaternion C-extensions under cpython-3.15.0b3. Root cause is -# upstream Cython, not this repo: 3.15 makes PyObject/exception-state internals -# opaque and the currently-published cp315 wheels' Cython-generated code -# (e.g. scipy `_cyutility` `__Pyx__ExceptionSave`) still dereferences them -> -# null SIGSEGV. numpy/matplotlib (non-Cython-core) are fine. Tracking: Cython -# cython/cython#7399. 3.15 is kept here intentionally as an early-warning canary; -# it clears once Cython lands #7399 and scipy/numpy-quaternion republish wheels. +# Python versions tested by the generated project. Separate versions with spaces. PYTHONS=3.11 3.12 3.13 3.14 3.15 -# Throwaway clean-room venv for `make testInEnv` (deleted on teardown). -# Kept DISTINCT from uv's `.venv` so the clean-room never nukes your dev env. +# Temporary virtual environment used by `make testInEnv`. +# It is separate from the `.venv` development environment and is removed after use. VENV=.cleanroom-venv -# Quality-target source dir. A generated project uses a src/ layout (no hooks/), -# so override the Makefile's root default (PY_SRC=hooks) → src here. This is what -# `make uv-lint` / `uv-typecheck` scan; the Makefile's PY_SRC ?= is the fallback. +# Source directory checked by linting and type-checking targets. PY_SRC=src diff --git a/Makefile b/Makefile index 7034ef3..1f4ca66 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,6 @@ -# ============================================================================ -# CONFIG -# ============================================================================ +# Configuration .PHONY: help version checkCleanGit open-github \ - clean clean-build clean-artifacts clean-test \ + clean clean-build clean-artifacts clean-test clean-node \ bump-patch bump-minor bump-major \ check-uv install-uv list-uv \ uv-bootstrap-pythons uv-bootstrap uv-sync uv-sync-headless uv-sync-dev uv-sync-release uv-sync-local uv-editable uv-refresh \ @@ -19,38 +17,30 @@ .DEFAULT_GOAL := help -# Load .env file if it exists +# Load local settings when available. ifneq (,$(wildcard .env)) include .env export endif -# Defaults (overridable via .env — the user-editable surface). Keep in sync with .env. -PYTHONS ?= 3.11 3.12 3.13 +# Defaults can be overridden in .env. +PYTHONS ?= 3.11 3.12 3.13 3.14 3.15 DEFAULT_PYTHON ?= 3.13 PYTHON ?= python3 VENV ?= .cleanroom-venv -# Quality-target paths. ROOT half has NO src/ — its Python lives in hooks/ + tests/ -# (see GAPS.md §6). The template half overrides these to src/. Overridable via .env. +# Paths checked by linting and type-checking targets. PY_SRC ?= hooks PY_TESTS ?= tests PY_EXAMPLES ?= PY_ALL ?= $(PY_SRC) $(PY_TESTS) $(PY_EXAMPLES) -# mypy cannot CRAWL a src/ layout that is editable-installed: the editable .pth puts -# src/ on sys.path, so each module resolves under both `pkg` and `src.pkg` → mypy's -# "Source file found twice under different module names" error. Drive mypy by package -# NAME instead (resolved via the single src/ root). Top-level packages = src/ subdirs -# that have an __init__.py. Empty for the hooks/ root variant (no src/), which crawls -# normally. See uv-typecheck. +# Discover packages by name so mypy handles editable src-layout installs correctly. MYPY_PKGS := $(patsubst src/%/,-p %,$(sort $(dir $(wildcard src/*/__init__.py)))) -# Derived -# Tool runner for uv- quality/test recipes. `--extra dev` ensures ruff/mypy/pytest are -# resolved (and installed if missing) from the "[dev]" extra even on a FRESH checkout — -# no reliance on a pre-existing .venv, rather than the ambient PATH. +# Derived settings +# Run quality and test tools through uv with the development dependencies available. UV := uv run --no-project PIP := $(PYTHON) -m pip BUMPVERSION := bumpversion --allow-dirty @@ -58,14 +48,12 @@ REPO := $(notdir $(CURDIR)) UNAME_S := $(shell uname -s) HR := ======================================== -# Guard: DEFAULT_PYTHON must be one of the versions we test against. +# The default version must be included in the test matrix. ifeq ($(filter $(DEFAULT_PYTHON),$(PYTHONS)),) $(error DEFAULT_PYTHON ($(DEFAULT_PYTHON)) is not in PYTHONS ($(PYTHONS)) — fix .env) endif -# ============================================================================ -# MARK: - Helpers · -# ============================================================================ +# Helpers define uninstall_package_list @$(1) | while read pkg; do \ @@ -83,10 +71,7 @@ define print_packages @echo endef -# Roll HISTORY.md on a version bump: open a fresh dated section under -# [Unreleased] (folding the accumulated notes into the just-bumped version) and -# amend it into bump2version's commit so version + changelog move together. -# Keep-a-Changelog convention: the `## [Unreleased]` header is the anchor. +# Add a dated release section to HISTORY.md and include it in the version commit. define roll_changelog @ver=$$($(MAKE) -s version); day=$$(date +%F); \ awk -v v="$$ver" -v d="$$day" '\ @@ -125,8 +110,7 @@ checkCleanGit: ## Guard: fail if the git working tree is dirty @[ -z "$$(git status --porcelain)" ] || \ (echo "Working tree is dirty. Commit or stash changes first."; exit 1) -# Static pattern rule: all three documented parts share one recipe (`$*` = the -# part). Bump the version, then roll the changelog into the same commit. +# Version bump targets share this recipe and update the changelog. bump-patch bump-minor bump-major: bump-%: ## Bump version (patch|minor|major) + roll HISTORY.md $(BUMPVERSION) $* $(call roll_changelog) @@ -142,11 +126,11 @@ open-github: ## Open the GitHub repository in the default browser (macOS/Linux) # ============================================================================ # MARK: - COMMON · CLEAN -# Base cleanup targets used by install, test, and CI workflows. +# Cleanup targets used by development and CI workflows. # ============================================================================ ##@ Common · Clean -clean: clean-build clean-artifacts clean-test ## Remove all build, cache, and test artifacts +clean: clean-build clean-artifacts clean-test clean-node ## Remove all build, cache, and test artifacts clean-build: ## Remove packaging and distribution artifacts rm -rf build/ dist/ .eggs/ @@ -171,6 +155,17 @@ clean-test: ## Remove test, coverage, and lint caches .tox/ \ .nox/ +clean-node: ## Recursively remove node_modules and its sibling package.json/lock files + find . -name 'node_modules' -type d -prune | while read -r dir; do \ + parent="$$(dirname "$$dir")"; \ + rm -f "$$parent/package.json" \ + "$$parent/package-lock.json" \ + "$$parent/npm-shrinkwrap.json" \ + "$$parent/yarn.lock" \ + "$$parent/pnpm-lock.yaml"; \ + rm -rf "$$dir"; \ + done + # ============================================================================ # MARK: - UV · TOOLING # ============================================================================ @@ -223,11 +218,8 @@ list-uv: check-uv ## List uv envs, installed Pythons, packages, and cache info uv-bootstrap-pythons: check-uv ## Install all configured Python versions via uv uv python install $(PYTHONS) -# Dependency model (BKM; see GAPS §5 / spec §6): pyproject.toml declares dependency -# NAMES ONLY — never version-pinned (only the application layer pins; module-level pins -# cause conflicts). The requirements*.txt files carry pins and git-based pointers, and -# every install path — pip AND uv — leans on them: `-r requirements.txt` then the -# editable self-install. No `uv.lock`, no `lock`/compile target. +# Dependency versions and source overrides are defined in requirements files. +# Install requirements before installing this package in editable mode. uv-bootstrap: check-uv uv-bootstrap-pythons ## Full bootstrap: pythons + venv + deps uv venv --python $(DEFAULT_PYTHON) @@ -275,7 +267,7 @@ uv-refresh: check-uv ## Clean cache + reinstall from requirements + upgrade edi # ============================================================================ # MARK: - UV · QUALITY # ============================================================================ -##@ UV · Quality (second-class uv RUNNER for the first-class flake8/black/mypy tools) +##@ UV · Quality uv-lint: check-uv ## Run flake8 via uv (read-only; non-zero exit for CI) $(UV) flake8 $(PY_ALL) @@ -290,16 +282,14 @@ else $(UV) mypy $(PY_TESTS) $(PY_EXAMPLES) endif -# ty (Astral's preview type-checker) is intentionally OUT for now (decision D1): -# it's pre-release and not wired into uv-fullCheck. Revisit when it stabilizes. +# The preview `ty` type checker is not included in the quality gate. uv-fullCheck: check-uv uv-lint uv-typecheck uv-test ## lint + typecheck + tests # ============================================================================ # MARK: - UV · TEST # ============================================================================ ##@ UV · Test -# Depends on uv-sync so a fresh checkout never tests an empty/stale .venv (no -# false-green no-op): the [dev] extra is installed from pyproject before pytest runs. +# Synchronize development dependencies before running tests. uv-test: check-uv uv-sync ## Run tests on DEFAULT_PYTHON (ensures a synced env first) $(UV) pytest @@ -375,10 +365,8 @@ uv-lifecycle-test: uv-flush-everything uv-bootstrap uv-test-all ## flush -> boo # MARK: - PIP · INSTALL # ============================================================================ ##@ PIP · Install -# Ambient-pip fallback (prefer the uv- path). Both pip and uv lean on the requirements -# file (BKM rule 4): install -r requirements.txt, then self-install the editable -# package. No --break-system-packages / --force-reinstall: use a venv (make uv-sync) -# rather than fighting an externally-managed interpreter. +# These targets use the active Python environment. Prefer the uv workflow for an +# isolated development environment. installDev: clean ## Install dev dependencies with pip (-r requirements.txt + editable [dev]) $(PIP) install -r requirements.txt $(PIP) install -e ".[dev]" @@ -391,16 +379,16 @@ refresh: ## Refresh pip packages: reinstall from requirements + upgrade editabl $(PIP) install --upgrade -e ".[dev]" # ============================================================================ -# MARK: - PIP · QUALITY (FIRST-CLASS) +# MARK: - PIP · QUALITY # ============================================================================ -##@ PIP · Quality (FIRST-CLASS: flake8 + black + mypy, run on ambient $(PYTHON)) -lint: ## Run flake8 (read-only; non-zero exit for CI) — first-class +##@ PIP · Quality +lint: ## Run flake8 using the active Python environment $(PYTHON) -m flake8 $(PY_ALL) -format: ## Format code with black — first-class +format: ## Format code with black using the active Python environment $(PYTHON) -m black $(PY_ALL) -typecheck: ## Strict type check with mypy — first-class +typecheck: ## Run strict type checks with mypy ifeq ($(strip $(MYPY_PKGS)),) $(PYTHON) -m mypy $(PY_SRC) $(PY_TESTS) $(PY_EXAMPLES) else @@ -408,15 +396,10 @@ else $(PYTHON) -m mypy $(PY_TESTS) $(PY_EXAMPLES) endif -fullCheck: lint typecheck test ## FIRST-CLASS gate: flake8 + mypy + pytest +fullCheck: lint typecheck test ## Run linting, type checks, and tests -# nuke's inverse (see the comment on `nuke`). Rebuilds the build backend -# (setuptools/wheel) that `ensurepip` never bundles on Python >= 3.12 (E1). -# Deliberately NOT wired as a prereq of installDev/e/refresh/build (D1) — those -# targets keep failing loudly on their own terms rather than growing a guard -# layer; check-pip is scoped only to the clean-room target (D2, see C2 comment -# on cleanRoomBootstrap below). -pip-bootstrap: ## Rebuild the ambient build backend after `nuke` (NETWORK REQUIRED) +# Restore pip and its build tools after running `make nuke`. +pip-bootstrap: ## Restore pip build tools after `nuke` (requires network access) @echo "Bootstrapping ambient pip + build backend (setuptools, wheel)..." $(PYTHON) -m ensurepip --upgrade $(PIP) install --upgrade setuptools wheel @@ -436,21 +419,8 @@ test: ## Run tests using the current Python environment cleanRoomCleanup: ## Delete the clean-room venv ($(VENV)) rm -rf $(VENV) || true -# check-pip guard. Assert ONLY what the clean room actually needs: that the -# ambient interpreter can build a working venv, i.e. `ensurepip` is present. -# -# It deliberately does NOT assert that `setuptools.build_meta` imports on the -# ambient interpreter. The clean room installs into $(VENV) under PEP-517 build -# isolation, which provisions its own setuptools from PyPI — the ambient -# interpreter's build backend is never consulted. Guarding on it produced a -# false negative that blocked a clean room which then succeeded when run by -# hand. That assertion is only meaningful for --no-build-isolation ambient -# installs (`e`, `installDev`), which D1 deliberately leaves ungated. -# -# NETWORK REQUIRED for the recipe below: since Python 3.12 ensurepip seeds pip -# only (E1), so a fresh venv has no build backend and PEP-517 isolation must -# reach PyPI. A backend-less ambient interpreter does not change that either -# way, which is precisely why it is not worth guarding here. +# Verify that the active Python can create a virtual environment with pip. +# Clean-room setup requires network access to install build dependencies. check-pip: ## Check the ambient interpreter can create the clean-room venv @$(PYTHON) -m ensurepip --version >/dev/null 2>&1 || { \ echo "ERROR: ensurepip unavailable on $(PYTHON) — cannot create $(VENV)."; \ @@ -458,14 +428,8 @@ check-pip: ## Check the ambient interpreter can create the clean-room venv echo " Or use the uv path: make uv-sync"; \ exit 1; } -# The clean room is an install path, so it obeys the same BKM rule as every other -# one (see the dependency-model comment above ##@ UV · Bootstrap): pyproject.toml -# declares dependency NAMES ONLY, and requirements.txt carries the pins and the -# git/path pointers. Installing ".[dev]" alone makes pip resolve those bare names -# against PyPI, which fails outright for any unpublished sibling dependency -# (`No matching distribution found`). Install the requirements file FIRST, then the -# package. Keep ".[dev]" NON-editable here — validating the real packaging path is -# this target's entire purpose. +# Install pinned requirements before the package. The package is installed normally, +# rather than editable, to validate the distribution workflow. cleanRoomBootstrap: cleanRoomCleanup check-pip ## Bootstrap the clean-room venv + deps (runs NO tests) $(PYTHON) -m venv $(VENV) . $(VENV)/bin/activate && \ @@ -486,11 +450,7 @@ testInEnv: clean cleanRoomBootstrap cleanRoomPytest cleanRoomCleanup ## Full cl # MARK: - PIP · BUILD & RELEASE # ============================================================================ ##@ PIP · Build & Release -# build/twine were previously invoked against ambient $(PYTHON), but both are -# declared in [project.optional-dependencies].dev, which installs into .venv — -# not the ambient interpreter (E4, a live bug independent of the FA this track -# is fixing). Route them through `uv run --with` instead so they resolve -# correctly on a checkout whose only setup was `make uv-sync`. +# Run build and publishing tools through uv so no global installation is required. build: check-uv clean-build ## Build sdist + wheel (uv run --with build python -m build) @echo "Building package..." $(UV) --with build python -m build @@ -503,11 +463,8 @@ release-test: checkCleanGit validateBuild ## Dry-run publish to TestPyPI (clean @echo "Uploading $(REPO) v$$($(MAKE) -s version) to TestPyPI..." @$(UV) --with twine twine upload --repository testpypi dist/* -# PyPI publishing is owned by CI, not this Makefile. Per the ci-cd spec, the -# pipeline is the single authoritative path to production — no manual, out-of-band -# uploads. `.github/workflows/tag-on-prod.yml` tags v on push to `prod`; -# a publish-on-tag workflow promotes that artifact. `make release` therefore -# refuses to upload and prints the release procedure instead. +# Production releases are published by CI. This target validates the build and +# displays the release procedure without uploading artifacts. release: validateBuild ## Refuse local upload; print the CI-driven release procedure @echo "Local PyPI upload is disabled — the pipeline is the authoritative publish path." @echo "" @@ -523,18 +480,9 @@ release: validateBuild ## Refuse local upload; print the CI-driven release proc # MARK: - PIP · FLUSH / LIST # ============================================================================ ##@ PIP · Flush / List -# `nuke` is the INFERIOR pip fallback (Lesson 2): it per-package-uninstalls from -# the AMBIENT interpreter ($(PIP)). Prefer `make uv-flush-envs` — deleting the -# venv dir is the reliable flush primitive. Use this only when you're stuck in a -# non-deletable (e.g. system) env. Non-editable URL/VCS installs are skipped. -# -# --exclude setuptools --exclude wheel: at Python >= 3.12, pip/_internal/commands/ -# freeze.py:12-20 stopped suppressing the build backend from `pip freeze` -# (_should_suppress_build_backends() is version-gated below 3.12), so an -# unqualified `freeze --exclude-editable | pip uninstall` now removes the very -# build backend the interpreter needs to install anything afterward — including -# itself. Keep these exclusions; do not "clean up" them in a later refactor. -nuke: ## Per-package uninstall from ambient env (inferior — prefer uv-flush-envs) +# Remove packages from the active Python environment. Prefer `uv-flush-envs` when +# using a virtual environment. Core packaging tools are preserved for recovery. +nuke: ## Remove packages from the active environment (prefer uv-flush-envs) @echo "Uninstalling regular packages (skipping system-managed)..." $(call uninstall_package_list,$(PIP) freeze --exclude-editable --exclude setuptools --exclude wheel | grep -v ' @ ') @@ -568,9 +516,7 @@ list: ## List pip packages in available environments # MARK: - Codegen # ============================================================================ -# Base dir for generated Python types; mirrors _PYTHON_TYPES_BASE in -# schema/scripts/reuse/codegen.sh. Kept in sync so the fleet-wide normalization -# sweep targets every generated model regardless of its generate script. +# Base directory for generated Python types. Keep this aligned with codegen.sh. _PYTHON_TYPES_BASE := src/foundationTypes codegen-all: check-uv ## Run all schema codegen scripts in schema/scripts/ @@ -582,4 +528,4 @@ codegen-all: check-uv ## Run all schema codegen scripts in schema/scripts/ @bash schema/scripts/reuse/normalize_generated.sh $(_PYTHON_TYPES_BASE) @echo "Formatting the generated tree with black (first-class formatter)..." $(UV) black $(_PYTHON_TYPES_BASE) - @echo "-- fini --" \ No newline at end of file + @echo "-- fini --" From 499cba7407a1c0cd333184d887016e909c1b8e02 Mon Sep 17 00:00:00 2001 From: kopecn Date: Thu, 24 Sep 2026 09:55:03 -0700 Subject: [PATCH 09/12] forgot to keep the headings... --- Makefile | 84 +++++++++++++++++++------------------------------------- 1 file changed, 28 insertions(+), 56 deletions(-) diff --git a/Makefile b/Makefile index 1f4ca66..64add2b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Configuration +# MARK: - Configuration .PHONY: help version checkCleanGit open-github \ clean clean-build clean-artifacts clean-test clean-node \ bump-patch bump-minor bump-major \ @@ -39,7 +39,7 @@ PY_ALL ?= $(PY_SRC) $(PY_TESTS) $(PY_EXAMPLES) MYPY_PKGS := $(patsubst src/%/,-p %,$(sort $(dir $(wildcard src/*/__init__.py)))) -# Derived settings +# MARK: - Derived settings # Run quality and test tools through uv with the development dependencies available. UV := uv run --no-project PIP := $(PYTHON) -m pip @@ -53,7 +53,7 @@ ifeq ($(filter $(DEFAULT_PYTHON),$(PYTHONS)),) $(error DEFAULT_PYTHON ($(DEFAULT_PYTHON)) is not in PYTHONS ($(PYTHONS)) — fix .env) endif -# Helpers +# MARK: - Helpers define uninstall_package_list @$(1) | while read pkg; do \ @@ -85,11 +85,9 @@ define roll_changelog esac endef -# ============================================================================ -# MARK: - HELP -# ============================================================================ +# MARK: - Help help: ## Show this help - @echo "$(REPO) — make targets (bare = pip · FIRST-CLASS · uv-… = uv runner · second-class)" + @echo "$(REPO) — available make targets" @echo "config: DEFAULT_PYTHON=$(DEFAULT_PYTHON) PYTHONS=$(PYTHONS)" @echo "" @awk 'BEGIN {FS = ":.*?## "} \ @@ -98,20 +96,18 @@ help: ## Show this help $(MAKEFILE_LIST) -# ============================================================================ -# MARK: - COMMON · VERSION & GIT -# ============================================================================ +# MARK: - Version and Git ##@ Common · Version & Git version: ## Display the current project version @$(PYTHON) -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])" 2>/dev/null \ || grep -m1 '^version' pyproject.toml | cut -d'"' -f2 -checkCleanGit: ## Guard: fail if the git working tree is dirty +checkCleanGit: ## Verify that the Git working tree is clean @[ -z "$$(git status --porcelain)" ] || \ (echo "Working tree is dirty. Commit or stash changes first."; exit 1) # Version bump targets share this recipe and update the changelog. -bump-patch bump-minor bump-major: bump-%: ## Bump version (patch|minor|major) + roll HISTORY.md +bump-patch bump-minor bump-major: bump-%: ## Bump the version and update HISTORY.md $(BUMPVERSION) $* $(call roll_changelog) @@ -124,10 +120,8 @@ open-github: ## Open the GitHub repository in the default browser (macOS/Linux) elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$$url"; \ else echo "No browser opener found; visit: $$url"; fi -# ============================================================================ -# MARK: - COMMON · CLEAN +# MARK: - Cleanup # Cleanup targets used by development and CI workflows. -# ============================================================================ ##@ Common · Clean clean: clean-build clean-artifacts clean-test clean-node ## Remove all build, cache, and test artifacts @@ -155,7 +149,7 @@ clean-test: ## Remove test, coverage, and lint caches .tox/ \ .nox/ -clean-node: ## Recursively remove node_modules and its sibling package.json/lock files +clean-node: ## Remove node_modules directories and related package files find . -name 'node_modules' -type d -prune | while read -r dir; do \ parent="$$(dirname "$$dir")"; \ rm -f "$$parent/package.json" \ @@ -166,11 +160,9 @@ clean-node: ## Recursively remove node_modules and its sibling package.json/lock rm -rf "$$dir"; \ done -# ============================================================================ -# MARK: - UV · TOOLING -# ============================================================================ +# MARK: - uv tooling ##@ UV · Tooling -check-uv: ## Check if uv is installed (guard for all uv- targets) +check-uv: ## Verify that uv is installed @command -v uv >/dev/null 2>&1 || { \ echo "ERROR: uv not found."; \ echo " Install it with: make install-uv"; \ @@ -211,9 +203,7 @@ list-uv: check-uv ## List uv envs, installed Pythons, packages, and cache info @uv cache dir @du -sh $$(uv cache dir) 2>/dev/null || echo "Cache empty or not accessible" -# ============================================================================ -# MARK: - UV · BOOTSTRAP & SYNC -# ============================================================================ +# MARK: - uv setup and synchronization ##@ UV · Bootstrap & Sync uv-bootstrap-pythons: check-uv ## Install all configured Python versions via uv uv python install $(PYTHONS) @@ -264,11 +254,9 @@ uv-refresh: check-uv ## Clean cache + reinstall from requirements + upgrade edi uv pip install -r requirements.txt uv pip install --upgrade -e ".[dev]" -# ============================================================================ -# MARK: - UV · QUALITY -# ============================================================================ +# MARK: - uv quality checks ##@ UV · Quality -uv-lint: check-uv ## Run flake8 via uv (read-only; non-zero exit for CI) +uv-lint: check-uv ## Run flake8 via uv $(UV) flake8 $(PY_ALL) uv-format: check-uv ## Format code with black via uv @@ -283,11 +271,9 @@ else endif # The preview `ty` type checker is not included in the quality gate. -uv-fullCheck: check-uv uv-lint uv-typecheck uv-test ## lint + typecheck + tests +uv-fullCheck: check-uv uv-lint uv-typecheck uv-test ## Run linting, type checks, and tests -# ============================================================================ -# MARK: - UV · TEST -# ============================================================================ +# MARK: - uv tests ##@ UV · Test # Synchronize development dependencies before running tests. uv-test: check-uv uv-sync ## Run tests on DEFAULT_PYTHON (ensures a synced env first) @@ -325,9 +311,7 @@ uv-test-all: check-uv ## Run tests across all configured Python versions (.venv uv-test-matrix: uv-bootstrap-pythons uv-test-all ## Ensure Pythons installed, then run all tests -# ============================================================================ -# MARK: - UV · FLUSH / NUKE -# ============================================================================ +# MARK: - uv environment cleanup ##@ UV · Flush / Nuke uv-flush-envs: ## Remove all virtual environments (.venv + .venvs/) @@ -342,7 +326,7 @@ uv-flush-cache: check-uv ## Clean uv cache uv cache clean @echo "uv cache cleaned." -uv-flush-pythons: ## Remove uv-managed Python installs (NUCLEAR) +uv-flush-pythons: ## Remove uv-managed Python installations @echo "WARNING: This removes ALL uv-managed Python installations!" @echo "Location: ~/.local/share/uv/python" rm -rf ~/.local/share/uv/python @@ -351,19 +335,17 @@ uv-flush-pythons: ## Remove uv-managed Python installs (NUCLEAR) uv-flush-everything: clean uv-flush-envs uv-flush-cache ## Full cleanup (keeps pythons) @echo "Environment flushed. Run 'make uv-flush-pythons' separately for global Pythons." -uv-nuke: uv-flush-everything ## NUCLEAR: everything then prompt for Python removal +uv-nuke: uv-flush-everything ## Clean all uv data and optionally remove Python installations @echo "" @echo ">> Running uv-nuke..." @$(MAKE) uv-flush-pythons @echo "" @echo "Environment nuked. Run 'make uv-bootstrap' to rebuild from scratch." -uv-lifecycle-test: uv-flush-everything uv-bootstrap uv-test-all ## flush -> bootstrap -> test-all +uv-lifecycle-test: uv-flush-everything uv-bootstrap uv-test-all ## Rebuild the uv environment and run the test matrix @echo ">> Lifecycle test complete" -# ============================================================================ -# MARK: - PIP · INSTALL -# ============================================================================ +# MARK: - pip installation ##@ PIP · Install # These targets use the active Python environment. Prefer the uv workflow for an # isolated development environment. @@ -378,9 +360,7 @@ refresh: ## Refresh pip packages: reinstall from requirements + upgrade editabl $(PIP) install -r requirements.txt $(PIP) install --upgrade -e ".[dev]" -# ============================================================================ -# MARK: - PIP · QUALITY -# ============================================================================ +# MARK: - pip quality checks ##@ PIP · Quality lint: ## Run flake8 using the active Python environment $(PYTHON) -m flake8 $(PY_ALL) @@ -408,9 +388,7 @@ pip-bootstrap: ## Restore pip build tools after `nuke` (requires network access @echo "Python), this interpreter refuses ambient installs by design — use" @echo "'make uv-bootstrap' instead (offline-capable via uv's cache)." -# ============================================================================ -# MARK: - PIP · TEST -# ============================================================================ +# MARK: - pip tests ##@ PIP · Test test: ## Run tests using the current Python environment @@ -430,7 +408,7 @@ check-pip: ## Check the ambient interpreter can create the clean-room venv # Install pinned requirements before the package. The package is installed normally, # rather than editable, to validate the distribution workflow. -cleanRoomBootstrap: cleanRoomCleanup check-pip ## Bootstrap the clean-room venv + deps (runs NO tests) +cleanRoomBootstrap: cleanRoomCleanup check-pip ## Create the clean-room environment and install dependencies $(PYTHON) -m venv $(VENV) . $(VENV)/bin/activate && \ which python3 && \ @@ -446,9 +424,7 @@ cleanRoomPytest: ## Run pytest inside the clean-room venv testInEnv: clean cleanRoomBootstrap cleanRoomPytest cleanRoomCleanup ## Full clean-room test @echo ">> testInEnv completed" -# ============================================================================ -# MARK: - PIP · BUILD & RELEASE -# ============================================================================ +# MARK: - Build and release ##@ PIP · Build & Release # Run build and publishing tools through uv so no global installation is required. build: check-uv clean-build ## Build sdist + wheel (uv run --with build python -m build) @@ -476,9 +452,7 @@ release: validateBuild ## Refuse local upload; print the CI-driven release proc @echo "For a local pre-flight, use: make release-test (TestPyPI)." @exit 1 -# ============================================================================ -# MARK: - PIP · FLUSH / LIST -# ============================================================================ +# MARK: - pip environment cleanup ##@ PIP · Flush / List # Remove packages from the active Python environment. Prefer `uv-flush-envs` when # using a virtual environment. Core packaging tools are preserved for recovery. @@ -512,9 +486,7 @@ list: ## List pip packages in available environments echo; \ done -# ============================================================================ -# MARK: - Codegen -# ============================================================================ +# MARK: - Code generation # Base directory for generated Python types. Keep this aligned with codegen.sh. _PYTHON_TYPES_BASE := src/foundationTypes From 951ea644410f06cb0668ed41ad395de36bc7c0e1 Mon Sep 17 00:00:00 2001 From: kopecn Date: Thu, 24 Sep 2026 13:27:55 -0700 Subject: [PATCH 10/12] cleanups... --- .claude/archive/math-tools-port.md | 30 ++ .../archive/math-tools-port/00-overview.md | 195 ------------- .../01-template-rename-and-refresh.md | 123 -------- .../math-tools-port/02-errors-and-layering.md | 74 ----- .../03-governance-and-readme.md | 106 ------- .../04-precision-time-interval.md | 109 ------- .../math-tools-port/05-precision-timestamp.md | 84 ------ .../archive/math-tools-port/06-position.md | 89 ------ .../07-quaternion-additions.md | 84 ------ .../math-tools-port/08-spatial-pose.md | 98 ------- .../09-univariate-polynomial.md | 106 ------- .../math-tools-port/10-roots-kernel.md | 128 -------- .../math-tools-port/11-waveform1d-core.md | 130 --------- .../12-waveform1d-operators.md | 97 ------- .../13-waveform1d-generators.md | 105 ------- .../14-dsp-support-and-protocol.md | 128 -------- .../math-tools-port/15-waveform-position.md | 96 ------ .../math-tools-port/16-waveform-quaternion.md | 94 ------ .../17-waveform-spatial-pose.md | 107 ------- .../archive/math-tools-port/18-dsp-calc.md | 138 --------- .../math-tools-port/19-dsp-correlation.md | 108 ------- .../math-tools-port/20-dsp-envelope.md | 106 ------- .../math-tools-port/21-dsp-spectral.md | 99 ------- .../math-tools-port/22-dsp-filtering.md | 127 -------- .../archive/math-tools-port/23-dsp-peaks.md | 91 ------ .../archive/math-tools-port/24-dsp-phase.md | 94 ------ .../math-tools-port/25-dsp-resampling.md | 107 ------- .../math-tools-port/26-dsp-time-alignment.md | 147 ---------- .../math-tools-port/27-dsp-triggers.md | 117 -------- .../math-tools-port/28-dsp-windowing.md | 93 ------ .../math-tools-port/29-dsp-zero-crossings.md | 88 ------ .../archive/math-tools-port/30-dsp-compose.md | 95 ------ .../31-otg-enums-and-errors.md | 79 ----- .../math-tools-port/32-otg-input-parameter.md | 91 ------ .../archive/math-tools-port/33-otg-profile.md | 136 --------- .../34-otg-block-brake-bound.md | 132 --------- .../35-otg-trajectory-and-output.md | 119 -------- .../math-tools-port/36-otg-velocity-steps.md | 118 -------- .../37-otg-position-first-second-steps.md | 108 ------- .../38-otg-position-third-step1.md | 118 -------- .../39-otg-position-third-step2.md | 103 ------- .../40-otg-calculator-target.md | 133 --------- .../archive/math-tools-port/41-otg-driver.md | 136 --------- .../math-tools-port/42-otg-oracle-suites.md | 171 ----------- .../math-tools-port/43-public-surface.md | 74 ----- .../44-otg-step2-udud-discriminant.md | 108 ------- .../45-otg-trivial-profile-length.md | 109 ------- .../46-otg-step1-sqrt-guard.md | 104 ------- ...ition-tolerance-and-timestamp-accessors.md | 107 ------- .../48-roots-degenerate-cases.md | 130 --------- .../49-waveform-spatial-pose-indexing.md | 112 ------- .../50-aggregate-container-api-idioms.md | 134 --------- ...51-waveform-generator-phase-conventions.md | 121 -------- .../52-subpackage-public-surface.md | 125 -------- .../53-window-convention-reconciliation.md | 134 --------- .../54-test-closure-tracks-bc.md | 185 ------------ .../math-tools-port/55-test-closure-dsp.md | 229 --------------- .../56-test-closure-otg-oracles.md | 274 ------------------ .../57-docs-and-convention-sweep.md | 266 ----------------- .../2026-08-26-spherical-spatial-packaging.md | 52 +--- .claude/specs/mathToolsArchitecture.md | 38 +-- .claude/specs/otg.md | 62 +--- .claude/specs/polynomials.md | 40 +-- .claude/specs/precisionTimeMath.md | 6 +- .claude/specs/spatialMath.md | 57 +--- .claude/specs/sphericalGeometry.md | 16 +- .claude/specs/templateConformance.md | 30 +- .claude/specs/waveformCore.md | 11 +- .claude/specs/waveformDsp.md | 168 +---------- 69 files changed, 105 insertions(+), 7424 deletions(-) create mode 100644 .claude/archive/math-tools-port.md delete mode 100644 .claude/archive/math-tools-port/00-overview.md delete mode 100644 .claude/archive/math-tools-port/01-template-rename-and-refresh.md delete mode 100644 .claude/archive/math-tools-port/02-errors-and-layering.md delete mode 100644 .claude/archive/math-tools-port/03-governance-and-readme.md delete mode 100644 .claude/archive/math-tools-port/04-precision-time-interval.md delete mode 100644 .claude/archive/math-tools-port/05-precision-timestamp.md delete mode 100644 .claude/archive/math-tools-port/06-position.md delete mode 100644 .claude/archive/math-tools-port/07-quaternion-additions.md delete mode 100644 .claude/archive/math-tools-port/08-spatial-pose.md delete mode 100644 .claude/archive/math-tools-port/09-univariate-polynomial.md delete mode 100644 .claude/archive/math-tools-port/10-roots-kernel.md delete mode 100644 .claude/archive/math-tools-port/11-waveform1d-core.md delete mode 100644 .claude/archive/math-tools-port/12-waveform1d-operators.md delete mode 100644 .claude/archive/math-tools-port/13-waveform1d-generators.md delete mode 100644 .claude/archive/math-tools-port/14-dsp-support-and-protocol.md delete mode 100644 .claude/archive/math-tools-port/15-waveform-position.md delete mode 100644 .claude/archive/math-tools-port/16-waveform-quaternion.md delete mode 100644 .claude/archive/math-tools-port/17-waveform-spatial-pose.md delete mode 100644 .claude/archive/math-tools-port/18-dsp-calc.md delete mode 100644 .claude/archive/math-tools-port/19-dsp-correlation.md delete mode 100644 .claude/archive/math-tools-port/20-dsp-envelope.md delete mode 100644 .claude/archive/math-tools-port/21-dsp-spectral.md delete mode 100644 .claude/archive/math-tools-port/22-dsp-filtering.md delete mode 100644 .claude/archive/math-tools-port/23-dsp-peaks.md delete mode 100644 .claude/archive/math-tools-port/24-dsp-phase.md delete mode 100644 .claude/archive/math-tools-port/25-dsp-resampling.md delete mode 100644 .claude/archive/math-tools-port/26-dsp-time-alignment.md delete mode 100644 .claude/archive/math-tools-port/27-dsp-triggers.md delete mode 100644 .claude/archive/math-tools-port/28-dsp-windowing.md delete mode 100644 .claude/archive/math-tools-port/29-dsp-zero-crossings.md delete mode 100644 .claude/archive/math-tools-port/30-dsp-compose.md delete mode 100644 .claude/archive/math-tools-port/31-otg-enums-and-errors.md delete mode 100644 .claude/archive/math-tools-port/32-otg-input-parameter.md delete mode 100644 .claude/archive/math-tools-port/33-otg-profile.md delete mode 100644 .claude/archive/math-tools-port/34-otg-block-brake-bound.md delete mode 100644 .claude/archive/math-tools-port/35-otg-trajectory-and-output.md delete mode 100644 .claude/archive/math-tools-port/36-otg-velocity-steps.md delete mode 100644 .claude/archive/math-tools-port/37-otg-position-first-second-steps.md delete mode 100644 .claude/archive/math-tools-port/38-otg-position-third-step1.md delete mode 100644 .claude/archive/math-tools-port/39-otg-position-third-step2.md delete mode 100644 .claude/archive/math-tools-port/40-otg-calculator-target.md delete mode 100644 .claude/archive/math-tools-port/41-otg-driver.md delete mode 100644 .claude/archive/math-tools-port/42-otg-oracle-suites.md delete mode 100644 .claude/archive/math-tools-port/43-public-surface.md delete mode 100644 .claude/archive/math-tools-port/44-otg-step2-udud-discriminant.md delete mode 100644 .claude/archive/math-tools-port/45-otg-trivial-profile-length.md delete mode 100644 .claude/archive/math-tools-port/46-otg-step1-sqrt-guard.md delete mode 100644 .claude/archive/math-tools-port/47-position-tolerance-and-timestamp-accessors.md delete mode 100644 .claude/archive/math-tools-port/48-roots-degenerate-cases.md delete mode 100644 .claude/archive/math-tools-port/49-waveform-spatial-pose-indexing.md delete mode 100644 .claude/archive/math-tools-port/50-aggregate-container-api-idioms.md delete mode 100644 .claude/archive/math-tools-port/51-waveform-generator-phase-conventions.md delete mode 100644 .claude/archive/math-tools-port/52-subpackage-public-surface.md delete mode 100644 .claude/archive/math-tools-port/53-window-convention-reconciliation.md delete mode 100644 .claude/archive/math-tools-port/54-test-closure-tracks-bc.md delete mode 100644 .claude/archive/math-tools-port/55-test-closure-dsp.md delete mode 100644 .claude/archive/math-tools-port/56-test-closure-otg-oracles.md delete mode 100644 .claude/archive/math-tools-port/57-docs-and-convention-sweep.md diff --git a/.claude/archive/math-tools-port.md b/.claude/archive/math-tools-port.md new file mode 100644 index 0000000..19e135c --- /dev/null +++ b/.claude/archive/math-tools-port.md @@ -0,0 +1,30 @@ +--- +version: 1.0 +type: retrospective +name: math-tools-port +purpose: Concise record of the completed py-MathTools template migration and Swift math capability port +scope: project +status: complete +last_updated: 2026-09-24 +semver: 0.0.1 +author: Nicholas Bergantz +--- + +# Math Tools Port Retrospective + +## Goal + +Bring py-MathTools onto the py-foundationTools template conventions and implement the Swift `FoundationMathTypes` capability set as the Tier-3 Python math layer. + +## Outcome + +The 57 implementation and corrective-action chunks completed the precision-time, spatial, waveform, DSP, polynomial, and online trajectory generation surfaces. The final recorded gate passed Ruff, strict mypy, and 1,550 tests. The original chunk-by-chunk plans and resolution notes remain available in Git history, principally commits `24582be` and `6002a9a`. + +## Preserved Decisions + +- `Position` and `SpatialPose` arithmetic may return base types for subclasses. No in-repository subclasses existed; revisit only when subclass preservation is required. +- `SpatialPose.inverse` and `UnivariatePolynomial.derivative` remain methods. Changing them to properties would be a breaking cosmetic change. +- Behavioral test coverage for spherical geometry and `math_plot_helpers` was deferred from the original port. Current spherical defects and required outcomes are tracked separately in the active findings and specifications. +- `WaveformPaddingStrategy`, `WaveformFilterCoefficients`, `WaveformFrequencyRange`, and `WaveformSpectrogramScaling` remain public support types without current DSP consumers. Whether to remove them or add consuming APIs requires a separate contract decision. +- `SpectralMixin.spectrogram` retains the legacy SciPy implementation despite the DSP specification naming `ShortTimeFFT`; migration would be a behavioral change. +- The uncalled `_time_none_smooth` online trajectory generation path remains because the Swift source also retains it. diff --git a/.claude/archive/math-tools-port/00-overview.md b/.claude/archive/math-tools-port/00-overview.md deleted file mode 100644 index e820701..0000000 --- a/.claude/archive/math-tools-port/00-overview.md +++ /dev/null @@ -1,195 +0,0 @@ ---- -plan: math-tools-port -status: complete -last_updated: 2026-07-23 -semver: 0.1.2 -author: Nicholas Bergantz ---- - -# Action Plan — Template Migration + Swift Math Port - -**Goal:** bring py-MathTools onto the py-foundationTools template conventions -and port the Swift `FoundationMathTypes` capability set (spatial SE(3) types, -precision time, waveforms + DSP, polynomials/roots, OTG trajectory -generation) as the Tier-3 math layer, per the accepted specs in -[`../specs/`](../specs/mathToolsArchitecture.md). - -**Usability north star:** a robotics/DSP engineer in a notebook can build a -waveform or pose, do the obvious math, and reach scipy-grade analysis with -minimal ceremony — well-typed numpy-adjacent Python, not translated Swift. - -## Conventions every chunk inherits (do not restate per chunk) - -1. **Spec is authoritative.** Each chunk lists its governing spec section(s). - If implementation forces a contract change, update the spec in the same - chunk and bump its `semver`. -2. **TDD:** write the failing tests first, implement, then run the gate. -3. **Gate:** `make uv-fullCheck` (ruff lint + mypy strict + pytest) must pass - at the end of every chunk. Test layout mirrors the package - (`tests//test_.py`), `unittest.TestCase` style, `test_*` - methods. -4. **Stay in scope:** touch ONLY the files the chunk lists. Adjacent - problems get reported in the chunk's completion notes, not fixed. -5. **Frontmatter:** every chunk carries `status: pending` → set - `in_progress` while working, then `complete` when done (not `done` - — `complete` is the convention actually used across every chunk file - in this repo); bump `last_updated`. -6. **Swift reference roots** (read-only, for faithful-port chunks): - - `SWIFT_MATH` = `/Users/nbergantz/__Workspaces__/spmWorkspaces/spmMathTools/spm/Sources/spmMathTools/FoundationMathTypes` - - `SWIFT_TYPES` = `/Users/nbergantz/__Workspaces__/spmWorkspaces/spmFoundationTools/spm/Sources/FoundationTypes` - - `SWIFT_TESTS` = `/Users/nbergantz/__Workspaces__/spmWorkspaces/spmMathTools/spm/Tests/spmMathToolsTests` -7. **Shared API idioms** (umbrella spec "API idioms"): `normalized()` method - / `normalize()` in-place; `isclose(rtol, atol)`; `__array__`; - `__hash__ = None` on mutable numpy-backed classes; snake_case throughout. -8. Do not commit; the human reviews and commits per track. - -## Dependency graph - -``` -Track A (template) 01 ──► 02 ──► 03 - │ - ┌────────────────┴───────────────────────────────┐ -Track B │ 04 ─► 05 06 ─► 07 ─► 08 09 10 │ (02 before all B) - │ └──────┬──────────┘└───┬────┘ │ │ -Track C │ ▼ │ │ │ - │ 11 ─► {12, 13, 14} │ │ │ - │ │ │ ▼ │ │ - │ │ │ 15(◄06) 16(◄07) ─► 17(◄08,15,16)│ -Track D │ │ ▼ │ │ - │ │ 18..29 (one per mixin; 26 also ◄19) │ │ - │ │ └────────► 30 (compose) │ │ -Track E │ └──────────────────────────────────────────┘ │ - │ 31(◄10) ─► 32 │ - │ 31 ─► 33 ─► 34 ─► {35, 36, 37, 38, 39} │ - │ {32,35..39} ─► 40 ─► 41 ─► 42 │ - └────────────────────────────────────────────────┘ -``` - -Tracks B/C/D/E parallelize after 01–02; within a track, run in numeric -order unless the graph says otherwise. Chunk 43 (public surface) runs last, -after 09, 17, 30, and 42. - -## Chunk index - -| # | Chunk | Track | Depends on | Spec | -|---|---|---|---|---| -| 01 | [template-rename-and-refresh](01-template-rename-and-refresh.md) | A | — | templateConformance | -| 02 | [errors-and-layering](02-errors-and-layering.md) | A | 01 | umbrella, templateConformance §5 | -| 03 | [governance-and-readme](03-governance-and-readme.md) | A | 02 | templateConformance §3–4 | -| 04 | [precision-time-interval](04-precision-time-interval.md) | B | 02 | precisionTimeMath | -| 05 | [precision-timestamp](05-precision-timestamp.md) | B | 04 | precisionTimeMath | -| 06 | [position](06-position.md) | B | 02 | spatialMath | -| 07 | [quaternion-additions](07-quaternion-additions.md) | B | 06 | spatialMath | -| 08 | [spatial-pose](08-spatial-pose.md) | B | 07 | spatialMath | -| 09 | [univariate-polynomial](09-univariate-polynomial.md) | B | 02 | polynomials | -| 10 | [roots-kernel](10-roots-kernel.md) | B | 02 | polynomials | -| 11 | [waveform1d-core](11-waveform1d-core.md) | C | 05 | waveformCore | -| 12 | [waveform1d-operators](12-waveform1d-operators.md) | C | 11 | waveformCore | -| 13 | [waveform1d-generators](13-waveform1d-generators.md) | C | 11 | waveformCore | -| 14 | [dsp-support-and-protocol](14-dsp-support-and-protocol.md) | C | 11 | waveformDsp | -| 15 | [waveform-position](15-waveform-position.md) | C | 11, 06 | waveformCore | -| 16 | [waveform-quaternion](16-waveform-quaternion.md) | C | 11, 07 | waveformCore | -| 17 | [waveform-spatial-pose](17-waveform-spatial-pose.md) | C | 08, 15, 16 | waveformCore | -| 18 | [dsp-calc](18-dsp-calc.md) | D | 14 | waveformDsp | -| 19 | [dsp-correlation](19-dsp-correlation.md) | D | 14 | waveformDsp | -| 20 | [dsp-envelope](20-dsp-envelope.md) | D | 14 | waveformDsp | -| 21 | [dsp-spectral](21-dsp-spectral.md) | D | 14 | waveformDsp | -| 22 | [dsp-filtering](22-dsp-filtering.md) | D | 14 | waveformDsp | -| 23 | [dsp-peaks](23-dsp-peaks.md) | D | 14 | waveformDsp | -| 24 | [dsp-phase](24-dsp-phase.md) | D | 14 | waveformDsp | -| 25 | [dsp-resampling](25-dsp-resampling.md) | D | 14 | waveformDsp | -| 26 | [dsp-time-alignment](26-dsp-time-alignment.md) | D | 14, 19 | waveformDsp | -| 27 | [dsp-triggers](27-dsp-triggers.md) | D | 14 | waveformDsp | -| 28 | [dsp-windowing](28-dsp-windowing.md) | D | 14 | waveformDsp | -| 29 | [dsp-zero-crossings](29-dsp-zero-crossings.md) | D | 14 | waveformDsp | -| 30 | [dsp-compose](30-dsp-compose.md) | D | 18–29 | waveformDsp | -| 31 | [otg-enums-and-errors](31-otg-enums-and-errors.md) | E | 10 | otg | -| 32 | [otg-input-parameter](32-otg-input-parameter.md) | E | 31 | otg | -| 33 | [otg-profile](33-otg-profile.md) | E | 31 | otg | -| 34 | [otg-block-brake-bound](34-otg-block-brake-bound.md) | E | 33 | otg | -| 35 | [otg-trajectory-and-output](35-otg-trajectory-and-output.md) | E | 34 | otg | -| 36 | [otg-velocity-steps](36-otg-velocity-steps.md) | E | 34 | otg | -| 37 | [otg-position-first-second-steps](37-otg-position-first-second-steps.md) | E | 34 | otg | -| 38 | [otg-position-third-step1](38-otg-position-third-step1.md) | E | 34 | otg | -| 39 | [otg-position-third-step2](39-otg-position-third-step2.md) | E | 34 | otg | -| 40 | [otg-calculator-target](40-otg-calculator-target.md) | E | 32, 35–39 | otg | -| 41 | [otg-driver](41-otg-driver.md) | E | 40 | otg | -| 42 | [otg-oracle-suites](42-otg-oracle-suites.md) | E | 41 | otg | -| 43 | [public-surface](43-public-surface.md) | A | 30, 42, 09, 17 | umbrella | - ---- - -# Corrective actions — 2026-07-22 post-audit - -Chunks 01–43 were audited against their governing specs by five parallel -skeptical auditors (one per track), with the top findings independently -reproduced at runtime and source level before being recorded here. - -**Verdict: substantially complete.** `make uv-fullCheck` is green (exit 0) and -the port is faithful in the large — the 31-case OTG numeric truth table was -re-derived from the Swift source with 0 mismatches, no test in the repo is -skipped or xfail'd, and no unsolved algorithm case, stub, or swallowed `None` -was found in the solvers. The confirmed gaps are specific and are chunked below. - -Chunks 44–57 form **Track F**. They are corrective, not new capability. - -## Confirmed defect summary - -| Class | Count | Where | -|---|---|---| -| (c) drift / correctness | 8 | OTG solvers, spatial/time accessors, waveform indexing | -| (a) not implemented | 6 | aggregate API idioms, subpackage exports, roots degeneracy | -| (b) implemented, untested | ~45 | concentrated in DSP mixins and OTG oracles | -| (e) docs/convention drift | 14 | dead workflow refs, stale tolerances, boilerplate | -| (d) out-of-scope violations | **0** | — none found in any track | - -The single most consequential finding is a **transcription defect** at -`position_third_order_step2.py:1731,1752,1763` (`j·tf⁴` where Swift has -`j·tf³`), confirmed by direct comparison against -`PositionThirdOrderStep2.swift:1279,1286,1289`. It changes profile-branch -selection in ~0.57% of prescribed-duration Step2 solves and is invisible to the -current gate — because the entire 1,784-case OTG oracle corpus is 1-DOF, which -takes a fast path that **never invokes Step2 at all** (measured: 0 invocations). - -## Dependency graph - -``` -Track F (corrective) - - OTG code fixes 44 45 46 - └────┴────┴──────► 56 (OTG oracle closure) - - Track B/C code fixes 47 48 49 ─► 50 51 - └────┴────┴─► 54 (B/C test closure) - - Surface / conventions 52 53 ─────────────► 55 (DSP test closure) - - Everything above ───────────────────────────────► 57 (docs sweep, last) -``` - -44–49, 51, 52, 53 are mutually independent and may run in parallel. -50 waits on 49. The three closure chunks wait on their code fixes. 57 runs last -so it reconciles docs against the post-fix repo. - -## Corrective chunk index - -| # | Chunk | Kind | Depends on | Origin | -|---|---|---|---|---| -| 44 | [otg-step2-udud-discriminant](44-otg-step2-udud-discriminant.md) | fix | — | E-1 | -| 45 | [otg-trivial-profile-length](45-otg-trivial-profile-length.md) | fix | — | E-2 | -| 46 | [otg-step1-sqrt-guard](46-otg-step1-sqrt-guard.md) | fix | — | E-3 | -| 47 | [position-tolerance-and-timestamp-accessors](47-position-tolerance-and-timestamp-accessors.md) | fix | — | B-1, B-2 | -| 48 | [roots-degenerate-cases](48-roots-degenerate-cases.md) | fix | — | B-7, B-14 | -| 49 | [waveform-spatial-pose-indexing](49-waveform-spatial-pose-indexing.md) | fix | — | C-4, C-5 | -| 50 | [aggregate-container-api-idioms](50-aggregate-container-api-idioms.md) | fix | 49 | C-1, C-2, C-3, C-7 | -| 51 | [waveform-generator-phase-conventions](51-waveform-generator-phase-conventions.md) | fix | — | C-15 | -| 52 | [subpackage-public-surface](52-subpackage-public-surface.md) | fix | — | A-1, A-2 | -| 53 | [window-convention-reconciliation](53-window-convention-reconciliation.md) | fix | — | D-windowing | -| 54 | [test-closure-tracks-bc](54-test-closure-tracks-bc.md) | tests | 47–51 | 11 (b) findings | -| 55 | [test-closure-dsp](55-test-closure-dsp.md) | tests | 53 | 23 (b) findings | -| 56 | [test-closure-otg-oracles](56-test-closure-otg-oracles.md) | tests | 44, 45, 46 | 8 (b) findings | -| 57 | [docs-and-convention-sweep](57-docs-and-convention-sweep.md) | docs | 44–56 | 14 (e) findings | - -Findings deliberately **not** actioned (B-4, B-5, B-6, A-7, D-cross-cutting, -D-spectral-(c), E-10) are recorded with rationale in chunk 57's -"Recorded — no action" section, so the decisions are not relitigated. diff --git a/.claude/archive/math-tools-port/01-template-rename-and-refresh.md b/.claude/archive/math-tools-port/01-template-rename-and-refresh.md deleted file mode 100644 index 2d09849..0000000 --- a/.claude/archive/math-tools-port/01-template-rename-and-refresh.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -chunk: 01-template-rename-and-refresh -track: A -status: complete -depends_on: [] -spec: ../specs/templateConformance.md §Gap 1, §Gap 2.4; ../specs/spatialMath.md §Modules (ABC re-parent) -last_updated: 2026-07-11 -semver: 0.0.1 -author: Nicholas Bergantz ---- - -# 01 — Package rename + environment refresh - -**Deliverable:** the repo builds and gates green under the new snake_case -package names against the pinned foundation branch. Mechanical migration — -no behavior changes beyond the required ABC re-parent. - -## Files - -- `git mv src/pyMathTools src/math_tools`; inside it: - `git mv src/math_tools/spatial/Quaternion.py src/math_tools/spatial/quaternion.py`, - `git mv src/math_tools/spherical/sphericalGenerators.py src/math_tools/spherical/spherical_generators.py`, - `git mv src/math_tools/spherical/sphericalTransforms.py src/math_tools/spherical/spherical_transforms.py` - (`constructors.py`, `hints.py` keep their names). -- `git mv src/pyMathToolsPlotHelpers src/math_plot_helpers`; - `git mv src/math_plot_helpers/plotUnitSpherical.py src/math_plot_helpers/plot_unit_spherical.py`. -- Edit: `src/math_tools/spatial/quaternion.py` (imports/base only, see below), - every `__init__.py` touched by moves, `tests/test_quaternion.py`, - `examples/sphericalPlotting/plotArcs.py`, - `examples/sphericalPlotting/plotQuatUnitCircles.py`, `Makefile` (mypy - package list if it names packages), `.env` if it names packages. -- Add: `py.typed` in `src/math_tools/` and `src/math_plot_helpers/` roots - (keep the existing ones in subpackages). - -## Design constraints - -1. **First action:** `make uv-refresh` so `.venv` matches the - `requirements.txt` branch pin (the stale install has the pre-template ABC - layout; nothing imports correctly until this runs). -2. **ABC re-parent (the one semantic edit):** in `quaternion.py`, replace - `from foundationTypes.mathTypes.quaternionABC import QuaternionABC` with - `from foundation_abc.math.spatialABCs import QuaternionABC`. The old ABC - carried `DataModelHelper`; the new one is ABC-only with a concrete - `to_dict`. In `tests/test_quaternion.py`, update the serialization tests' - base-class assertions (`DataModelHelper` inheritance assertion → the new - ABC) — assertions on `to_dict`/`from_dict` *values* stay untouched. If any - other import from the old layout exists (grep `foundationTypes.mathTypes.` - across `src/`), re-point to `foundation_abc.math.*` / - `foundationTypes.mathTypes.MathTypes` equivalents. -3. All other edits are import-path text substitutions - (`pyMathTools` → `math_tools`, `pyMathToolsPlotHelpers` → - `math_plot_helpers`, moved module filenames). -4. `pyproject.toml`: update `description` only if trivially co-located; the - deps list changes in chunk 03, not here. - -## TDD steps - -1. `make uv-refresh`; run `make uv-test` to record the pre-existing pass/fail - baseline (the ABC import may already be broken — note it). -2. Perform moves + edits. -3. `make uv-fullCheck` green. - -## Acceptance criteria - -- [x] `grep -rn "pyMathTools" src/ tests/ examples/ Makefile .env pyproject.toml` → no hits -- [x] `grep -rn "foundationTypes.mathTypes.quaternionABC" src/ tests/` → no hits -- [x] `git log --follow --oneline src/math_tools/spatial/quaternion.py` shows history (moves were `git mv`) — verified via `git status`/`git diff --staged` showing `renamed: src/pyMathTools/spatial/Quaternion.py -> src/math_tools/spatial/quaternion.py`; `--follow` itself needs a commit to walk, which this chunk intentionally leaves to the supervising process -- [x] All ~90 quaternion tests pass (89 collected/passed); diff to `tests/test_quaternion.py` contains only import lines, base-class assertion lines, and their two adjacent docstring lines (see Resolution notes) -- [x] `make uv-fullCheck` passes - -## Out of scope - -`errors.py`, layering test, README, `.claude/CLAUDE.md`, pyproject dependency -list, any new math code, any `__init__.py` re-export curation beyond fixing -broken imports. - -## Resolution notes - -- `make uv-refresh` pulled the pinned foundation branch fresh; pre-move - `make uv-test` baseline reproduced the expected pre-existing break - (`ModuleNotFoundError: foundationTypes.mathTypes.quaternionABC`), confirming - the venv was stale before this chunk and the gate is meaningful after. -- Moves done via `git mv` as specified. `git status`/`git diff --staged` - correctly report `quaternion.py` as a rename from `Quaternion.py`; git's - similarity heuristic cross-matched some of the (byte-identical, empty) - `__init__.py` files to different-but-equivalent old empty `__init__.py` - paths — cosmetic only, every file's on-disk destination was verified - directly with `find`, and it does not affect `--follow` on the files that - matter (confirmed for `quaternion.py`). -- The old-layout ABC repoint (design constraint 2) turned out to reach beyond - `quaternion.py`: `foundationTypes.mathTypes.unitSphericalArcABC` and - `unitSphericalSmallCircleABC` (imported by `spherical_generators.py`, - `spherical_transforms.py`, `constructors.py`, `plot_unit_spherical.py`) - were also deleted from the pinned foundation branch and now live at - `foundation_abc.math.sphericalABCs`. Re-pointed all of them per the chunk's - explicit "grep `foundationTypes.mathTypes.` across `src/`" instruction — - required for `make uv-fullCheck` (mypy strict) to pass, since mypy scans - all of `src/`, not just the quaternion module. `foundationTypes.mathTypes.MathTypes.*` - imports (the codegen `*Type` classes) were left untouched — that module - still exists unchanged in the new layout. - This is a scope note, not a spec deviation: constraint 2's own text - authorized exactly this action; the "Files" list section above just didn't - enumerate every file it touched. -- `examples/sphericalPlotting/plotArcs.py` and `plotQuatUnitCircles.py` got - only the mechanical package-name substitution, per constraint 3 and Gap 4's - explicit ownership of their pre-existing broken `foundationTypes.mathTypes.UnitSphericalArc` - / `UnitSphericalSmallCircle` imports (not part of this chunk's scope, and - not scanned by `make uv-typecheck` since `PY_EXAMPLES` is unset in `.env`). -- `tests/test_quaternion.py`: `test_inheritance_from_data_model_helper` - asserted `isinstance(q, DataModelHelper)`, which is no longer true (the new - `QuaternionABC` is ABC-only). Repointed the import and assertion to - `QuaternionABC` per constraint 2's directive, and updated that test's and - the class's docstrings by one line each so the docstrings don't contradict - the assertion right below them — the only lines in this diff beyond raw - import-path substitution. No `to_dict`/`from_dict` value assertions were - touched. -- Added `py.typed` at `src/math_tools/` and `src/math_plot_helpers/` package - roots (subpackage ones already existed). -- No `Makefile`/`.env` edits were needed — neither names packages explicitly - (both scope quality targets via path variables, not package names). -- `pyproject.toml` `description` left untouched — no trivially co-located - edit was applicable in this chunk; still boilerplate text, tracked as Gap 4 - (chunk 03). diff --git a/.claude/archive/math-tools-port/02-errors-and-layering.md b/.claude/archive/math-tools-port/02-errors-and-layering.md deleted file mode 100644 index 78fdbfb..0000000 --- a/.claude/archive/math-tools-port/02-errors-and-layering.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -chunk: 02-errors-and-layering -track: A -status: complete -depends_on: [01] -spec: ../specs/mathToolsArchitecture.md §Error semantics; ../specs/templateConformance.md §Gap 5 -last_updated: 2026-07-11 -semver: 0.0.1 -author: Nicholas Bergantz ---- - -# 02 — Exception hierarchy + package layering test - -**Deliverable:** `math_tools/errors.py` and the AST-based layering test. - -## Files - -- Create: `src/math_tools/errors.py` -- Create: `tests/test_package_layering.py` -- Create: `tests/test_errors.py` - -## Design constraints - -1. `errors.py` defines exactly (each with a one-line docstring): - `MathToolsError(Exception)`, `WaveformCompatibilityError(MathToolsError)`, - `TimestampComparisonError(MathToolsError)`, - `PolynomialSolveError(MathToolsError)`. -2. Layering test pattern: copy the approach of py-foundationTools - `tests/test_package_layering.py` (AST-walk every module under `src/`, - collect `import`/`from` roots). Assertions per templateConformance §Gap 5: - `math_tools` imports neither `math_plot_helpers` nor `matplotlib`; - only `math_plot_helpers` imports `matplotlib`. -3. Additionally assert `math_tools.otg` (once it exists) does not import - `numpy` — write the rule now, guarded to skip if the package dir is - absent, so OTG chunks inherit enforcement for free. - -## TDD steps - -1. Write `tests/test_errors.py` (hierarchy, catchability as `MathToolsError`) - and `tests/test_package_layering.py`; watch errors test fail. -2. Implement `errors.py`; layering test must pass against the current tree. -3. Temporarily add `import matplotlib` to a `math_tools` module and confirm - the layering test fails; revert. `make uv-fullCheck` green. - -## Acceptance criteria - -- [x] All four exception classes exist and subclass as specified -- [x] Layering test fails on an injected `import matplotlib` in `math_tools` (verified then reverted) -- [x] `make uv-fullCheck` passes - -## Out of scope - -Any consumer of the exceptions; OTG's `OtgError` (lives in `otg/errors.py`, -chunk 31); README/CLAUDE.md. - -## Resolution notes - -- `errors.py` implements exactly the four classes from the spec, each a - one-line docstring, no added behavior. -- `test_package_layering.py` follows the py-foundationTools AST-scan pattern - (`tests/test_package_layering.py` there) rather than executing imports, so - it can't be defeated by import side effects. Four checks: `math_tools` ↛ - `math_plot_helpers`, `math_tools` ↛ `matplotlib`, only `math_plot_helpers` - → `matplotlib` (scans all of `src/` excluding that package), and the - guarded `math_tools.otg` ↛ `numpy` rule (currently a no-op skip since - `otg/` doesn't exist yet — will activate automatically once chunk 31 lands). -- Verification step 3 (inject `import matplotlib` into `math_tools/errors.py`, - confirm two layering assertions fail, revert) was done live against the - actual gate, not simulated; file diffed back to the original after. -- One ruff fix needed: the injected-violation assertion message exceeded the - 100-char line limit in `test_only_math_plot_helpers_imports_matplotlib`'s - sibling test; wrapped the f-string across two lines. -- No spec changes were required — the chunk's design constraints matched the - umbrella spec's Error semantics section exactly. diff --git a/.claude/archive/math-tools-port/03-governance-and-readme.md b/.claude/archive/math-tools-port/03-governance-and-readme.md deleted file mode 100644 index 804cdab..0000000 --- a/.claude/archive/math-tools-port/03-governance-and-readme.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -chunk: 03-governance-and-readme -track: A -status: complete -depends_on: [02] -spec: ../specs/templateConformance.md §Gap 2, §Gap 3, §Gap 4 -last_updated: 2026-07-11 -semver: 0.0.2 -author: Nicholas Bergantz ---- - -# 03 — Governance docs, README, dependency names - -**Deliverable:** `.claude/CLAUDE.md`, real README, pyproject dependency -names, fixed examples. - -## Files - -- Create: `.claude/CLAUDE.md` -- Edit: `README.md`, `pyproject.toml` (deps + description only), - `examples/sphericalPlotting/plotArcs.py`, - `examples/sphericalPlotting/plotQuatUnitCircles.py` - -## Design constraints - -1. `.claude/CLAUDE.md` follows the shape of py-foundationTools - `.claude/CLAUDE.md` but stays short: repo role (Tier 3 of foundation's - `mathTypeTiers.md`), the two-package layering diagram, gate command, - Makefile-is-source-of-truth note, and a linked index of every spec in - `.claude/specs/`. Reference specs; never duplicate their content. -2. `pyproject.toml` `dependencies` = names only: - `pyFoundationTools`, `numpy`, `scipy`, `numpy-quaternion`, `matplotlib`. - Replace the boilerplate `description`. -3. README per templateConformance Gap 4: describe only what exists at - execution time (quaternion, spherical utilities, template workflows); - sections: title → Features → Installation → Quick Start → Development - Workflows → Requirements. Quick Start snippets must actually run. -4. `plotArcs.py`: fix the dead - `foundationTypes.mathTypes.UnitSphericalArc.UnitSphericalArc` import to - the real generated type in `foundationTypes.mathTypes.MathTypes` - (verify the class name by reading that module). - -## TDD steps - -1. Add a test `tests/test_governance.py`: `.claude/CLAUDE.md` exists and its - text links every `*.md` in `.claude/specs/`; `README.md` contains no - "Boilerplate". Watch it fail. -2. Write the docs; run both example scripts manually - (`uv run python examples/sphericalPlotting/plotArcs.py` with a - non-interactive matplotlib backend) to prove imports resolve. -3. `make uv-fullCheck` green. - -## Acceptance criteria - -- [x] `tests/test_governance.py` passes -- [x] `pyproject.toml` deps are exactly the five names, unpinned -- [x] Both example scripts import-run without error -- [x] `make uv-fullCheck` passes - -## Out of scope - -Makefile/CI edits; requirements pin changes; any `src/` code beyond the -example imports. - -## Resolution notes - -- `tests/test_governance.py` added: asserts `.claude/CLAUDE.md` exists and - its text contains the filename of every `.claude/specs/*.md` file, and - that `README.md` contains no `"Boilerplate"` text. Confirmed it failed - before the docs existed, passed after. -- `.claude/CLAUDE.md` written to the shape of py-foundationTools' - `.claude/CLAUDE.md` (Project Overview → layering → Commands → Specs index - → Tests) but scoped to what exists in this repo today: Tier 3 role, the - two-package layering diagram, the gate command, a Makefile-is-source-of- - truth note, and a linked index of all 7 specs in `.claude/specs/`. -- `README.md` rewritten per Gap 4's section shape (title → Features → - Installation → Quick Start → Development Workflows → Requirements), - describing only what exists at execution time: `Quaternion`, the - spherical arc/small-circle utilities, `errors.py`, and the - `math_plot_helpers` plotting package. Both Quick Start snippets - (quaternion arithmetic/conversion, spherical arc construction + - endpoint) were executed directly to confirm they run as written. -- `pyproject.toml`: `dependencies` set to the five names - (`pyFoundationTools`, `numpy`, `scipy`, `numpy-quaternion`, `matplotlib`) - — the prior list was missing `numpy`/`scipy` and had an unrelated stray - order; `description` replaced with a one-line Tier-3 summary. -- `examples/sphericalPlotting/plotArcs.py`: fixed the dead - `foundationTypes.mathTypes.UnitSphericalArc.UnitSphericalArc` import to - `foundationTypes.mathTypes.MathTypes.UnitSphericalArcType` (verified the - real class name by reading the installed `MathTypes.py`) and updated the - two local usages/type hints accordingly. -- `examples/sphericalPlotting/plotQuatUnitCircles.py`: same dead-import - pattern existed for `UnitSphericalSmallCircle` (not called out by name in - the chunk's design constraint 4, but the file was listed for edit and the - acceptance criterion requires *both* scripts to import-run without - error) — fixed to `foundationTypes.mathTypes.MathTypes.UnitSphericalSmallCircleType`. -- Both example scripts verified to run end-to-end with - `MPLBACKEND=Agg PYTHONPATH=src .venv/bin/python examples/sphericalPlotting/