machinechecked 0.1.1__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {machinechecked-0.1.1 → machinechecked-0.1.2}/PKG-INFO +3 -1
  2. {machinechecked-0.1.1 → machinechecked-0.1.2}/README.md +2 -0
  3. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/machinechecked.egg-info/PKG-INFO +3 -1
  4. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/machinechecked.egg-info/SOURCES.txt +2 -2
  5. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/formalization.py +3 -1
  6. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/simple.py +151 -14
  7. machinechecked-0.1.2/engine/python/prooflens/web/assets/index-DY4pFtGh.js +285 -0
  8. machinechecked-0.1.1/engine/python/prooflens/web/assets/index-B2GDDbsW.css → machinechecked-0.1.2/engine/python/prooflens/web/assets/index-g0SLRgHD.css +1 -1
  9. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/index.html +2 -2
  10. {machinechecked-0.1.1 → machinechecked-0.1.2}/pyproject.toml +1 -1
  11. machinechecked-0.1.1/engine/python/prooflens/web/assets/index-DN7uLe7p.js +0 -285
  12. {machinechecked-0.1.1 → machinechecked-0.1.2}/LICENSE +0 -0
  13. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/machinechecked.egg-info/dependency_links.txt +0 -0
  14. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/machinechecked.egg-info/entry_points.txt +0 -0
  15. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/machinechecked.egg-info/requires.txt +0 -0
  16. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/machinechecked.egg-info/top_level.txt +0 -0
  17. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/__init__.py +0 -0
  18. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/__main__.py +0 -0
  19. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/assess.py +0 -0
  20. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/audit.py +0 -0
  21. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/bundle.py +0 -0
  22. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/cli.py +0 -0
  23. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/config.py +0 -0
  24. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/data/ProofLensProbe.lean +0 -0
  25. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/data/prooflens.sty +0 -0
  26. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/data/schema/evidence.schema.json +0 -0
  27. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/data/schema/inventory.schema.json +0 -0
  28. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/data/schema/manifest-claim.schema.json +0 -0
  29. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/data/schema/manifest-index.schema.json +0 -0
  30. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/evidence.py +0 -0
  31. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/export.py +0 -0
  32. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/manifest.py +0 -0
  33. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/pdfclaims.py +0 -0
  34. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/pipeline.py +0 -0
  35. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/probe.py +0 -0
  36. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/promote.py +0 -0
  37. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/review_queue.py +0 -0
  38. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/reviews.py +0 -0
  39. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/schemas.py +0 -0
  40. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/server.py +0 -0
  41. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/sources.py +0 -0
  42. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/tex.py +0 -0
  43. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/util.py +0 -0
  44. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  45. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  46. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  47. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  48. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  49. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  50. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  51. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  52. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  53. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  54. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  55. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  56. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  57. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  58. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  59. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  60. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  61. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  62. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  63. {machinechecked-0.1.1 → machinechecked-0.1.2}/engine/python/prooflens/web/assets/pdf.worker.min-yatZIOMy.mjs +0 -0
  64. {machinechecked-0.1.1 → machinechecked-0.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: machinechecked
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Read a mathematical paper beside its Lean formalisation, and see claim by claim what Lean establishes.
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://machinechecked.org
@@ -30,6 +30,8 @@ MachineChecked opens a paper, finds its numbered statements and lets you explore
30
30
 
31
31
  **A Lean proof establishes a formal statement—not automatically the claim in the paper.** MachineChecked keeps those two questions separate.
32
32
 
33
+ **MachineChecked runs locally, in your Lean project.** It reads your paper and your compiled Lean declarations, writes an interactive reader, and keeps its results on your computer: generated data in `.machinechecked/`, the links you confirm in `machinechecked.links.json`, and the reader as `MachineChecked.html` if you ask for one. No account, registration or upload is needed. Publishing an audit (on your own GitHub Pages, say, and listing it on machinechecked.org) is a separate step you choose to take.
34
+
33
35
  [Try the demo](https://machinechecked.org/demo/) · [Website](https://machinechecked.org/) · [Installation and usage](https://github.com/cgarryZA/MachineChecked/blob/main/docs/GETTING_STARTED.md)
34
36
 
35
37
  ## Open your paper
@@ -6,6 +6,8 @@ MachineChecked opens a paper, finds its numbered statements and lets you explore
6
6
 
7
7
  **A Lean proof establishes a formal statement—not automatically the claim in the paper.** MachineChecked keeps those two questions separate.
8
8
 
9
+ **MachineChecked runs locally, in your Lean project.** It reads your paper and your compiled Lean declarations, writes an interactive reader, and keeps its results on your computer: generated data in `.machinechecked/`, the links you confirm in `machinechecked.links.json`, and the reader as `MachineChecked.html` if you ask for one. No account, registration or upload is needed. Publishing an audit (on your own GitHub Pages, say, and listing it on machinechecked.org) is a separate step you choose to take.
10
+
9
11
  [Try the demo](https://machinechecked.org/demo/) · [Website](https://machinechecked.org/) · [Installation and usage](https://github.com/cgarryZA/MachineChecked/blob/main/docs/GETTING_STARTED.md)
10
12
 
11
13
  ## Open your paper
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: machinechecked
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Read a mathematical paper beside its Lean formalisation, and see claim by claim what Lean establishes.
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://machinechecked.org
@@ -30,6 +30,8 @@ MachineChecked opens a paper, finds its numbered statements and lets you explore
30
30
 
31
31
  **A Lean proof establishes a formal statement—not automatically the claim in the paper.** MachineChecked keeps those two questions separate.
32
32
 
33
+ **MachineChecked runs locally, in your Lean project.** It reads your paper and your compiled Lean declarations, writes an interactive reader, and keeps its results on your computer: generated data in `.machinechecked/`, the links you confirm in `machinechecked.links.json`, and the reader as `MachineChecked.html` if you ask for one. No account, registration or upload is needed. Publishing an audit (on your own GitHub Pages, say, and listing it on machinechecked.org) is a separate step you choose to take.
34
+
33
35
  [Try the demo](https://machinechecked.org/demo/) · [Website](https://machinechecked.org/) · [Installation and usage](https://github.com/cgarryZA/MachineChecked/blob/main/docs/GETTING_STARTED.md)
34
36
 
35
37
  ## Open your paper
@@ -56,6 +56,6 @@ engine/python/prooflens/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2
56
56
  engine/python/prooflens/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2
57
57
  engine/python/prooflens/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2
58
58
  engine/python/prooflens/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2
59
- engine/python/prooflens/web/assets/index-B2GDDbsW.css
60
- engine/python/prooflens/web/assets/index-DN7uLe7p.js
59
+ engine/python/prooflens/web/assets/index-DY4pFtGh.js
60
+ engine/python/prooflens/web/assets/index-g0SLRgHD.css
61
61
  engine/python/prooflens/web/assets/pdf.worker.min-yatZIOMy.mjs
@@ -62,8 +62,10 @@ def statements(fy: dict) -> list[dict]:
62
62
  if not names or not m:
63
63
  continue
64
64
  rest = src[m.end():]
65
+ module = r.get("module") if isinstance(r.get("module"), str) else None
65
66
  out.append({"source": src, "kind": m.group(1).lower(), "number": m.group(2),
66
- "hint": _tokens(rest), "lean": names, "status": r.get("status")})
67
+ "hint": _tokens(rest), "lean": names, "status": r.get("status"),
68
+ "module": module})
67
69
  return out
68
70
 
69
71
 
@@ -300,6 +300,123 @@ def run_simple_probe(root: Path, modules: list[str], libs: list[str], requests:
300
300
  return recs
301
301
 
302
302
 
303
+ def library_of(module: str, libs: list[str]) -> str:
304
+ """The library a module belongs to: the one whose root is its first component."""
305
+ first = module.split(".")[0]
306
+ return next((l for l in libs if l.split(".")[0] == first), first)
307
+
308
+
309
+ def probe_modules(root: Path, modules: list[str], libs: list[str], requests: list[dict],
310
+ lake: str = "lake", log=print) -> tuple[list[dict], dict[str, str]]:
311
+ """Probe the built modules without assuming they can all be imported into one Lean session.
312
+
313
+ A project may build modules that do not belong together: two libraries declaring the same
314
+ name, for instance a statement left as a challenge and its solution. Lean may load both and
315
+ silently keep one, so each library is probed in its own session, always; a library that
316
+ fails to load is split in halves, down to single modules. Each record is tagged with the
317
+ group it came from, and a name found in several groups is ambiguous until its module is
318
+ given. Returns the records and the modules that could not be loaded, with the reason."""
319
+ recs, failed = [], {}
320
+
321
+ def run(group: list[str], label: str):
322
+ try:
323
+ out = run_simple_probe(root, group, libs, requests, lake)
324
+ except ViewError as e:
325
+ if len(group) == 1:
326
+ failed[group[0]] = str(e)[-600:]
327
+ return
328
+ log(f"lean: {label} could not be loaded in one session; splitting it")
329
+ half = len(group) // 2
330
+ run(group[:half], label + "/a")
331
+ run(group[half:], label + "/b")
332
+ return
333
+ for r in out:
334
+ r["probe_group"] = label
335
+ recs.extend(out)
336
+
337
+ by_lib: dict[str, list[str]] = {}
338
+ for m in sorted(modules):
339
+ by_lib.setdefault(library_of(m, libs), []).append(m)
340
+ for lib, ms in sorted(by_lib.items()):
341
+ run(ms, lib)
342
+ return recs, failed
343
+
344
+
345
+ DECL_KEYWORDS = r"(?:theorem|lemma|def|abbrev|instance|structure|inductive|class|opaque|axiom)"
346
+
347
+
348
+ def declared_names(text: str) -> set[str]:
349
+ """The full names a Lean source declares, as far as its text shows: each declaration's
350
+ identifier inside the `namespace` blocks around it."""
351
+ names, stack = set(), []
352
+ decl = re.compile(rf"^\s*(?:@\[[^\]]*\]\s*)?(?:(?:private|protected|noncomputable|nonrec|"
353
+ rf"partial|unsafe)\s+)*{DECL_KEYWORDS}\s+([\w.'«»]+)")
354
+ for line in text.splitlines():
355
+ m = re.match(r"\s*(namespace|section)\b\s*([\w.'«»]*)", line)
356
+ if m:
357
+ stack.append((m.group(1), m.group(2)))
358
+ continue
359
+ m = re.match(r"\s*end\b\s*([\w.'«»]*)", line)
360
+ if m and stack:
361
+ stack.pop()
362
+ continue
363
+ m = decl.match(line)
364
+ if m:
365
+ ident = m.group(1)
366
+ if ident.startswith("_root_."):
367
+ names.add(ident[len("_root_."):])
368
+ else:
369
+ ns = [n for kind, n in stack if kind == "namespace" and n]
370
+ names.add(".".join(ns + [ident]))
371
+ return names
372
+
373
+
374
+ def declaring_modules(root: Path, modules: list[str], name: str) -> list[str]:
375
+ """Modules among `modules` whose source declares `name`, as far as its text shows. Used only
376
+ to explain a declaration that no probed module holds; it never makes a link read as
377
+ checked."""
378
+ out = []
379
+ for m in modules:
380
+ p = root / (m.replace(".", "/") + ".lean")
381
+ try:
382
+ if p.is_file() and name in declared_names(p.read_text(encoding="utf-8", errors="replace")):
383
+ out.append(m)
384
+ except OSError:
385
+ continue
386
+ return out
387
+
388
+
389
+ def resolve_link(name: str, records: list[dict], module: str | None, root: Path,
390
+ unbuilt: list[str], failed: dict[str, str]) -> tuple[dict | None, str | None]:
391
+ """The probe record a link means, or why there is none.
392
+
393
+ A declaration is identified by its name and its module: when several loaded modules declare
394
+ the name, the module the project gave (formalization.yaml's `module`) decides, and without
395
+ one the link is ambiguous rather than guessed. With no record, the source says whether the
396
+ declaration's module is unbuilt or could not be loaded: only a declaration absent from the
397
+ modules that were inspected reads as not found."""
398
+ found = [r for r in records if "missing" not in r.get("errors", [])]
399
+ mod_of = lambda r: (r.get("decl") or {}).get("module") or r.get("module")
400
+ if module:
401
+ mine = [r for r in found if mod_of(r) == module]
402
+ if mine:
403
+ return mine[0], None
404
+ if module in unbuilt:
405
+ return None, f"linked: Lean not checked (its module {module} is not built)"
406
+ if module in failed:
407
+ return None, f"linked: Lean not checked (its module {module} could not be loaded)"
408
+ elif len({mod_of(r) for r in found}) > 1:
409
+ mods = ", ".join(sorted(str(mod_of(r)) for r in found))
410
+ return None, f"linked: Lean name is ambiguous (declared in {mods})"
411
+ elif found:
412
+ return found[0], None
413
+ for m in declaring_modules(root, unbuilt, name):
414
+ return None, f"linked: Lean not checked (its module {m} is not built)"
415
+ for m in declaring_modules(root, list(failed), name):
416
+ return None, f"linked: Lean not checked (its module {m} could not be loaded)"
417
+ return None, None
418
+
419
+
303
420
  def link_state(rec: dict | None) -> tuple[str, str]:
304
421
  """The status of a confirmed link from its probe record, and its class."""
305
422
  if rec is None or "missing" in rec.get("errors", []):
@@ -438,8 +555,10 @@ def build_view(root: Path, paper_path: Path, out_dir: Path, lake: str = "lake",
438
555
  index, linkrecs, decls = [], {}, {}
439
556
  if fy and "error" not in fy:
440
557
  for c in claims:
441
- c["declared"] = [n for st in fz.links_for(fy, c["kind"], c["number"], paper.title)
442
- for n in st["lean"]]
558
+ rows = fz.links_for(fy, c["kind"], c["number"], paper.title)
559
+ c["declared"] = [n for st in rows for n in st["lean"]]
560
+ c["declared_modules"] = {n: st["module"] for st in rows for n in st["lean"]
561
+ if st.get("module")}
443
562
  links = load_links(root).get("papers", {}).get(paper_key, {})
444
563
  if not libs:
445
564
  notes.append("no Lean library found in the lakefile")
@@ -454,13 +573,16 @@ def build_view(root: Path, paper_path: Path, out_dir: Path, lake: str = "lake",
454
573
  icache = out_dir / "build" / "index-cache.json"
455
574
  idigest = lean_digest(root)
456
575
  cached = read_json(icache) if icache.exists() else {}
576
+ failed = {}
457
577
  if cached.get("digest") == idigest:
458
- index = cached["index"]
578
+ index, failed = cached["index"], cached.get("failed", {})
459
579
  else:
460
580
  log(f"lean: indexing {len(built)} built module(s)")
461
- recs = run_simple_probe(root, built, libs, [{"kind": "index", "name": ""}], lake)
581
+ recs, failed = probe_modules(root, built, libs, [{"kind": "index", "name": ""}],
582
+ lake, log)
462
583
  index = [r for r in recs if r["record"] == "index"]
463
- write_json(icache, {"digest": idigest, "index": index}, indent=None)
584
+ write_json(icache, {"digest": idigest, "index": index, "failed": failed},
585
+ indent=None)
464
586
  wanted = set()
465
587
  for c in claims:
466
588
  entry = links.get(c["key"], {})
@@ -472,23 +594,38 @@ def build_view(root: Path, paper_path: Path, out_dir: Path, lake: str = "lake",
472
594
  wanted |= {s["name"] for s in c["suggestions"][:1] if s.get("kind")}
473
595
  if wanted:
474
596
  log(f"lean: checking {len(wanted)} linked or suggested declaration(s)")
475
- recs = run_simple_probe(root, built, libs,
476
- [{"kind": "link", "name": n} for n in sorted(wanted)], lake)
477
- linkrecs = {r["name"]: r for r in recs if r["record"] == "link"}
478
- decls = {r["name"]: r for r in recs if r["record"] == "decl"}
597
+ loadable = [m for m in built if m not in failed]
598
+ recs, more = probe_modules(root, loadable, libs,
599
+ [{"kind": "link", "name": n} for n in sorted(wanted)],
600
+ lake, log)
601
+ failed.update(more)
602
+ for r in recs:
603
+ if r["record"] == "link":
604
+ linkrecs.setdefault(r["name"], []).append(r)
605
+ elif r["record"] == "decl":
606
+ decls.setdefault(r["name"], r)
607
+ if failed:
608
+ notes.append(f"{len(failed)} built module(s) could not be loaded by MachineChecked "
609
+ f"and are not indexed: {', '.join(sorted(failed)[:5])}"
610
+ + ("…" if len(failed) > 5 else ""))
479
611
 
480
612
  # Statuses.
481
613
  for c in claims:
482
614
  entry = links.get(c["key"], {})
483
615
  c["links"] = []
484
616
  for n in c.pop("link_names", None) or linked_names(c, entry):
485
- status, cls = link_state(linkrecs.get(n)) if built else \
486
- ("linked: Lean not checked (the project is not built)", "unspecified")
617
+ rec = None
618
+ if not built:
619
+ status, cls = "linked: Lean not checked (the project is not built)", "unspecified"
620
+ else:
621
+ rec, why = resolve_link(n, linkrecs.get(n, []), c.get("declared_modules", {}).get(n),
622
+ root, unbuilt, failed)
623
+ status, cls = (why, "unspecified") if why else link_state(rec)
487
624
  c["links"].append({"name": n, "status": status, "class": cls,
488
- "source": link_source(c, entry, n),
489
- "record": linkrecs.get(n)})
625
+ "source": link_source(c, entry, n), "record": rec})
490
626
  for s in c.get("suggestions", []):
491
- s["record"] = linkrecs.get(s["name"])
627
+ found = [r for r in linkrecs.get(s["name"], []) if "missing" not in r.get("errors", [])]
628
+ s["record"] = found[0] if len({((r.get("decl") or {}).get("module")) for r in found}) == 1 else None
492
629
  if c["links"]:
493
630
  worst = max(c["links"], key=lambda l: ["proved", "unspecified", "open", "malformed"]
494
631
  .index(l["class"]))