Skip to content

Commit 771952c

Browse files
Merge pull request #16 from BayyinahEnterprise/v0.9.3.1-cli-numpy-divergence-display
v0.9.3.1: CLI hotfix — NumpyDivergenceDiagnostic body now prints
2 parents f5a825c + 13b25c1 commit 771952c

7 files changed

Lines changed: 214 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,87 @@ introduced this convention.
1919

2020
---
2121

22+
## [0.9.3.1] - 2026-05-04
23+
24+
CLI hotfix. Closes round-34 HIGH-1: the CLI's
25+
``_check_onnx_file`` printer hardcoded an isinstance tuple
26+
that filtered the new ``NumpyDivergenceDiagnostic`` family
27+
out, so users running ``furqan-lint check`` on a divergence-
28+
firing model saw:
29+
30+
MARAD <path>
31+
1 violation(s):
32+
33+
with no body. The substrate-side ``check_numpy_divergence``
34+
correctly emitted the diagnostic; the CLI surface dropped it.
35+
The bug rendered the v0.9.3 headline feature unusable from
36+
the CLI; the fix is one line plus an import.
37+
38+
### Fixed
39+
40+
- **Round-34 HIGH-1 (HIGH).** ``cli._check_onnx_file``
41+
isinstance tuple extended from three diagnostic families
42+
(AllPathsEmitDiagnostic, OpsetComplianceDiagnostic,
43+
ShapeCoverageDiagnostic) to four. The fourth family,
44+
``NumpyDivergenceDiagnostic`` (added in v0.9.3), now prints
45+
its diagnosis body alongside the other three.
46+
- A regression test
47+
(``test_cli_numpy_divergence_body_prints``) runs the CLI
48+
end-to-end via subprocess on a divergence-firing fixture,
49+
asserts the ``numpy_divergence`` tag and diagnosis prose
50+
appear in stdout, and asserts the negative case (stdout
51+
does not match the bug shape of an empty body after the
52+
violations count). The regression pin is the surface-to-
53+
substrate match: a future addition of a fifth diagnostic
54+
family (e.g., ``ScoreValidityDiagnostic`` in v0.9.4) will
55+
not silent-drop because the test catches the same bug
56+
shape.
57+
58+
### Known limitations carried to v0.9.4
59+
60+
- **ONNX printer's ``minimal_fix`` field is silently dropped**
61+
(consistent across all four diagnostic families). The
62+
Python, Rust, and Go marad printers in the same
63+
``cli.py`` follow a different pattern that prints both
64+
``diagnosis`` and ``minimal_fix``; the ONNX path prints
65+
``diagnosis`` only. Bundling this fix to v0.9.4 Part 5b
66+
alongside the structural CLI-integration gate per §3.5
67+
of the v0.9.3.1 prompt: fixing it in one family only
68+
would create a new inconsistency, and fixing it for all
69+
four families is structurally adjacent to v0.9.4's gate
70+
work that prevents the v0.9.3.1 bug class from recurring.
71+
- **CHANGELOG-math gate skips in lean CI envs** (round-34
72+
HIGH-2). The canonical CHANGELOG test count includes the
73+
11 onnxruntime-gated tests in
74+
``test_onnx_numpy_divergence.py`` that use
75+
``pytest.importorskip("onnxruntime")``. CI's test jobs
76+
install ``[dev,onnx]`` (no onnxruntime), producing an
77+
empirical count 11 below the canonical number. Gate now
78+
skips with a descriptive message when ``onnxruntime`` is
79+
not importable; runs at full strength on developer
80+
machines and the bundle-author sandbox. v0.9.4 Part 5b
81+
closes this by adding an ``[onnx-runtime]`` job to the
82+
CI matrix so the gate runs in CI too. Until then,
83+
arithmetic drift in the CHANGELOG ``### Tests`` line is
84+
caught locally but not in CI for releases that touch
85+
onnxruntime-gated test counts.
86+
87+
### Tests
88+
89+
Test count: 412 (v0.9.3 ship state) -> 413 (v0.9.3.1).
90+
Net delta: +1.
91+
92+
Breakdown:
93+
94+
- Regression test: +1
95+
(``test_cli_numpy_divergence_body_prints``)
96+
97+
### Round-34 closure ledger
98+
99+
| Finding | Source | Severity | Closure |
100+
| --- | --- | --- | --- |
101+
| HIGH-1 | round-34 audit (post-v0.9.3-ship) | HIGH | Closed in v0.9.3.1. Decision 1 extends the isinstance tuple; Decision 2 adds the source-module import; Decision 3 lands the regression test. Decision 4 defers the structural CLI-integration gate to v0.9.4 (the bug class, not just the bug instance). The fix is one line; the regression test pins the surface to the substrate. |
102+
22103
## [0.9.3] - 2026-05-04
23104

