runspec-logops 0.1.0__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.
@@ -0,0 +1,61 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .eggs/
12
+ .venv/
13
+ venv/
14
+ env/
15
+ .env
16
+ pip-wheel-metadata/
17
+ .pytest_cache/
18
+ .mypy_cache/
19
+ .ruff_cache/
20
+ htmlcov/
21
+ .coverage
22
+ coverage.xml
23
+ *.cover
24
+
25
+ # Node
26
+ node_modules/
27
+ dist/
28
+ *.js.map
29
+ .npm
30
+
31
+ # Go
32
+ *.exe
33
+ *.test
34
+ *.out
35
+ vendor/
36
+
37
+ # IDE
38
+ .idea/
39
+ .vscode/
40
+ *.iml
41
+ *.iws
42
+ *.ipr
43
+ .DS_Store
44
+ Thumbs.db
45
+
46
+ # Docs
47
+ site/
48
+
49
+ # Misc
50
+ *.log
51
+ *.tmp
52
+
53
+ # External reference repos (cloned locally, not committed)
54
+ chainlit-docs/
55
+ .chainlit/
56
+
57
+ # Claude Code local config (machine-specific)
58
+ .claude/launch.json
59
+
60
+ # Stray committed test venv (removed from tracking)
61
+ .venv-test/
@@ -0,0 +1,24 @@
1
+ # runspec-logops Changelog
2
+
3
+ ## [0.1.0] — 2026-06-18
4
+
5
+ Initial release.
6
+
7
+ Three runnables that let an agent triage application issues from a log file
8
+ cheaply — the runnables do the heavy lifting on the host where the log lives and
9
+ return small structured JSON, so the agent reads condensed conclusions, not raw
10
+ data:
11
+
12
+ - **`summarize-log`** (autonomous) — streams a log file and clusters repeated
13
+ errors by signature, returning the top-N distinct issues with counts,
14
+ first/last timestamp and one bounded sample each. Output size is bounded
15
+ regardless of input size. Supports a severity floor (`--level`) and a time
16
+ window (`--since`/`--until`).
17
+ - **`map-trace`** (autonomous) — resolves a stack trace, or a `summarize-log`
18
+ signature, to the few relevant source snippets in a checkout (`--repo`).
19
+ - **`bundle-digest`** (confirm) — summarises a log, optionally maps a trace to
20
+ code, and packs digest + snippets + manifest into one small zip for transfer.
21
+
22
+ Thin wrappers over `runspec-logops-core`. To wrap the logic privately (bake in
23
+ corporate paths/defaults, surface only your own runnables), depend on
24
+ `runspec-logops-core` directly rather than installing this package.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: runspec-logops
3
+ Version: 0.1.0
4
+ Summary: Log-condensing + code-mapping runnables for runspec — cheap application-support triage from a log file
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: runspec-logops-core>=0.1.0
7
+ Requires-Dist: runspec>=0.27.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: mypy; extra == 'dev'
10
+ Requires-Dist: pytest>=8.0; extra == 'dev'
11
+ Requires-Dist: ruff; extra == 'dev'
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "runspec-logops"
7
+ version = "0.1.0"
8
+ requires-python = ">=3.10"
9
+ description = "Log-condensing + code-mapping runnables for runspec — cheap application-support triage from a log file"
10
+ dependencies = [
11
+ "runspec>=0.27.0",
12
+ "runspec-logops-core>=0.1.0",
13
+ ]
14
+
15
+ [project.scripts]
16
+ summarize-log = "runspec_logops.digest:main_summarize_log"
17
+ map-trace = "runspec_logops.codemap:main_map_trace"
18
+ bundle-digest = "runspec_logops.bundle:main_bundle_digest"
19
+
20
+ [project.optional-dependencies]
21
+ dev = [
22
+ "ruff",
23
+ "mypy",
24
+ "pytest>=8.0",
25
+ ]
26
+
27
+ [tool.pytest.ini_options]
28
+ testpaths = ["tests"]
29
+
30
+ [tool.mypy]
31
+ python_version = "3.10"
32
+
33
+ [tool.ruff]
34
+ line-length = 200
35
+ target-version = "py310"
36
+
37
+ [tool.ruff.lint]
38
+ select = ["E", "F", "I", "UP", "B", "SIM"]
@@ -0,0 +1,21 @@
1
+ """runspec-logops — log-condensing + code-mapping runnables.
2
+
3
+ The pure logic lives in the dependency-free ``runspec-logops-core`` package; the
4
+ modules here wrap each helper in a runspec runnable. The helper functions are
5
+ re-exported for convenience and back-compat (e.g. ``from runspec_logops import
6
+ summarize_log``). To import the helpers *without* surfacing any runnables in a
7
+ venv — the corporate-wrapping path — depend on ``runspec-logops-core`` directly
8
+ instead.
9
+ """
10
+
11
+ from runspec_logops_core import (
12
+ build_bundle,
13
+ map_trace_to_sources,
14
+ summarize_log,
15
+ )
16
+
17
+ __all__ = [
18
+ "summarize_log",
19
+ "map_trace_to_sources",
20
+ "build_bundle",
21
+ ]
@@ -0,0 +1,30 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+ from runspec_logops_core import build_bundle, map_trace_to_sources, summarize_log
6
+ from runspec_logops_core.errors import SourceNotFoundError
7
+
8
+
9
+ def main_bundle_digest() -> None:
10
+ spec = rs.parse("bundle-digest")
11
+ file_path = str(spec.file)
12
+ # Unset optionals resolve to an Arg wrapping value None; read .value so the
13
+ # absent case is a real None rather than the string "None".
14
+ repo = str(spec.repo) if spec.repo.value is not None else None
15
+ trace = str(spec.trace) if spec.trace.value is not None else None
16
+ destination = str(spec.destination)
17
+ top = int(spec.top)
18
+ try:
19
+ digest = summarize_log(file_path, top=top)
20
+ code_map = None
21
+ if trace and repo:
22
+ code_map = map_trace_to_sources(repo, trace)
23
+ result = build_bundle(digest, code_map, dest_dir=destination, metadata={"trace": trace})
24
+ print(json.dumps(result))
25
+ except SourceNotFoundError as e:
26
+ print(json.dumps({"error": str(e), "file": file_path}))
27
+ sys.exit(1)
28
+ except OSError as e:
29
+ print(json.dumps({"error": str(e), "file": file_path}))
30
+ sys.exit(1)
@@ -0,0 +1,23 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+ from runspec_logops_core import map_trace_to_sources
6
+ from runspec_logops_core.errors import SourceNotFoundError
7
+
8
+
9
+ def main_map_trace() -> None:
10
+ spec = rs.parse("map-trace")
11
+ repo = str(spec.repo)
12
+ trace = str(spec.trace)
13
+ context = int(spec.context)
14
+ max_files = int(spec.max_files)
15
+ try:
16
+ result = map_trace_to_sources(repo, trace, context=context, max_files=max_files)
17
+ print(json.dumps(result))
18
+ except SourceNotFoundError as e:
19
+ print(json.dumps({"error": str(e), "repo": repo}))
20
+ sys.exit(1)
21
+ except OSError as e:
22
+ print(json.dumps({"error": str(e), "repo": repo}))
23
+ sys.exit(1)
@@ -0,0 +1,34 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+ from runspec_logops_core import summarize_log
6
+ from runspec_logops_core.errors import SourceNotFoundError
7
+
8
+
9
+ def main_summarize_log() -> None:
10
+ spec = rs.parse("summarize-log")
11
+ file_path = str(spec.file)
12
+ level = str(spec.level)
13
+ # Unset optionals resolve to an Arg wrapping value None; read .value so the
14
+ # absent case is a real None rather than the string "None".
15
+ since = spec.since.value
16
+ until = spec.until.value
17
+ top = int(spec.top)
18
+ max_sample_lines = int(spec.max_sample_lines)
19
+ try:
20
+ digest = summarize_log(
21
+ file_path,
22
+ level=level,
23
+ since=since,
24
+ until=until,
25
+ top=top,
26
+ max_sample_lines=max_sample_lines,
27
+ )
28
+ print(json.dumps(digest))
29
+ except SourceNotFoundError as e:
30
+ print(json.dumps({"error": str(e), "file": file_path}))
31
+ sys.exit(1)
32
+ except OSError as e:
33
+ print(json.dumps({"error": str(e), "file": file_path}))
34
+ sys.exit(1)
@@ -0,0 +1,111 @@
1
+ [config]
2
+ autonomy-default = "autonomous"
3
+
4
+ [config.logging]
5
+ # Per-invocation log files: one {runnable}.{utc-ts}.{run_id}.log per run.
6
+ # Multi-writer safe for shared venvs (use `runspec logs` to view/compact/prune).
7
+ store = "per-run"
8
+
9
+ # ── Log condensing ─────────────────────────────────────────────────────────────
10
+
11
+ [summarize-log]
12
+ serve = ["local"]
13
+ description = "Condense a log file into a small digest: cluster repeated errors by signature and return the top-N distinct issues with counts, first/last timestamp and one sample each. The digest size is bounded regardless of how big the log is, so read this instead of the raw log."
14
+ autonomy = "autonomous"
15
+ output = "json"
16
+
17
+ [summarize-log.args.file]
18
+ type = "path"
19
+ short = "-f"
20
+ description = "Path to the log file to summarise"
21
+
22
+ [summarize-log.args.level]
23
+ type = "choice"
24
+ options = ["all", "warning", "error", "critical"]
25
+ default = "all"
26
+ description = "Only include events at or above this severity"
27
+
28
+ [summarize-log.args.since]
29
+ type = "str"
30
+ required = false
31
+ description = "Window start: ISO timestamp, 'now', or '<n> <unit> ago' (e.g. '2 hours ago')"
32
+
33
+ [summarize-log.args.until]
34
+ type = "str"
35
+ required = false
36
+ description = "Window end: ISO timestamp, 'now', or '<n> <unit> ago'"
37
+
38
+ [summarize-log.args.top]
39
+ type = "int"
40
+ short = "-n"
41
+ default = 10
42
+ description = "Number of distinct signatures to return (most frequent first)"
43
+
44
+ [summarize-log.args.max-sample-lines]
45
+ type = "int"
46
+ default = 20
47
+ description = "Max lines of the sample kept per signature"
48
+
49
+ # ── Trace → code ────────────────────────────────────────────────────────────────
50
+
51
+ [map-trace]
52
+ serve = ["local"]
53
+ description = "Map a stack trace (or a summarize-log signature like 'NullPointerException@com.acme.OrderSvc.price') to the few relevant source snippets in a checkout. Returns only the lines around each frame, never the whole repo."
54
+ autonomy = "autonomous"
55
+ output = "json"
56
+
57
+ [map-trace.args.repo]
58
+ type = "path"
59
+ short = "-r"
60
+ description = "Path to the source checkout to search"
61
+
62
+ [map-trace.args.trace]
63
+ type = "str"
64
+ short = "-t"
65
+ description = "The stack trace text, or a signature emitted by summarize-log"
66
+
67
+ [map-trace.args.context]
68
+ type = "int"
69
+ default = 8
70
+ description = "Lines of context to include around each resolved frame"
71
+
72
+ [map-trace.args.max-files]
73
+ type = "int"
74
+ default = 5
75
+ description = "Maximum number of frames/files to resolve"
76
+
77
+ # ── Bundle for transfer ─────────────────────────────────────────────────────────
78
+
79
+ [bundle-digest]
80
+ serve = ["local"]
81
+ description = "Summarise a log and (optionally) map a trace to code, then pack the digest + snippets + manifest into one small zip ready for transfer to the local machine. Writes a file."
82
+ autonomy = "confirm"
83
+ output = "json"
84
+
85
+ [bundle-digest.args.file]
86
+ type = "path"
87
+ short = "-f"
88
+ description = "Path to the log file"
89
+
90
+ [bundle-digest.args.repo]
91
+ type = "path"
92
+ short = "-r"
93
+ required = false
94
+ description = "Path to the source checkout for the optional code map (omit for a digest-only bundle)"
95
+
96
+ [bundle-digest.args.trace]
97
+ type = "str"
98
+ required = false
99
+ description = "Optional trace/signature to map to code; omit for a digest-only bundle"
100
+
101
+ [bundle-digest.args.destination]
102
+ type = "path"
103
+ short = "-d"
104
+ default = "/tmp"
105
+ description = "Directory to write the zip into"
106
+
107
+ [bundle-digest.args.top]
108
+ type = "int"
109
+ short = "-n"
110
+ default = 10
111
+ description = "Number of distinct signatures to include in the digest"
File without changes
@@ -0,0 +1,56 @@
1
+ """Smoke tests: each runnable parses args, calls its core helper, prints JSON.
2
+
3
+ The runnables are exercised end-to-end via their installed console scripts (the
4
+ same path `runspec serve` invokes), so this also proves the runspec.toml ships in
5
+ the package and resolves.
6
+ """
7
+
8
+ import json
9
+ import shutil
10
+ import subprocess
11
+
12
+ import pytest
13
+
14
+
15
+ def _run(script: str, *args: str) -> dict:
16
+ exe = shutil.which(script)
17
+ if not exe:
18
+ pytest.skip(f"{script} console script not installed")
19
+ proc = subprocess.run([exe, *args], capture_output=True, text=True)
20
+ assert proc.returncode == 0, proc.stderr
21
+ return json.loads(proc.stdout)
22
+
23
+
24
+ def test_helpers_reexported() -> None:
25
+ from runspec_logops_core import summarize_log as core_summarize
26
+
27
+ from runspec_logops import build_bundle, map_trace_to_sources, summarize_log
28
+
29
+ assert summarize_log is core_summarize
30
+ assert callable(map_trace_to_sources) and callable(build_bundle)
31
+
32
+
33
+ def test_summarize_log_runnable(tmp_path) -> None:
34
+ log = tmp_path / "app.log"
35
+ log.write_text("\n".join(["2026-06-18T08:00:00Z ERROR NullPointerException: boom\n\tat com.acme.Svc.go(Svc.java:9)"] * 5 + ["2026-06-18T08:01:00Z INFO ok"]) + "\n")
36
+ out = _run("summarize-log", "--file", str(log), "-n", "5")
37
+ assert out["file"] == str(log)
38
+ assert out["top"][0]["count"] == 5
39
+ assert out["top"][0]["signature"].startswith("NullPointerException@")
40
+
41
+
42
+ def test_map_trace_runnable(tmp_path) -> None:
43
+ (tmp_path / "app.py").write_text("\n".join(f"line {i}" for i in range(1, 30)) + "\n")
44
+ trace = 'File "app.py", line 10, in handler'
45
+ out = _run("map-trace", "--repo", str(tmp_path), "--trace", trace, "--context", "2")
46
+ assert out["files_matched"] == 1
47
+ assert out["frames"][0]["file"] == "app.py"
48
+
49
+
50
+ def test_bundle_digest_runnable(tmp_path) -> None:
51
+ log = tmp_path / "app.log"
52
+ log.write_text("2026-06-18T08:00:00Z ERROR ValueError: bad\n")
53
+ dest = tmp_path / "out"
54
+ out = _run("bundle-digest", "--file", str(log), "--destination", str(dest))
55
+ assert out["destination"].endswith(".zip")
56
+ assert "digest.json" in out["contents"]