sciwrite-lint 0.2.0__py3-none-any.whl

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 (76) hide show
  1. sciwrite_lint/__init__.py +3 -0
  2. sciwrite_lint/__main__.py +527 -0
  3. sciwrite_lint/_network.py +195 -0
  4. sciwrite_lint/api.py +1484 -0
  5. sciwrite_lint/checks/__init__.py +1 -0
  6. sciwrite_lint/checks/_section_utils.py +111 -0
  7. sciwrite_lint/checks/cite_purpose.py +122 -0
  8. sciwrite_lint/checks/claim_support.py +96 -0
  9. sciwrite_lint/checks/cross_section_consistency.py +185 -0
  10. sciwrite_lint/checks/dangling_cite.py +93 -0
  11. sciwrite_lint/checks/dangling_ref.py +116 -0
  12. sciwrite_lint/checks/full_paper_consistency.py +604 -0
  13. sciwrite_lint/checks/ref_internal_checks.py +919 -0
  14. sciwrite_lint/checks/reference_accuracy.py +277 -0
  15. sciwrite_lint/checks/reference_exists.py +119 -0
  16. sciwrite_lint/checks/reference_unreliable.py +244 -0
  17. sciwrite_lint/checks/registry.py +136 -0
  18. sciwrite_lint/checks/retracted_cite.py +96 -0
  19. sciwrite_lint/checks/structure_promises.py +115 -0
  20. sciwrite_lint/checks/unreferenced_figure.py +70 -0
  21. sciwrite_lint/claims.py +330 -0
  22. sciwrite_lint/claude_backend.py +94 -0
  23. sciwrite_lint/claude_cli.py +405 -0
  24. sciwrite_lint/cli/__init__.py +1 -0
  25. sciwrite_lint/cli/check.py +480 -0
  26. sciwrite_lint/cli/config.py +229 -0
  27. sciwrite_lint/cli/fetch.py +250 -0
  28. sciwrite_lint/cli/misc.py +1202 -0
  29. sciwrite_lint/cli/rank.py +470 -0
  30. sciwrite_lint/cli/verify.py +437 -0
  31. sciwrite_lint/config.py +646 -0
  32. sciwrite_lint/cross_paper.py +174 -0
  33. sciwrite_lint/eval_claims.py +1196 -0
  34. sciwrite_lint/fulltext.py +851 -0
  35. sciwrite_lint/llm_utils.py +386 -0
  36. sciwrite_lint/local_pdfs.py +122 -0
  37. sciwrite_lint/manuscript_store.py +674 -0
  38. sciwrite_lint/models.py +139 -0
  39. sciwrite_lint/pdf/__init__.py +1 -0
  40. sciwrite_lint/pdf/grobid.py +785 -0
  41. sciwrite_lint/pdf/pdf_download.py +258 -0
  42. sciwrite_lint/pipeline.py +2694 -0
  43. sciwrite_lint/prompt_safety.py +30 -0
  44. sciwrite_lint/rate_limiter.py +227 -0
  45. sciwrite_lint/references/__init__.py +1 -0
  46. sciwrite_lint/references/citations.py +715 -0
  47. sciwrite_lint/references/crossref.py +282 -0
  48. sciwrite_lint/references/embedding_store.py +380 -0
  49. sciwrite_lint/references/matching.py +273 -0
  50. sciwrite_lint/references/metadata.py +273 -0
  51. sciwrite_lint/references/reference_store.py +823 -0
  52. sciwrite_lint/references/retraction_watch.py +178 -0
  53. sciwrite_lint/references/workspace_db.py +1390 -0
  54. sciwrite_lint/report.py +163 -0
  55. sciwrite_lint/schemas.py +260 -0
  56. sciwrite_lint/scoring/__init__.py +1 -0
  57. sciwrite_lint/scoring/chain.py +716 -0
  58. sciwrite_lint/scoring/contribution.py +322 -0
  59. sciwrite_lint/scoring/scilint_score.py +611 -0
  60. sciwrite_lint/tex_parser.py +248 -0
  61. sciwrite_lint/usage.py +594 -0
  62. sciwrite_lint/vision/__init__.py +1 -0
  63. sciwrite_lint/vision/cache.py +140 -0
  64. sciwrite_lint/vision/describe.py +311 -0
  65. sciwrite_lint/vision/image_extraction.py +491 -0
  66. sciwrite_lint/vision/pipeline.py +207 -0
  67. sciwrite_lint/vllm/__init__.py +1 -0
  68. sciwrite_lint/vllm/metrics.py +157 -0
  69. sciwrite_lint/vllm/vllm_server.py +445 -0
  70. sciwrite_lint/web.py +369 -0
  71. sciwrite_lint-0.2.0.dist-info/METADATA +268 -0
  72. sciwrite_lint-0.2.0.dist-info/RECORD +76 -0
  73. sciwrite_lint-0.2.0.dist-info/WHEEL +5 -0
  74. sciwrite_lint-0.2.0.dist-info/entry_points.txt +2 -0
  75. sciwrite_lint-0.2.0.dist-info/licenses/LICENSE +21 -0
  76. sciwrite_lint-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ """sciwrite-lint: a linter for scientific manuscripts."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,527 @@
1
+ """CLI entry point: sciwrite-lint check paper.tex
2
+
3
+ Command implementations live in sciwrite_lint.cli submodules:
4
+ - cli.check: check, checks
5
+ - cli.verify: verify, verify-claims, status
6
+ - cli.fetch: fetch
7
+ - cli.rank: contributions
8
+ - cli.misc: init, parse, override, dismiss-claim, grobid, vllm, vision
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ from loguru import logger
18
+
19
+ from sciwrite_lint import __version__
20
+ from sciwrite_lint.config import LintConfig, PaperConfig, load_config
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Helpers (used by CLI submodules)
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ def _load_config(args: argparse.Namespace) -> LintConfig:
29
+ """Load config from --config flag or auto-discovery."""
30
+ if getattr(args, "config", None):
31
+ return load_config(Path(args.config))
32
+
33
+ config = load_config(None)
34
+ if config.config_path is None:
35
+ from sciwrite_lint.config import _detect_papers
36
+
37
+ logger.error("No .sciwrite-lint.toml found.")
38
+ detected = _detect_papers()
39
+ if detected:
40
+ logger.error(" Detected .tex files:")
41
+ for p in detected:
42
+ bib = f" (bib: {p['bib']})" if p.get("bib") else ""
43
+ logger.error(f" {p['file_path']}{bib}")
44
+ logger.error(" Run: sciwrite-lint init")
45
+ logger.error(" Then review .sciwrite-lint.toml before running checks.")
46
+ return config
47
+
48
+
49
+ def _resolve_paper(config: LintConfig, name: str) -> PaperConfig | None:
50
+ """Resolve a paper name to its config. Print error if not found."""
51
+ pc = config.get_paper(name)
52
+ if not pc:
53
+ if config.papers:
54
+ names = ", ".join(p.name for p in config.papers)
55
+ logger.error(f"Unknown paper '{name}'. Registered: {names}")
56
+ else:
57
+ logger.error(f"Unknown paper '{name}'. No papers registered.")
58
+ logger.error(
59
+ f" Add [[papers]] to {config.config_path or '.sciwrite-lint.toml'}"
60
+ )
61
+ return pc
62
+
63
+
64
+ def _paper_names(config: LintConfig) -> list[str]:
65
+ return [p.name for p in config.papers]
66
+
67
+
68
+ def _resolve_input_files(
69
+ args: argparse.Namespace, config: LintConfig
70
+ ) -> list[tuple[str, Path]]:
71
+ """Resolve which files to check (.tex or .pdf).
72
+
73
+ Priority: positional file > --paper (from config) > all papers in config.
74
+ Returns list of (name, path) pairs.
75
+ """
76
+ if hasattr(args, "file") and args.file:
77
+ p = Path(args.file)
78
+ return [(p.stem, p)]
79
+
80
+ paper_filter = getattr(args, "paper", None)
81
+ if paper_filter:
82
+ pc = _resolve_paper(config, paper_filter)
83
+ if not pc:
84
+ return []
85
+ return [(pc.name, pc.file_path)]
86
+
87
+ if config.papers:
88
+ return [(pc.name, pc.file_path) for pc in config.papers]
89
+
90
+ logger.error("No papers registered. Either:")
91
+ logger.error(" sciwrite-lint check <file.tex|file.pdf> — check a specific file")
92
+ logger.error(
93
+ " sciwrite-lint init — set up project with [[papers]]"
94
+ )
95
+ return []
96
+
97
+
98
+ def _classify_verify_issue(issue: str) -> tuple[str, str]:
99
+ """Classify a verify issue string into (level, rule_id) for findings."""
100
+ from sciwrite_lint.pipeline import _classify_verify_issue as _classify
101
+
102
+ return _classify(issue)
103
+
104
+
105
+ def _setup_logging(config: LintConfig) -> None:
106
+ """Configure loguru rotating file sink from config."""
107
+ logger.add(
108
+ "logs/sciwrite-lint.log",
109
+ rotation="10 MB",
110
+ retention="30 days",
111
+ compression="gz",
112
+ level=config.log_level,
113
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level:<8} | {name}:{function}:{line} | {message}",
114
+ )
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # Main parser
119
+ # ---------------------------------------------------------------------------
120
+
121
+
122
+ def main(argv: list[str] | None = None) -> int:
123
+ from sciwrite_lint.cli.check import run_check, run_checks_list
124
+ from sciwrite_lint.cli.config import run_config
125
+ from sciwrite_lint.cli.fetch import run_fetch
126
+ from sciwrite_lint.cli.misc import (
127
+ run_containers,
128
+ run_dismiss_claim,
129
+ run_grobid,
130
+ run_init,
131
+ run_override,
132
+ run_parse,
133
+ run_vision,
134
+ run_vllm,
135
+ )
136
+ from sciwrite_lint.cli.rank import run_contributions
137
+ from sciwrite_lint.cli.verify import (
138
+ run_ref_health,
139
+ run_status,
140
+ run_verify,
141
+ run_verify_claims,
142
+ )
143
+
144
+ parser = argparse.ArgumentParser(
145
+ prog="sciwrite-lint",
146
+ description="A linter for scientific manuscripts.",
147
+ )
148
+ parser.add_argument(
149
+ "--version", action="version", version=f"sciwrite-lint {__version__}"
150
+ )
151
+ sub = parser.add_subparsers(dest="command")
152
+
153
+ # --- check ---
154
+ p_check = sub.add_parser(
155
+ "check",
156
+ help="Full pipeline: verify references, check claims, assess reliability → SciLint Score",
157
+ )
158
+ p_check.add_argument(
159
+ "file",
160
+ nargs="?",
161
+ default=None,
162
+ help="Path to .tex or .pdf file (.tex: text + LLM rules, .pdf: GROBID parse + checks)",
163
+ )
164
+ p_check.add_argument("--paper", help="Paper name from [[papers]] in config")
165
+ p_check.add_argument("--config", help="Path to .sciwrite-lint.toml")
166
+ p_check.add_argument("--format", choices=["terminal", "json"], default=None)
167
+ p_check.add_argument(
168
+ "--fresh",
169
+ action="store_true",
170
+ help="Start from scratch: re-verify references, re-fetch, re-parse, re-run claims (ignore all caches)",
171
+ )
172
+ p_check.add_argument(
173
+ "--concurrency",
174
+ type=int,
175
+ default=2,
176
+ help="Max papers in concurrent stages when checking 2+ papers "
177
+ "(default: 2, validated up to 4). GPU stages always batched into one "
178
+ "subprocess; vLLM and network stages run concurrently up to this "
179
+ "limit. Above 4, the live monitor may fail to track progress and "
180
+ "vLLM/API throughput plateaus.",
181
+ )
182
+ p_check.set_defaults(func=run_check)
183
+
184
+ # --- checks ---
185
+ p_checks = sub.add_parser("checks", help="List all registered checks")
186
+ p_checks.set_defaults(func=run_checks_list)
187
+
188
+ # --- init ---
189
+ p_init = sub.add_parser(
190
+ "init", help="Initialize project: config + references directory"
191
+ )
192
+ p_init.add_argument(
193
+ "--force", action="store_true", help="Overwrite existing config"
194
+ )
195
+ p_init.set_defaults(func=run_init)
196
+
197
+ # --- config ---
198
+ p_config = sub.add_parser("config", help="Manage polite email and API keys")
199
+ config_sub = p_config.add_subparsers(dest="config_action")
200
+
201
+ p_config_show = config_sub.add_parser(
202
+ "show", help="Show current email and API key status"
203
+ )
204
+ p_config_show.add_argument("--config", help="Path to .sciwrite-lint.toml")
205
+ p_config_show.set_defaults(func=run_config)
206
+
207
+ p_config_email = config_sub.add_parser(
208
+ "set-email", help="Set polite email (for CrossRef, Unpaywall, Retraction Watch)"
209
+ )
210
+ p_config_email.add_argument("email", help="Your email address")
211
+ p_config_email.add_argument("--config", help="Path to .sciwrite-lint.toml")
212
+ p_config_email.set_defaults(func=run_config)
213
+
214
+ p_config_set_key = config_sub.add_parser(
215
+ "set-key", help="Save an API key for a service"
216
+ )
217
+ p_config_set_key.add_argument(
218
+ "service",
219
+ choices=["semantic-scholar", "ncbi", "core"],
220
+ help="Service name",
221
+ )
222
+ p_config_set_key.add_argument("key", help="API key value")
223
+ p_config_set_key.set_defaults(func=run_config)
224
+
225
+ p_config_rm_key = config_sub.add_parser(
226
+ "remove-key", help="Remove a stored API key"
227
+ )
228
+ p_config_rm_key.add_argument(
229
+ "service",
230
+ choices=["semantic-scholar", "ncbi", "core"],
231
+ help="Service to remove key for",
232
+ )
233
+ p_config_rm_key.set_defaults(func=run_config)
234
+
235
+ # --- verify ---
236
+ p_verify = sub.add_parser("verify", help="Verify citations against APIs (slow)")
237
+ p_verify.add_argument("--paper", required=True, help="Paper to verify")
238
+ p_verify.add_argument("--key", help="Verify a single citation key")
239
+ p_verify.add_argument(
240
+ "--fetch", action="store_true", help="Also attempt full-text download"
241
+ )
242
+ p_verify.add_argument("--config", help="Path to .sciwrite-lint.toml")
243
+ p_verify.add_argument("--format", choices=["terminal", "json"], default=None)
244
+ p_verify.set_defaults(func=run_verify)
245
+
246
+ # --- status ---
247
+ p_status = sub.add_parser("status", help="Show citation verification status")
248
+ p_status.add_argument("--paper", required=True, help="Paper to show")
249
+ p_status.add_argument("--tier", choices=["T1", "T2", "T3"], help="Filter by tier")
250
+ p_status.add_argument("--config", help="Path to .sciwrite-lint.toml")
251
+ p_status.set_defaults(func=run_status)
252
+
253
+ # --- ref-health ---
254
+ p_rh = sub.add_parser(
255
+ "ref-health",
256
+ help="Fast deterministic reference health check (no API calls)",
257
+ )
258
+ p_rh.add_argument("file", nargs="?", help=".tex file to check")
259
+ p_rh.add_argument("--paper", help="Paper name from config")
260
+ p_rh.add_argument("--config", help="Path to .sciwrite-lint.toml")
261
+ p_rh.set_defaults(func=run_ref_health)
262
+
263
+ # --- fetch ---
264
+ p_fetch = sub.add_parser("fetch", help="Download full text for T2 citations")
265
+ p_fetch.add_argument("--paper", required=True, help="Paper to fetch for")
266
+ p_fetch.add_argument("--key", help="Fetch for a single citation key")
267
+ p_fetch.add_argument(
268
+ "--all-missing", action="store_true", help="Fetch all T2 citations"
269
+ )
270
+ p_fetch.add_argument("--config", help="Path to .sciwrite-lint.toml")
271
+ p_fetch.set_defaults(func=run_fetch)
272
+
273
+ # --- parse ---
274
+ p_parse = sub.add_parser(
275
+ "parse", help="Parse T1 PDFs via GROBID and compute embeddings"
276
+ )
277
+ p_parse.add_argument("--paper", required=True, help="Paper name from [[papers]]")
278
+ p_parse.add_argument("--key", help="Parse a single citation key")
279
+ p_parse.add_argument(
280
+ "--fresh", action="store_true", help="Ignore cached results, re-parse all"
281
+ )
282
+ p_parse.add_argument(
283
+ "--no-embed", action="store_true", help="Skip embedding computation"
284
+ )
285
+ p_parse.add_argument("--config", help="Path to .sciwrite-lint.toml")
286
+ p_parse.set_defaults(func=run_parse)
287
+
288
+ # --- verify-claims ---
289
+ p_vc = sub.add_parser("verify-claims", help="Verify claims against cited sources")
290
+ p_vc.add_argument("--paper", required=True, help="Paper to verify")
291
+ p_vc.add_argument(
292
+ "--backend",
293
+ choices=["vllm", "claude"],
294
+ default="vllm",
295
+ help="vllm (local LLM, fast) or claude (Claude CLI, deep)",
296
+ )
297
+ p_vc.add_argument(
298
+ "--model",
299
+ choices=["qwen3", "gemma3"],
300
+ default="",
301
+ help="vLLM model preset (default: qwen3)",
302
+ )
303
+ p_vc.add_argument("--key", help="Verify only claims citing this key")
304
+ p_vc.add_argument("--limit", type=int, help="Max claims to verify")
305
+ p_vc.add_argument(
306
+ "--fresh",
307
+ action="store_true",
308
+ help="Ignore cached results, re-verify all claims",
309
+ )
310
+ p_vc.add_argument("--output-dir", default=None, help="Output directory")
311
+ p_vc.add_argument("--config", help="Path to .sciwrite-lint.toml")
312
+ p_vc.add_argument("--format", choices=["terminal", "json"], default=None)
313
+ p_vc.set_defaults(func=run_verify_claims)
314
+
315
+ # --- override ---
316
+ p_override = sub.add_parser(
317
+ "override", help="Manually override a citation's verification tier"
318
+ )
319
+ p_override.add_argument("--paper", required=True, help="Paper name from [[papers]]")
320
+ p_override.add_argument("--key", required=True, help="Citation key to override")
321
+ p_override.add_argument(
322
+ "--tier",
323
+ choices=["T1", "T2"],
324
+ required=True,
325
+ help="Target tier (T1 if you have full text, T2 if confirmed real)",
326
+ )
327
+ p_override.add_argument(
328
+ "--reason", required=True, help="Why you're overriding (stored in metadata)"
329
+ )
330
+ p_override.add_argument(
331
+ "--clear", action="store_true", help="Remove an existing override"
332
+ )
333
+ p_override.add_argument("--config", help="Path to .sciwrite-lint.toml")
334
+ p_override.set_defaults(func=run_override)
335
+
336
+ # --- dismiss-claim ---
337
+ p_dismiss = sub.add_parser(
338
+ "dismiss-claim", help="Dismiss a claim finding as false positive"
339
+ )
340
+ p_dismiss.add_argument("--paper", required=True, help="Paper the claim belongs to")
341
+ p_dismiss.add_argument("--key", required=True, help="Citation key")
342
+ p_dismiss.add_argument(
343
+ "--line", type=int, required=True, help="Line number of the claim"
344
+ )
345
+ p_dismiss.add_argument(
346
+ "--reason", required=True, help="Why this is a false positive"
347
+ )
348
+ p_dismiss.add_argument("--clear", action="store_true", help="Remove dismissal")
349
+ p_dismiss.add_argument("--output-dir", default=None, help="Output directory")
350
+ p_dismiss.set_defaults(func=run_dismiss_claim)
351
+
352
+ # --- contributions ---
353
+ p_score = sub.add_parser(
354
+ "contributions",
355
+ help="Compute contribution axes and update SciLint Score. Requires vLLM.",
356
+ )
357
+ p_score.add_argument(
358
+ "file",
359
+ nargs="?",
360
+ default=None,
361
+ help="Path to .tex or .pdf file (standalone scoring, no check needed)",
362
+ )
363
+ p_score.add_argument("--paper", default=None, help="Paper to score")
364
+ p_score.add_argument("--output-dir", default=None, help="Output directory")
365
+ p_score.add_argument(
366
+ "--findings", help="Path to check findings JSON (for internal score)"
367
+ )
368
+ p_score.add_argument(
369
+ "--model", default="", help="vLLM model preset (default: from config)"
370
+ )
371
+ p_score.add_argument(
372
+ "--format",
373
+ choices=["terminal", "json"],
374
+ default="terminal",
375
+ help="Output format (default: terminal)",
376
+ dest="format",
377
+ )
378
+ p_score.add_argument("--config", help="Path to .sciwrite-lint.toml")
379
+ p_score.set_defaults(func=run_contributions)
380
+
381
+ # --- vision ---
382
+ p_vision = sub.add_parser(
383
+ "vision",
384
+ help="Extract and describe manuscript figures via Qwen3-VL-2B (also runs automatically in check pipeline).",
385
+ )
386
+ p_vision.add_argument("--paper", required=True, help="Paper name from [[papers]]")
387
+ p_vision.add_argument(
388
+ "--device",
389
+ choices=["auto", "cpu", "cuda"],
390
+ default="auto",
391
+ help="Device for VL inference (default: auto — CUDA if available)",
392
+ )
393
+ p_vision.add_argument(
394
+ "--fresh",
395
+ action="store_true",
396
+ help="Ignore vision cache and re-describe all figures",
397
+ )
398
+ p_vision.add_argument("--config", help="Path to .sciwrite-lint.toml")
399
+ p_vision.set_defaults(func=run_vision)
400
+
401
+ # --- containers (grobid + vllm together) ---
402
+ p_containers = sub.add_parser(
403
+ "containers", help="Manage GROBID and vLLM containers together"
404
+ )
405
+ p_containers.add_argument(
406
+ "action",
407
+ choices=["start", "stop", "restart", "status", "monitor"],
408
+ help="start/stop/restart/status/monitor of all containers",
409
+ )
410
+ p_containers.add_argument(
411
+ "--model", choices=["qwen3", "gemma3"], help="vLLM model to use"
412
+ )
413
+ p_containers.add_argument(
414
+ "--update",
415
+ action="store_true",
416
+ help="Pull latest container images before starting",
417
+ )
418
+ p_containers.add_argument(
419
+ "-i",
420
+ "--interval",
421
+ type=float,
422
+ default=2,
423
+ help="Monitor refresh interval in seconds (default: 2)",
424
+ )
425
+ p_containers.add_argument(
426
+ "--recreate",
427
+ action="store_true",
428
+ help="Remove and recreate containers (applies config changes like memory limits)",
429
+ )
430
+ p_containers.add_argument("--config", help="Path to .sciwrite-lint.toml")
431
+ p_containers.set_defaults(func=run_containers)
432
+
433
+ # --- grobid ---
434
+ p_grobid = sub.add_parser("grobid", help="Manage GROBID container")
435
+ p_grobid.add_argument(
436
+ "action",
437
+ choices=["start", "stop", "status"],
438
+ help="start/stop/status of GROBID container",
439
+ )
440
+ p_grobid.add_argument("--config", help="Path to .sciwrite-lint.toml")
441
+ p_grobid.set_defaults(func=run_grobid)
442
+
443
+ # --- vllm ---
444
+ p_vllm = sub.add_parser(
445
+ "vllm", help="Manage local vLLM server for LLM-engine rules"
446
+ )
447
+ vllm_sub = p_vllm.add_subparsers(dest="vllm_action")
448
+
449
+ p_vllm_status = vllm_sub.add_parser(
450
+ "status", help="Show vLLM status and API health"
451
+ )
452
+ p_vllm_status.set_defaults(func=run_vllm)
453
+
454
+ p_vllm_start = vllm_sub.add_parser("start", help="Start vLLM server")
455
+ p_vllm_start.add_argument(
456
+ "--model", choices=["qwen3", "gemma3"], help="Model to serve"
457
+ )
458
+ p_vllm_start.add_argument(
459
+ "--update",
460
+ action="store_true",
461
+ help="Pull latest vLLM image before starting",
462
+ )
463
+ p_vllm_start.add_argument("--config", help="Path to .sciwrite-lint.toml")
464
+ p_vllm_start.set_defaults(func=run_vllm)
465
+
466
+ p_vllm_stop = vllm_sub.add_parser("stop", help="Stop vLLM server")
467
+ p_vllm_stop.add_argument(
468
+ "--model", choices=["qwen3", "gemma3"], help="Model to stop"
469
+ )
470
+ p_vllm_stop.set_defaults(func=run_vllm)
471
+
472
+ p_vllm_logs = vllm_sub.add_parser("logs", help="Show vLLM container logs")
473
+ p_vllm_logs.add_argument("--model", choices=["qwen3", "gemma3"])
474
+ p_vllm_logs.add_argument(
475
+ "-f", "--follow", action="store_true", help="Follow log output"
476
+ )
477
+ p_vllm_logs.add_argument("-n", "--tail", type=int, default=50, help="Lines to show")
478
+ p_vllm_logs.set_defaults(func=run_vllm)
479
+
480
+ p_vllm_rm = vllm_sub.add_parser("rm", help="Remove vLLM container")
481
+ p_vllm_rm.add_argument("--model", choices=["qwen3", "gemma3"])
482
+ p_vllm_rm.add_argument(
483
+ "--force", action="store_true", help="Force remove even if running"
484
+ )
485
+ p_vllm_rm.set_defaults(func=run_vllm)
486
+
487
+ p_vllm_monitor = vllm_sub.add_parser(
488
+ "monitor", help="Live terminal monitor for vLLM server"
489
+ )
490
+ p_vllm_monitor.add_argument(
491
+ "-i",
492
+ "--interval",
493
+ type=float,
494
+ default=2,
495
+ help="Refresh interval in seconds (default: 2)",
496
+ )
497
+ p_vllm_monitor.add_argument("--config", help="Path to .sciwrite-lint.toml")
498
+ p_vllm_monitor.set_defaults(func=run_vllm)
499
+
500
+ args = parser.parse_args(argv)
501
+ if not args.command:
502
+ parser.print_help()
503
+ return 0
504
+
505
+ if args.command == "config" and not getattr(args, "config_action", None):
506
+ p_config.print_help()
507
+ return 0
508
+
509
+ if args.command == "vllm" and not getattr(args, "vllm_action", None):
510
+ p_vllm.print_help()
511
+ return 0
512
+
513
+ # Set up file logging from config (stderr sink stays at INFO by default)
514
+ try:
515
+ config = load_config(
516
+ Path(args.config) if getattr(args, "config", None) else None
517
+ )
518
+ except Exception as e:
519
+ logger.debug("Config load failed, using defaults: {}", e)
520
+ config = LintConfig()
521
+ _setup_logging(config)
522
+
523
+ return args.func(args)
524
+
525
+
526
+ if __name__ == "__main__":
527
+ sys.exit(main())