Skip to content

Commit 433b297

Browse files
lukaszsamsonclaude
andcommitted
Expose tightening hints in JSON and fix prompt-mode staleness
Two review findings: - :spec_wider_return entries were auto-triaged but only counted in stats, contradicting the comment that says tightening candidates remain discoverable. The JSON output now carries them in a tightening_hints section with full entry detail (not gated). - Prompt mode omitted untranslatable entries and still instructed reviewers to judge spec_wider_return, which no longer appears in its data. It now includes untranslatables and the instructions match the actual verdict set, including the dynamic_args probe. Also drops the Code.fetch_docs/1 exclusion: :erl_anno's nominal types resolve now that Code.Typespec.fetch_types/1 returns them (elixir-lang#15562), and the tool reported the entry as stale -- zero untranslatable specs remain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent cbfabed commit 433b297

2 files changed

Lines changed: 26 additions & 16 deletions

File tree

lib/elixir/scripts/compare_specs_and_signatures.exs

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -766,11 +766,13 @@ defmodule Main do
766766
stale: stale
767767
}
768768

769+
hints = Enum.filter(all, &(&1.verdict == :spec_wider_return))
770+
769771
out =
770772
case opts[:format] do
771773
"report" -> render_report(stats, residue, untranslatable, all, gate)
772-
"json" -> JSON.encode_to_iodata!(render_json(stats, residue, untranslatable))
773-
"prompt" -> render_prompt(stats, residue)
774+
"json" -> JSON.encode_to_iodata!(render_json(stats, residue, untranslatable, hints))
775+
"prompt" -> render_prompt(stats, residue, untranslatable)
774776
end
775777

776778
case opts[:output] do
@@ -825,13 +827,18 @@ defmodule Main do
825827
%{mfa: mfa_string(e), why: e[:why]}
826828
end
827829

828-
defp render_json(stats, residue, untranslatable) do
830+
# tightening_hints carries the auto-triaged :spec_wider_return entries:
831+
# inference proved the return strictly narrower than an exactly-translated
832+
# spec. Not gated (specs are deliberately wide contracts), but listed so
833+
# documentation-tightening candidates remain identifiable.
834+
defp render_json(stats, residue, untranslatable, hints) do
829835
%{
830836
elixir: System.version(),
831837
format_version: 3,
832838
stats: stats,
833839
residue: Enum.map(residue, &entry_json/1),
834-
untranslatable: Enum.map(untranslatable, &untranslatable_json/1)
840+
untranslatable: Enum.map(untranslatable, &untranslatable_json/1),
841+
tightening_hints: Enum.map(hints, &entry_json/1)
835842
}
836843
end
837844

@@ -931,29 +938,33 @@ defmodule Main do
931938
defp percent(_n, 0), do: "n/a"
932939
defp percent(n, total), do: "#{Float.round(n * 100 / total, 1)}%"
933940

934-
defp render_prompt(stats, residue) do
935-
json = JSON.encode_to_iodata!(render_json(stats, residue, []))
941+
defp render_prompt(stats, residue, untranslatable) do
942+
json = JSON.encode_to_iodata!(render_json(stats, residue, untranslatable, []))
936943

937944
[
938945
"""
939946
You are reviewing the RESIDUE of a mechanical comparison between Elixir
940947
@spec declarations and compiler-inferred type signatures. Everything
941948
mechanically decidable has already been decided:
942949
943-
- functions where spec and inference agree, or where inference is merely
944-
wider (expected conservatism), are NOT included;
945-
- "verdict" was computed with semantic subtyping in the checker's own
946-
type lattice, slice-wise per spec clause;
950+
- functions where spec and inference agree, where inference is merely
951+
wider (expected conservatism), or where inference is strictly inside
952+
the spec (specs are deliberately wide public contracts) are NOT
953+
included;
954+
- "verdict" was computed by applying the inferred signature with the
955+
checker's own application rule, slice-wise per spec clause, using
956+
semantic subtyping in the checker's type lattice;
947957
- "translation_exact" flags where the spec had to be approximated
948958
(e.g. non_neg_integer() -> integer()); do not draw conclusions that
949-
depend on precision the translation lost.
959+
depend on precision the translation lost;
960+
- "dynamic_args" is the checker's return prediction for a call with
961+
fully-unknown arguments and its relation to the spec return.
950962
951963
For each entry, judge:
952964
1. contradiction: which side is wrong, and what should change?
953-
2. spec_wider_return: is tightening the spec desirable documentation-wise
954-
(e.g. a function that never returns [] declared as returning keyword())
955-
or is the wider spec intentional API contract?
956-
3. mixed: characterize the difference and whether it is actionable.
965+
2. mixed: characterize the difference and whether it is actionable.
966+
3. untranslatable: which typespec construct is unsupported and whether
967+
a sound over-approximation could be added to the translator.
957968
958969
Be concise; order by severity; reference module.function/arity.
959970

lib/elixir/scripts/compare_specs_exclusions.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ Calendar.ISO.parse_utc_datetime/1 # mixed
3333
Calendar.ISO.parse_utc_datetime/2 # mixed
3434
Calendar.ISO.time_to_iodata/5 # mixed
3535
Code.Typespec.spec_to_quoted/2 # mixed
36-
Code.fetch_docs/1 # untranslatable ({:missing_type, :erl_anno, :location, 0}) -- OTP 28 nominal type; resolves once Code.Typespec returns nominal types
3736
Config.__env__!/0 # mixed
3837
Config.__target__!/0 # mixed
3938
Date.from_erl/2 # mixed

0 commit comments

Comments
 (0)