simdref 0.0.6__tar.gz → 0.0.7__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 (81) hide show
  1. {simdref-0.0.6/src/simdref.egg-info → simdref-0.0.7}/PKG-INFO +5 -4
  2. {simdref-0.0.6 → simdref-0.0.7}/README.md +4 -3
  3. {simdref-0.0.6 → simdref-0.0.7}/pyproject.toml +1 -1
  4. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/cli.py +94 -41
  5. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/export.py +10 -2
  6. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/storage.py +60 -4
  7. {simdref-0.0.6 → simdref-0.0.7/src/simdref.egg-info}/PKG-INFO +5 -4
  8. simdref-0.0.7/tests/test_cli_bare_query.py +232 -0
  9. {simdref-0.0.6 → simdref-0.0.7}/tests/test_issue2_fixes.py +91 -1
  10. simdref-0.0.7/tests/test_storage_payload.py +273 -0
  11. simdref-0.0.6/tests/test_cli_bare_query.py +0 -90
  12. simdref-0.0.6/tests/test_storage_payload.py +0 -56
  13. {simdref-0.0.6 → simdref-0.0.7}/LICENSE +0 -0
  14. {simdref-0.0.6 → simdref-0.0.7}/setup.cfg +0 -0
  15. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/__init__.py +0 -0
  16. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/__main__.py +0 -0
  17. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/annotate.py +0 -0
  18. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/arm_instructions.py +0 -0
  19. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/display.py +0 -0
  20. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/filters.py +0 -0
  21. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/ingest.py +0 -0
  22. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/ingest_catalog.py +0 -0
  23. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/ingest_pdf.py +0 -0
  24. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/ingest_sources.py +0 -0
  25. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/lsp.py +0 -0
  26. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/manpages.py +0 -0
  27. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/models.py +0 -0
  28. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/pdfparse/__init__.py +0 -0
  29. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/pdfparse/base.py +0 -0
  30. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/pdfparse/intel.py +0 -0
  31. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/pdfparse/registry.py +0 -0
  32. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/pdfparse/types.py +0 -0
  33. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/pdfrefs.py +0 -0
  34. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/perf.py +0 -0
  35. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/perf_sources/__init__.py +0 -0
  36. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/perf_sources/cores.py +0 -0
  37. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/perf_sources/llvm_mca.py +0 -0
  38. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/perf_sources/llvm_scheduling.py +0 -0
  39. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/perf_sources/merge.py +0 -0
  40. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/queries.py +0 -0
  41. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/riscv.py +0 -0
  42. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/search.py +0 -0
  43. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/tui.py +0 -0
  44. {simdref-0.0.6 → simdref-0.0.7}/src/simdref/ui_labels.py +0 -0
  45. {simdref-0.0.6 → simdref-0.0.7}/src/simdref.egg-info/SOURCES.txt +0 -0
  46. {simdref-0.0.6 → simdref-0.0.7}/src/simdref.egg-info/dependency_links.txt +0 -0
  47. {simdref-0.0.6 → simdref-0.0.7}/src/simdref.egg-info/entry_points.txt +0 -0
  48. {simdref-0.0.6 → simdref-0.0.7}/src/simdref.egg-info/requires.txt +0 -0
  49. {simdref-0.0.6 → simdref-0.0.7}/src/simdref.egg-info/top_level.txt +0 -0
  50. {simdref-0.0.6 → simdref-0.0.7}/tests/test_annotate.py +0 -0
  51. {simdref-0.0.6 → simdref-0.0.7}/tests/test_annotate_positions.py +0 -0
  52. {simdref-0.0.6 → simdref-0.0.7}/tests/test_audit_coverage.py +0 -0
  53. {simdref-0.0.6 → simdref-0.0.7}/tests/test_auto_update_on_version_change.py +0 -0
  54. {simdref-0.0.6 → simdref-0.0.7}/tests/test_cli_bootstrap_progress.py +0 -0
  55. {simdref-0.0.6 → simdref-0.0.7}/tests/test_cli_help.py +0 -0
  56. {simdref-0.0.6 → simdref-0.0.7}/tests/test_cli_llm.py +0 -0
  57. {simdref-0.0.6 → simdref-0.0.7}/tests/test_coverage_live.py +0 -0
  58. {simdref-0.0.6 → simdref-0.0.7}/tests/test_coverage_parity.py +0 -0
  59. {simdref-0.0.6 → simdref-0.0.7}/tests/test_display.py +0 -0
  60. {simdref-0.0.6 → simdref-0.0.7}/tests/test_export.py +0 -0
  61. {simdref-0.0.6 → simdref-0.0.7}/tests/test_filters.py +0 -0
  62. {simdref-0.0.6 → simdref-0.0.7}/tests/test_ingest_sources.py +0 -0
  63. {simdref-0.0.6 → simdref-0.0.7}/tests/test_intel_operation_and_url.py +0 -0
  64. {simdref-0.0.6 → simdref-0.0.7}/tests/test_issues_23_24_25.py +0 -0
  65. {simdref-0.0.6 → simdref-0.0.7}/tests/test_lsp_web.py +0 -0
  66. {simdref-0.0.6 → simdref-0.0.7}/tests/test_models.py +0 -0
  67. {simdref-0.0.6 → simdref-0.0.7}/tests/test_pdfparse.py +0 -0
  68. {simdref-0.0.6 → simdref-0.0.7}/tests/test_perf.py +0 -0
  69. {simdref-0.0.6 → simdref-0.0.7}/tests/test_perf_sources.py +0 -0
  70. {simdref-0.0.6 → simdref-0.0.7}/tests/test_presentation.py +0 -0
  71. {simdref-0.0.6 → simdref-0.0.7}/tests/test_preset_persistence.py +0 -0
  72. {simdref-0.0.6 → simdref-0.0.7}/tests/test_riscv_docs_pages.py +0 -0
  73. {simdref-0.0.6 → simdref-0.0.7}/tests/test_search.py +0 -0
  74. {simdref-0.0.6 → simdref-0.0.7}/tests/test_search_pushdown.py +0 -0
  75. {simdref-0.0.6 → simdref-0.0.7}/tests/test_source_kind_filter.py +0 -0
  76. {simdref-0.0.6 → simdref-0.0.7}/tests/test_source_validation.py +0 -0
  77. {simdref-0.0.6 → simdref-0.0.7}/tests/test_storage_schema.py +0 -0
  78. {simdref-0.0.6 → simdref-0.0.7}/tests/test_tui.py +0 -0
  79. {simdref-0.0.6 → simdref-0.0.7}/tests/test_ui_labels_parity.py +0 -0
  80. {simdref-0.0.6 → simdref-0.0.7}/tests/test_version_matches_pyproject.py +0 -0
  81. {simdref-0.0.6 → simdref-0.0.7}/tests/test_x86_linking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simdref
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: Searchable SIMD intrinsic and instruction reference with CLI, manpages, TUI, and LSP.
5
5
  Author: Marco