24105
numpy-vs-ONNX divergence detection. Closes Gap 4 (HIGH

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "furqan-lint"
7-
version = "0.9.3"
7+
version = "0.9.3.1"
88
description = "Structural-honesty checks for Python, powered by Furqan"
99
readme = "README.md"
1010
requires-python = ">=3.10"

src/furqan_lint/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""furqan-lint: structural-honesty checks for Python."""
22

3-
__version__ = "0.9.3"
3+
__version__ = "0.9.3.1"
44

55
# Explicit public surface declaration. The implicit surface (anything
66
# not starting with an underscore at module level) is fragile: any

src/furqan_lint/cli.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,9 @@ def _check_onnx_file(path: Path) -> int:
360360
OnnxParseError,
361361
parse_model,
362362
)
363+
from furqan_lint.onnx_adapter.numpy_divergence import (
364+
NumpyDivergenceDiagnostic,
365+
)
363366
from furqan_lint.onnx_adapter.runner import (
364367
AllPathsEmitDiagnostic,
365368
OpsetComplianceDiagnostic,
@@ -405,7 +408,11 @@ def _check_onnx_file(path: Path) -> int:
405408
print(f" {len(diagnostics)} violation(s):")
406409
for name, d in diagnostics:
407410
if isinstance(
408-
d, AllPathsEmitDiagnostic | OpsetComplianceDiagnostic | ShapeCoverageDiagnostic
411+
d,
412+
AllPathsEmitDiagnostic
413+
| OpsetComplianceDiagnostic
414+
| ShapeCoverageDiagnostic
415+
| NumpyDivergenceDiagnostic,
409416
):
410417
print(f" [{name}] {d.diagnosis}")
411418
return 1

tests/test_changelog_math_gate.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,29 @@ def test_changelog_math_matches_pytest_collect() -> None:
117117
Skips when the entry contains <TBD> / <DATE> placeholders
118118
(in-flight release commit; the release commit replaces
119119
placeholders with empirical values).
120+
121+
Skips when ``onnxruntime`` is not importable. Round-34
122+
HIGH-2 finding: the canonical CHANGELOG count includes
123+
11 tests in ``test_onnx_numpy_divergence.py`` that use
124+
``pytest.importorskip("onnxruntime")`` and skip-collect
125+
when the runtime is absent. The CI test jobs install
126+
``[dev,onnx]`` (no onnxruntime), producing an empirical
127+
count 11 below the CHANGELOG count. Until the CI matrix
128+
adds an ``[onnx-runtime]`` job (planned for v0.9.4 Part
129+
5b alongside the structural CLI-integration gate), this
130+
test skips in the lean env. Developer machines and the
131+
bundle-author sandbox install ``[onnx-runtime]`` and run
132+
the gate at full strength.
120133
"""
134+
try:
135+
import onnxruntime # noqa: F401
136+
except ImportError:
137+
pytest.skip(
138+
"onnxruntime not importable; canonical CHANGELOG count "
139+
"includes 11 onnxruntime-gated tests that skip-collect "
140+
"in this env. Gate runs at full strength when "
141+
"[onnx-runtime] is installed. See round-34 HIGH-2."
142+
)
121143
parsed = _parse_changelog_math(REPO_ROOT / "CHANGELOG.md")
122144
if parsed is None:
123145
pytest.skip(

tests/test_onnx_numpy_divergence.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,90 @@ def test_numpy_divergence_multi_probe_one_finding_per_diverging(
335335
findings = list(check_numpy_divergence(_load_proto(onnx_path), onnx_path))
336336
assert len(findings) == 1
337337
assert findings[0].probe_index == 1
338+
339+
340+
def test_cli_numpy_divergence_body_prints(tmp_path: Path) -> None:
341+
"""Round-34 HIGH-1 regression test: the CLI's
342+
``_check_onnx_file`` printer must include the diagnosis
343+
body for ``NumpyDivergenceDiagnostic`` findings.
344+
345+
Pre-v0.9.3.1 the printer's isinstance tuple filtered the
346+
fourth diagnostic family out, producing
347+
``MARAD <path>\n 1 violation(s):\n`` with no body.
348+
The substrate-side check_numpy_divergence emits a full
349+
diagnostic; the CLI surface dropped it. v0.9.3.1
350+
extends the isinstance tuple to include
351+
NumpyDivergenceDiagnostic so the body prints.
352+
353+
The test runs the CLI end-to-end via subprocess on a
354+
fixture that triggers a divergence finding, then asserts
355+
the diagnosis prose appears in stdout. Per §3.5 of the
356+
v0.9.3.1 prompt, the test does NOT assert that
357+
``minimal_fix`` prints (that gap is closed by v0.9.4
358+
Part 5b alongside the structural CLI-integration gate).
359+
"""
360+
import re
361+
import subprocess
362+
import sys
363+
364+
onnx_path = tmp_path / "dummy.onnx"
365+
_save_relu_float_model(onnx_path)
366+
367+
# Disagreeing reference: returns 2x the input where ONNX Relu
368+
# would return max(input, 0). Triggers the tolerance-mode
369+
# divergence path.
370+
(tmp_path / "dummy_build.py").write_text(
371+
"import numpy as np\n"
372+
"def numpy_reference(grid):\n"
373+
" return np.array(grid, dtype=np.float32) * 2.0\n",
374+
encoding="utf-8",
375+
)
376+
import json
377+
378+
(tmp_path / "dummy.json").write_text(
379+
json.dumps(
380+
{
381+
"train": [
382+
{
383+
"input": [[-1.0, 2.0, -3.0, 4.0]],
384+
"output": [[0.0, 2.0, 0.0, 4.0]],
385+
}
386+
],
387+
"test": [],
388+
}
389+
),
390+
encoding="utf-8",
391+
)
392+
393+
result = subprocess.run(
394+
[sys.executable, "-m", "furqan_lint.cli", "check", str(onnx_path)],
395+
capture_output=True,
396+
text=True,
397+
check=False,
398+
)
399+
400+
# Exit 1: at least one MARAD fired.
401+
assert result.returncode == 1, (
402+
f"expected exit 1 (MARAD); got {result.returncode}\n"
403+
f"stdout: {result.stdout}\nstderr: {result.stderr}"
404+
)
405+
406+
# Headline line is present.
407+
assert "MARAD" in result.stdout
408+
assert "1 violation(s):" in result.stdout
409+
410+
# The diagnostic family tag and body prose are present.
411+
assert (
412+
"numpy_divergence" in result.stdout
413+
), f"expected '[numpy_divergence]' tag in stdout; got:\n{result.stdout}"
414+
assert (
415+
"disagree" in result.stdout or "reference" in result.stdout
416+
), f"expected diagnosis prose in stdout; got:\n{result.stdout}"
417+
418+
# Negative assertion: the bug shape was an empty body after
419+
# the violations count. Confirm we are NOT in that state.
420+
bug_shape = re.compile(r"1 violation\(s\):\s*\n\s*$")
421+
assert not bug_shape.search(result.stdout), (
422+
f"CLI output still matches the round-34 HIGH-1 bug shape "
423+
f"(empty body after violations count):\n{result.stdout!r}"
424+
)

tests/test_release_sweep_gate.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,21 @@ def test_no_stale_version_anchored_claims_in_user_surfaces() -> None:
194194

195195

196196
def _current_version_tuple() -> tuple[int, int, int]:
197-
"""Parse the current version from pyproject.toml as a tuple."""
197+
"""Parse the current version from pyproject.toml as a tuple.
198+
199+
Handles both 3-component (``X.Y.Z``) and 4-component
200+
(``X.Y.Z.W``) versions; the trailing ``.W`` (hotfix
201+
component) is dropped because the forward-reference check
202+
is anchored at the ``vN.M[.P]`` granularity, not at the
203+
hotfix granularity. v0.9.3.1 hotfix added 4-component
204+
handling.
205+
"""
198206
pyproject = (REPO_ROOT / "pyproject.toml").read_text()
199-
match = re.search(r'^version\s*=\s*"(\d+)\.(\d+)\.(\d+)"', pyproject, re.MULTILINE)
207+
match = re.search(
208+
r'^version\s*=\s*"(\d+)\.(\d+)\.(\d+)(?:\.\d+)?"',
209+
pyproject,
210+
re.MULTILINE,
211+
)
200212
if match is None:
201213
pytest.fail("could not parse version from pyproject.toml")
202214
return (int(match.group(1)), int(match.group(2)), int(match.group(3)))

0 commit comments

Comments
 (0)