6
6
  License: GPL-3.0-or-later
@@ -42,8 +42,9 @@ A single searchable reference for SIMD intrinsics and instructions across
42
42
  **x86 (Intel + uops.info)**, **Arm (ACLE / AARCHMRS)**, and **RISC-V
43
43
  (RVV + unified-db)**. Runs as a CLI, a Textual TUI, an LSP server,
44
44
  on-demand manpages (`simdref man` — or run `simdref install-manpages`
45
- so plain `man vpaddd` works), a static web app, and a structured JSON
46
- interface for LLM skills.
45
+ so plain `man vpaddd` works), a JSON site-data export for
46
+ [simdref-web](https://github.com/simd-labs/simdref-web), and a
47
+ structured JSON interface for LLM skills.
47
48
 
48
49
  [Web App](https://simdref.diamondinoia.com/) ·
49
50
  [TestPyPI](https://test.pypi.org/project/simdref/) ·
@@ -105,7 +106,7 @@ isa # open the interactive TUI
105
106
  **Web app** — a self-contained static SPA with filters and performance
106
107
  tables, published to GitHub Pages at
107
108
  [simdref.diamondinoia.com](https://simdref.diamondinoia.com/) and
108
- built from the `simdref-web` repo. This repo (`simdref-core`) exports the
109
+ built from the `simdref-web` repo. This repo exports the
109
110
  JSON data it consumes:
110
111
 
111
112
  ```bash
@@ -8,8 +8,9 @@ A single searchable reference for SIMD intrinsics and instructions across
8
8
  **x86 (Intel + uops.info)**, **Arm (ACLE / AARCHMRS)**, and **RISC-V
9
9
  (RVV + unified-db)**. Runs as a CLI, a Textual TUI, an LSP server,
10
10
  on-demand manpages (`simdref man` — or run `simdref install-manpages`
11
- so plain `man vpaddd` works), a static web app, and a structured JSON
12
- interface for LLM skills.
11
+ so plain `man vpaddd` works), a JSON site-data export for
12
+ [simdref-web](https://github.com/simd-labs/simdref-web), and a
13
+ structured JSON interface for LLM skills.
13
14
 
14
15
  [Web App](https://simdref.diamondinoia.com/) ·
15
16
  [TestPyPI](https://test.pypi.org/project/simdref/) ·
@@ -71,7 +72,7 @@ isa # open the interactive TUI
71
72
  **Web app** — a self-contained static SPA with filters and performance
72
73
  tables, published to GitHub Pages at
73
74
  [simdref.diamondinoia.com](https://simdref.diamondinoia.com/) and
74
- built from the `simdref-web` repo. This repo (`simdref-core`) exports the
75
+ built from the `simdref-web` repo. This repo exports the
75
76
  JSON data it consumes:
76
77
 
77
78
  ```bash
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simdref"
7
- version = "0.0.6"
7
+ version = "0.0.7"
8
8
  description = "Searchable SIMD intrinsic and instruction reference with CLI, manpages, TUI, and LSP."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -9,6 +9,7 @@ Display and formatting logic lives in :mod:`simdref.display`.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import contextlib
12
13
  import json
13
14
  import os
14
15
  import shutil
@@ -56,7 +57,6 @@ from simdref.display import (
56
57
  display_isa,
57
58
  instruction_query_text,
58
59
  instruction_variant_items,
59
- isa_sort_key,
60
60
  isa_visible,
61
61
  normalize_instruction_query,
62
62
  render_intrinsic,
@@ -92,6 +92,7 @@ from simdref.storage import (
92
92
  DEFAULT_MAN_DIR,
93
93
  SQLITE_PATH,
94
94
  WEB_DIR,
95
+ _write_atomic,
95
96
  build_sqlite,
96
97
  load_catalog,
97
98
  load_catalog_from_db,
@@ -230,6 +231,10 @@ def _release_asset_url(tag: str, asset_name: str) -> str:
230
231
  return f"https://github.com/{GITHUB_REPO}/releases/download/{tag}/{asset_name}"
231
232
 
232
233
 
234
+ class _ReleaseAssetMissing(Exception):
235
+ """The release carries no compatible asset for this name."""
236
+
237
+
233
238
  def _download_from_release() -> None:
234
239
  """Download pre-built catalog and database from GitHub Release.
235
240
 
@@ -244,7 +249,7 @@ def _download_from_release() -> None:
244
249
 
245
250
  is_tty = err_console.is_terminal and os.environ.get("GITHUB_ACTIONS") != "true"
246
251
 
247
- def _fetch_into(asset: str, dest: Path) -> bool:
252
+ def _fetch_into(asset: str, fh) -> bool:
248
253
  for tag in _release_tag_candidates():
249
254
  url = _release_asset_url(tag, asset)
250
255
  try:
@@ -268,17 +273,15 @@ def _download_from_release() -> None:
268
273
  )
269
274
  with progress:
270
275
  task = progress.add_task(f"downloading {asset} ({tag})", total=total)
271
- with open(dest, "wb") as f:
272
- for chunk in resp.iter_bytes(chunk_size=1024 * 64):
273
- f.write(chunk)
274
- progress.update(task, advance=len(chunk))
276
+ for chunk in resp.iter_bytes(chunk_size=1024 * 64):
277
+ fh.write(chunk)
278
+ progress.update(task, advance=len(chunk))
275
279
  else:
276
280
  err_console.print(f"downloading {asset} from {tag}...", style="dim")
277
281
  written = 0
278
- with open(dest, "wb") as f:
279
- for chunk in resp.iter_bytes(chunk_size=1024 * 64):
280
- f.write(chunk)
281
- written += len(chunk)
282
+ for chunk in resp.iter_bytes(chunk_size=1024 * 64):
283
+ fh.write(chunk)
284
+ written += len(chunk)
282
285
  if total:
283
286
  err_console.print(
284
287
  f"downloaded {asset}: {written / 1_048_576:.1f} MB "
@@ -313,12 +316,19 @@ def _download_from_release() -> None:
313
316
 
314
317
  for asset in ("catalog.msgpack", "catalog.db"):
315
318
  dest = DATA_DIR / asset
316
- if not _fetch_into(asset, dest):
319
+
320
+ def write(fh, asset=asset):
321
+ if not _fetch_into(asset, fh):
322
+ raise _ReleaseAssetMissing(asset)
323
+
324
+ try:
325
+ _write_atomic(dest, write)
326
+ except _ReleaseAssetMissing:
317
327
  err_console.print(
318
328
  f"failed to download {asset}: no compatible release asset found", style="red"
319
329
  )
320
330
  err_console.print("try 'simdref build' to build locally", style="yellow")
321
- raise typer.Exit(code=1)
331
+ raise typer.Exit(code=1) from None
322
332
  err_console.print("download complete", style="green")
323
333
 
324
334
 
@@ -458,9 +468,7 @@ def _build_runtime_locally(*, man_dir: Path, include_sdm: bool = False) -> None:
458
468
 
459
469
  web_task = count_progress.add_task("Exporting site data", total=1)
460
470
  export_site_data(catalog, WEB_DIR)
461
- count_progress.update(
462
- web_task, completed=1, description="Exporting site data \u2713"
463
- )
471
+ count_progress.update(web_task, completed=1, description="Exporting site data \u2713")
464
472
 
465
473
  err_console.print(
466
474
  f"updated catalog with {len(catalog.intrinsics)} intrinsics and {len(catalog.instructions)} instructions",
@@ -972,7 +980,11 @@ def _payload_source_kinds(arch_details) -> list[str]:
972
980
  # ---------------------------------------------------------------------------
973
981
 
974
982
 
975
- def _print_search_results_runtime(conn, query: str, limit: int = 20) -> None:
983
+ def _print_search_results_runtime(conn, query: str, limit: int = 20, as_json: bool = False) -> int:
984
+ """Render the ranked search table (or JSON) in the search's own
985
+ relevance order. Returns the number of rows actually printed — callers
986
+ use 0 to fall through to the no-match branch instead of printing an
987
+ empty table."""
976
988
  results, intrinsic_map, instruction_map = _search_runtime(conn, query, limit=limit)
977
989
  prepared_rows = []
978
990
  for result in results:
@@ -980,7 +992,6 @@ def _print_search_results_runtime(conn, query: str, limit: int = 20) -> None:
980
992
  isa = "-"
981
993
  lat = "-"
982
994
  cpi = "-"
983
- isa_sort = (99, "-")
984
995
  if result.kind == "instruction":
985
996
  item = instruction_map.get(result.key)
986
997
  if item is not None:
@@ -988,7 +999,6 @@ def _print_search_results_runtime(conn, query: str, limit: int = 20) -> None:
988
999
  continue
989
1000
  arch = display_architecture(item.architecture)
990
1001
  isa = display_isa(item.isa)
991
- isa_sort = isa_sort_key(item.isa)
992
1002
  lat, cpi = variant_perf_summary(item.arch_details)
993
1003
  elif result.kind == "intrinsic":
994
1004
  item = intrinsic_map.get(result.key)
@@ -997,18 +1007,37 @@ def _print_search_results_runtime(conn, query: str, limit: int = 20) -> None:
997
1007
  continue
998
1008
  arch = display_architecture(item.architecture)
999
1009
  isa = display_isa(item.isa)
1000
- isa_sort = isa_sort_key(item.isa)
1001
1010
  lat, cpi = intrinsic_perf_summary_runtime(conn, item, instruction_map)
1002
- prepared_rows.append((result, arch, isa, lat, cpi, isa_sort))
1003
- prepared_rows.sort(
1004
- key=lambda row: (
1005
- row[0].kind != "instruction",
1006
- row[5],
1007
- row[0].title.casefold(),
1008
- row[0].key.casefold(),
1011
+ prepared_rows.append((result, arch, isa, lat, cpi))
1012
+ if not prepared_rows:
1013
+ return 0
1014
+ if as_json:
1015
+ typer.echo(
1016
+ json.dumps(
1017
+ {
1018
+ "query": query,
1019
+ "mode": "search",
1020
+ "results": [
1021
+ {
1022
+ "kind": r.kind,
1023
+ "key": r.key,
1024
+ "title": r.title,
1025
+ "subtitle": r.subtitle,
1026
+ "arch": arch,
1027
+ "isa": isa,
1028
+ "latency_cycles": lat,
1029
+ "tput_cpi": cpi,
1030
+ }
1031
+ for r, arch, isa, lat, cpi in prepared_rows
1032
+ ],
1033
+ },
1034
+ indent=2,
1035
+ default=str,
1036
+ )
1009
1037
  )
1010
- )
1011
- render_search_results([(r, arch, isa, lat, cpi) for r, arch, isa, lat, cpi, _ in prepared_rows])
1038
+ return len(prepared_rows)
1039
+ render_search_results(prepared_rows)
1040
+ return len(prepared_rows)
1012
1041
 
1013
1042
 
1014
1043
  # ---------------------------------------------------------------------------
@@ -1024,21 +1053,45 @@ def _smart_lookup(
1024
1053
  ) -> int:
1025
1054
  """Dispatch a bare query.
1026
1055
 
1027
- If ``query`` resolves to known instruction records, print a plain-text
1028
- (or JSON) summary to stdout and exit — never blocks on a TUI. Only
1029
- open the TUI when the query is unknown and stdio is a TTY, so the user
1030
- can explore; in non-TTY contexts an unknown query exits with code 2.
1056
+ Exact intrinsic match prints the intrinsic detail and exits 0. Exact
1057
+ instruction match prints a plain-text (or JSON) summary and exits 0;
1058
+ ``--arch`` applies to this branch only. Fuzzy hits print the ranked
1059
+ search list (or JSON) and exit 0, on a TTY or not. Nothing found opens
1060
+ the TUI on a TTY (never under ``--json``) or exits 2 non-interactively.
1031
1061
  """
1032
1062
  ensure_runtime()
1033
- records = _find_instructions_fast(query)
1034
- if records:
1035
- return _print_non_interactive_summary(query, records=records, arch=arch, as_json=as_json)
1036
- if not (sys.stdin.isatty() and sys.stdout.isatty()):
1037
- err_console.print(
1038
- f"no instruction match for {query!r} (non-interactive; run in a TTY to search).",
1039
- style="yellow",
1040
- )
1041
- return 2
1063
+ with contextlib.closing(open_db()) as conn:
1064
+ intrinsic = load_intrinsic_from_db(conn, query)
1065
+ if intrinsic is not None and arch is not None:
1066
+ err_console.print("--arch applies to instruction queries only", style="red")
1067
+ return 2
1068
+ if intrinsic is not None:
1069
+ if as_json:
1070
+ typer.echo(json.dumps(asdict(intrinsic), indent=2, default=str))
1071
+ else:
1072
+ render_intrinsic(None, intrinsic, conn=conn)
1073
+ return 0
1074
+ records = _find_instructions_fast(query)
1075
+ if records:
1076
+ return _print_non_interactive_summary(
1077
+ query, records=records, arch=arch, as_json=as_json
1078
+ )
1079
+ if arch is not None:
1080
+ err_console.print("--arch applies to instruction queries only", style="red")
1081
+ return 2
1082
+ printed = _print_search_results_runtime(conn, query, as_json=as_json)
1083
+ if printed:
1084
+ if not as_json and sys.stdin.isatty() and sys.stdout.isatty():
1085
+ # A fuzzy match on a TTY opens the TUI (preset as passed, like
1086
+ # origin/main); non-TTY and --json keep printing the list.
1087
+ return _run_tui(initial_query=query, initial_preset=preset)
1088
+ return 0
1089
+ if as_json or not (sys.stdin.isatty() and sys.stdout.isatty()):
1090
+ err_console.print(
1091
+ f"no match for {query!r} (non-interactive; run in a TTY to search).",
1092
+ style="yellow",
1093
+ )
1094
+ return 2
1042
1095
  return _run_tui(initial_query=query, initial_preset=preset)
1043
1096
 
1044
1097
 
@@ -313,12 +313,20 @@ def _columnar_instructions(entries: list[dict]) -> dict:
313
313
  cols["form"].append(0 if e["form"] == e["display_form"] else e["form"])
314
314
  cols["dmn"].append(e["display_mnemonic"])
315
315
  cols["mn"].append(0 if e["mnemonic"] == e["display_mnemonic"] else e["mnemonic"])
316
- cols["key"].append(0 if e["key"] == e["architecture"] + ":" + e["form"].lower() else e["key"])
316
+ cols["key"].append(
317
+ 0 if e["key"] == e["architecture"] + ":" + e["form"].lower() else e["key"]
318
+ )
317
319
  cols["sum"].append(f[3])
318
320
  cols["isa"].append(isa([e["isa"], e["display_isa"], e["isa_families"], e["isa_subs"]]))
319
321
  cols["arch"].append(arch([e["architecture"], e["display_architecture"]]))
320
322
  cols["perf"].append(perf([e["lat"], e["cpi"]]))
321
- if f != [e["display_mnemonic"], e["display_key"], e["display_form"], f[3], e["display_isa"]]:
323
+ if f != [
324
+ e["display_mnemonic"],
325
+ e["display_key"],
326
+ e["display_form"],
327
+ f[3],
328
+ e["display_isa"],
329
+ ]:
322
330
  fields[i] = f
323
331
  if _truncate(f[3], 80) != e["summary"]:
324
332
  summaries[i] = e["summary"]
@@ -6,9 +6,12 @@ FTS5 virtual tables for fast full-text search with BM25 ranking.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import contextlib
9
10
  import os
10
11
  import re
12
+ import secrets
11
13
  import sqlite3
14
+ import stat
12
15
  import sys
13
16
  import zlib
14
17
  from itertools import islice
@@ -115,9 +118,7 @@ def read_installed_version_stamp() -> str | None:
115
118
  try:
116
119
  conn = sqlite3.connect(SQLITE_PATH)
117
120
  try:
118
- row = conn.execute(
119
- "SELECT value FROM meta WHERE key = 'installed_version'"
120
- ).fetchone()
121
+ row = conn.execute("SELECT value FROM meta WHERE key = 'installed_version'").fetchone()
121
122
  finally:
122
123
  conn.close()
123
124
  except sqlite3.DatabaseError:
@@ -183,10 +184,63 @@ def load_catalog_from_db(path: Path = SQLITE_PATH) -> Catalog:
183
184
  )
184
185
 
185
186
 
187
+ def _write_atomic(path, write_fn) -> None:
188
+ """Publish ``path`` atomically.
189
+
190
+ Writes via ``write_fn(fh)`` to a random sibling temp file, then
191
+ ``os.replace``. A symlinked destination keeps its link; the real file
192
+ is replaced. An existing target's mode is set on the temp before the
193
+ first byte is written; a new file gets ``0o666 & ~umask``, the same
194
+ mode as ``path.open("wb")``. Any failure unlinks the temp and
195
+ re-raises.
196
+ """
197
+ # ponytail: keeps mode bits only; owner, group and ACLs are not copied
198
+ # (a user cache file).
199
+ target = Path(os.path.realpath(path))
200
+ try:
201
+ old_mode = stat.S_IMODE(os.stat(target).st_mode)
202
+ except FileNotFoundError:
203
+ old_mode = None
204
+ while True:
205
+ tmp = f"{target}.{secrets.token_hex(8)}.tmp"
206
+ try:
207
+ fd = os.open(
208
+ tmp,
209
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL,
210
+ old_mode if old_mode is not None else 0o666,
211
+ )
212
+ break
213
+ except FileExistsError:
214
+ continue
215
+ try:
216
+ # fchmod must run (when needed) while the raw fd is the only handle,
217
+ # so a failure here never looks for a file object that does not exist.
218
+ if old_mode is not None:
219
+ os.fchmod(fd, old_mode) # tighten before any byte
220
+ fh = os.fdopen(fd, "wb")
221
+ except BaseException:
222
+ # One cleanup for a pre-fdopen failure: close the fd, drop the temp.
223
+ os.close(fd)
224
+ with contextlib.suppress(OSError):
225
+ os.unlink(tmp)
226
+ raise
227
+ try:
228
+ with fh:
229
+ write_fn(fh)
230
+ os.replace(tmp, target)
231
+ except BaseException:
232
+ with contextlib.suppress(OSError):
233
+ fh.close()
234
+ with contextlib.suppress(OSError):
235
+ os.unlink(tmp)
236
+ raise
237
+
238
+
186
239
  def save_catalog(catalog: Catalog, path: Path = CATALOG_PATH) -> None:
187
240
  ensure_dir(path.parent)
188
241
  packer = msgpack.Packer(use_bin_type=True)
189
- with path.open("wb") as fh:
242
+
243
+ def write(fh) -> None:
190
244
  fh.write(packer.pack_map_header(4))
191
245
  fh.write(packer.pack("intrinsics"))
192
246
  fh.write(packer.pack_array_header(len(catalog.intrinsics)))
@@ -203,6 +257,8 @@ def save_catalog(catalog: Catalog, path: Path = CATALOG_PATH) -> None:
203
257
  fh.write(packer.pack("generated_at"))
204
258
  fh.write(packer.pack(catalog.generated_at))
205
259
 
260
+ _write_atomic(path, write)
261
+
206
262
 
207
263
  def open_db(path: Path = SQLITE_PATH) -> sqlite3.Connection:
208
264
  conn = sqlite3.connect(path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simdref
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: Searchable SIMD intrinsic and instruction reference with CLI, manpages, TUI, and LSP.
5
5
  Author: Marco
6
6
  License: GPL-3.0-or-later
@@ -42,8 +42,9 @@ A single searchable reference for SIMD intrinsics and instructions across
42
42
  **x86 (Intel + uops.info)**, **Arm (ACLE / AARCHMRS)**, and **RISC-V
43
43
  (RVV + unified-db)**. Runs as a CLI, a Textual TUI, an LSP server,
44
44
  on-demand manpages (`simdref man` — or run `simdref install-manpages`
45
- so plain `man vpaddd` works), a static web app, and a structured JSON
46
- interface for LLM skills.
45
+ so plain `man vpaddd` works), a JSON site-data export for
46
+ [simdref-web](https://github.com/simd-labs/simdref-web), and a
47
+ structured JSON interface for LLM skills.
47
48
 
48
49
  [Web App](https://simdref.diamondinoia.com/) ·
49
50
  [TestPyPI](https://test.pypi.org/project/simdref/) ·
@@ -105,7 +106,7 @@ isa # open the interactive TUI
105
106
  **Web app** — a self-contained static SPA with filters and performance
106
107
  tables, published to GitHub Pages at
107
108
  [simdref.diamondinoia.com](https://simdref.diamondinoia.com/) and
108
- built from the `simdref-web` repo. This repo (`simdref-core`) exports the
109
+ built from the `simdref-web` repo. This repo exports the
109
110
  JSON data it consumes:
110
111
 
111
112
  ```bash