MertCapkin-GraphStack 4.5.1__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.
- graphstack/__init__.py +12 -0
- graphstack/__main__.py +10 -0
- graphstack/assets/docs/CURSOR_PROMPTS.md +215 -0
- graphstack/assets/handoff/BOOTSTRAP.md +73 -0
- graphstack/assets/handoff/BRIEF.md +66 -0
- graphstack/assets/handoff/REVIEW.md +7 -0
- graphstack/assets/handoff/board/README.md +60 -0
- graphstack/assets/orchestrator/ORCHESTRATOR.md +416 -0
- graphstack/assets/orchestrator/TOKEN_OPTIMIZER.md +319 -0
- graphstack/assets/scripts/board.ps1 +37 -0
- graphstack/assets/scripts/board.sh +22 -0
- graphstack/assets/scripts/gate-hook.ps1 +41 -0
- graphstack/assets/scripts/gate-hook.sh +26 -0
- graphstack/assets/scripts/post-commit +20 -0
- graphstack/assets/scripts/post-commit.ps1 +44 -0
- graphstack/board.py +361 -0
- graphstack/bootstrap.py +50 -0
- graphstack/cli.py +99 -0
- graphstack/compact/__init__.py +9 -0
- graphstack/compact/__pycache__/__init__.cpython-311.pyc +0 -0
- graphstack/compact/__pycache__/base.cpython-311.pyc +0 -0
- graphstack/compact/__pycache__/generic.cpython-311.pyc +0 -0
- graphstack/compact/__pycache__/git.cpython-311.pyc +0 -0
- graphstack/compact/__pycache__/registry.cpython-311.pyc +0 -0
- graphstack/compact/base.py +115 -0
- graphstack/compact/generic.py +90 -0
- graphstack/compact/git.py +167 -0
- graphstack/compact/registry.py +47 -0
- graphstack/constants.py +38 -0
- graphstack/gate.py +429 -0
- graphstack/graph.py +143 -0
- graphstack/hook.py +144 -0
- graphstack/init_cmd.py +113 -0
- graphstack/installer.py +366 -0
- graphstack/platform_utils.py +127 -0
- graphstack/run.py +103 -0
- graphstack/state.py +117 -0
- graphstack/tests/__init__.py +0 -0
- graphstack/tests/conftest.py +30 -0
- graphstack/tests/test_assets.py +35 -0
- graphstack/tests/test_board.py +166 -0
- graphstack/tests/test_compact.py +93 -0
- graphstack/tests/test_gate.py +406 -0
- graphstack/tests/test_graph.py +60 -0
- graphstack/tests/test_hook.py +57 -0
- graphstack/tests/test_init.py +58 -0
- graphstack/tests/test_installer.py +73 -0
- graphstack/tests/test_platform_utils.py +69 -0
- graphstack/tests/test_state.py +56 -0
- graphstack/tests/test_validate.py +204 -0
- graphstack/validate.py +469 -0
- mertcapkin_graphstack-4.5.1.dist-info/METADATA +720 -0
- mertcapkin_graphstack-4.5.1.dist-info/RECORD +57 -0
- mertcapkin_graphstack-4.5.1.dist-info/WHEEL +5 -0
- mertcapkin_graphstack-4.5.1.dist-info/entry_points.txt +2 -0
- mertcapkin_graphstack-4.5.1.dist-info/licenses/LICENSE +21 -0
- mertcapkin_graphstack-4.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Generic compaction for test runners and unknown commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from .base import (
|
|
8
|
+
_DEFAULT_MAX_LINES,
|
|
9
|
+
dedupe_consecutive,
|
|
10
|
+
is_critical_line,
|
|
11
|
+
truncate_preserving_critical,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
_PYTEST_SUMMARY_RE = re.compile(
|
|
15
|
+
r"(?i)=+\s*(FAILURES|ERRORS|short test summary|passed|failed)\s*=+"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def compact_pytest(text: str) -> str:
|
|
20
|
+
lines = text.splitlines()
|
|
21
|
+
if not lines:
|
|
22
|
+
return ""
|
|
23
|
+
|
|
24
|
+
keep: list[str] = []
|
|
25
|
+
in_failure_block = False
|
|
26
|
+
failure_blocks: list[list[str]] = []
|
|
27
|
+
current_block: list[str] = []
|
|
28
|
+
|
|
29
|
+
for line in lines:
|
|
30
|
+
if _PYTEST_SUMMARY_RE.search(line) or line.startswith("FAILED ") or line.startswith("ERROR "):
|
|
31
|
+
in_failure_block = True
|
|
32
|
+
if in_failure_block:
|
|
33
|
+
current_block.append(line)
|
|
34
|
+
if line.strip() == "" and len(current_block) > 3:
|
|
35
|
+
failure_blocks.append(current_block)
|
|
36
|
+
current_block = []
|
|
37
|
+
in_failure_block = False
|
|
38
|
+
continue
|
|
39
|
+
if is_critical_line(line):
|
|
40
|
+
keep.append(line)
|
|
41
|
+
elif "passed" in line.lower() and "failed" in line.lower():
|
|
42
|
+
keep.append(line)
|
|
43
|
+
|
|
44
|
+
if current_block:
|
|
45
|
+
failure_blocks.append(current_block)
|
|
46
|
+
|
|
47
|
+
# Last lines often hold the summary
|
|
48
|
+
tail = lines[-15:] if len(lines) > 15 else lines
|
|
49
|
+
for line in tail:
|
|
50
|
+
if line not in keep and (is_critical_line(line) or "passed" in line.lower()):
|
|
51
|
+
keep.append(line)
|
|
52
|
+
|
|
53
|
+
out: list[str] = []
|
|
54
|
+
for block in failure_blocks[-5:]:
|
|
55
|
+
out.extend(block[:40])
|
|
56
|
+
out.extend(keep)
|
|
57
|
+
out = dedupe_consecutive(out)
|
|
58
|
+
|
|
59
|
+
if len(out) > _DEFAULT_MAX_LINES:
|
|
60
|
+
out, omitted = truncate_preserving_critical(out)
|
|
61
|
+
out.append(f"... [{omitted} lines omitted — use: graphstack run --raw -- pytest ...]")
|
|
62
|
+
|
|
63
|
+
if not out:
|
|
64
|
+
return compact_generic(text)
|
|
65
|
+
return "\n".join(out)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def compact_generic(text: str, *, max_lines: int = _DEFAULT_MAX_LINES) -> str:
|
|
69
|
+
lines = text.splitlines()
|
|
70
|
+
if not lines:
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
# Strip obvious noise (progress bars, download meters)
|
|
74
|
+
filtered: list[str] = []
|
|
75
|
+
for line in lines:
|
|
76
|
+
if re.search(r"[\|/#\-]{4,}.*\d+%", line):
|
|
77
|
+
continue
|
|
78
|
+
if re.match(r"^\s*$", line) and filtered and filtered[-1] == "":
|
|
79
|
+
continue
|
|
80
|
+
filtered.append(line)
|
|
81
|
+
|
|
82
|
+
filtered = dedupe_consecutive(filtered)
|
|
83
|
+
if len(filtered) <= max_lines:
|
|
84
|
+
return "\n".join(filtered)
|
|
85
|
+
|
|
86
|
+
trimmed, omitted = truncate_preserving_critical(filtered, max_lines=max_lines)
|
|
87
|
+
trimmed.append(
|
|
88
|
+
f"... [{omitted} lines omitted — use: graphstack run --raw -- <command> for full output]"
|
|
89
|
+
)
|
|
90
|
+
return "\n".join(trimmed)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Git command output compactors — preserve paths, branch, and diff hunks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from .base import (
|
|
7
|
+
_DEFAULT_MAX_LINES,
|
|
8
|
+
dedupe_consecutive,
|
|
9
|
+
is_critical_line,
|
|
10
|
+
truncate_preserving_critical,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
_BRANCH_RE = re.compile(r"^On branch (.+)$|^HEAD detached at (.+)$", re.MULTILINE)
|
|
14
|
+
_AHEAD_BEHIND_RE = re.compile(
|
|
15
|
+
r"Your branch is (ahead of|behind) [^\s]+ by (\d+) commit",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def compact_git_status(text: str) -> str:
|
|
20
|
+
porcelain = _try_porcelain_status(text)
|
|
21
|
+
if porcelain is not None:
|
|
22
|
+
return porcelain
|
|
23
|
+
|
|
24
|
+
lines = text.splitlines()
|
|
25
|
+
out: list[str] = []
|
|
26
|
+
for line in lines:
|
|
27
|
+
if _BRANCH_RE.match(line) or _AHEAD_BEHIND_RE.search(line):
|
|
28
|
+
out.append(line.strip())
|
|
29
|
+
elif line.startswith("nothing to commit"):
|
|
30
|
+
out.append(line.strip())
|
|
31
|
+
elif line.strip().startswith(("modified:", "new file:", "deleted:", "renamed:")):
|
|
32
|
+
out.append(line.strip())
|
|
33
|
+
elif line.strip() and (
|
|
34
|
+
line.startswith("\t") or line.startswith(" ") or ":" in line[:40]
|
|
35
|
+
):
|
|
36
|
+
out.append(line.strip())
|
|
37
|
+
|
|
38
|
+
if not out:
|
|
39
|
+
return text.strip()
|
|
40
|
+
|
|
41
|
+
grouped = dedupe_consecutive(out)
|
|
42
|
+
if len(grouped) > _DEFAULT_MAX_LINES:
|
|
43
|
+
grouped, omitted = truncate_preserving_critical(grouped)
|
|
44
|
+
grouped.append(f"... [{omitted} lines omitted — use: graphstack run --raw -- git status]")
|
|
45
|
+
return "\n".join(grouped)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _try_porcelain_status(text: str) -> str | None:
|
|
49
|
+
lines = [ln for ln in text.splitlines() if ln.strip()]
|
|
50
|
+
if not lines:
|
|
51
|
+
return None
|
|
52
|
+
if not all(len(ln) >= 3 and ln[2] in (" ", "?") for ln in lines if not ln.startswith("#")):
|
|
53
|
+
# Heuristic: porcelain lines are XY + space + path
|
|
54
|
+
xy_lines = [ln for ln in lines if len(ln) >= 4 and ln[2] == " "]
|
|
55
|
+
if len(xy_lines) < len(lines) * 0.5:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
branch_lines = [ln[2:].strip() for ln in lines if ln.startswith("## ")]
|
|
59
|
+
staged: list[str] = []
|
|
60
|
+
unstaged: list[str] = []
|
|
61
|
+
untracked: list[str] = []
|
|
62
|
+
|
|
63
|
+
for line in lines:
|
|
64
|
+
if line.startswith("#") or line.startswith("##"):
|
|
65
|
+
continue
|
|
66
|
+
if len(line) < 4:
|
|
67
|
+
continue
|
|
68
|
+
xy, path = line[:2], line[3:].strip()
|
|
69
|
+
if xy == "??":
|
|
70
|
+
untracked.append(path)
|
|
71
|
+
elif xy[0] != " ":
|
|
72
|
+
staged.append(path)
|
|
73
|
+
elif xy[1] != " ":
|
|
74
|
+
unstaged.append(path)
|
|
75
|
+
|
|
76
|
+
parts: list[str] = []
|
|
77
|
+
if branch_lines:
|
|
78
|
+
parts.append(branch_lines[0])
|
|
79
|
+
if staged:
|
|
80
|
+
parts.append(f"staged ({len(staged)}): " + ", ".join(_limit_paths(staged)))
|
|
81
|
+
if unstaged:
|
|
82
|
+
parts.append(f"unstaged ({len(unstaged)}): " + ", ".join(_limit_paths(unstaged)))
|
|
83
|
+
if untracked:
|
|
84
|
+
parts.append(f"untracked ({len(untracked)}): " + ", ".join(_limit_paths(untracked)))
|
|
85
|
+
if not parts:
|
|
86
|
+
return None
|
|
87
|
+
return "\n".join(parts)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _limit_paths(paths: list[str], limit: int = 40) -> list[str]:
|
|
91
|
+
if len(paths) <= limit:
|
|
92
|
+
return paths
|
|
93
|
+
head = paths[:limit]
|
|
94
|
+
head.append(f"... +{len(paths) - limit} more")
|
|
95
|
+
return head
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def compact_git_diff(text: str, *, max_lines: int = 150) -> str:
|
|
99
|
+
lines = text.splitlines()
|
|
100
|
+
if len(lines) <= max_lines:
|
|
101
|
+
return text.strip()
|
|
102
|
+
|
|
103
|
+
# Always keep file headers and hunk headers; preserve +/- lines in each hunk
|
|
104
|
+
kept: list[str] = []
|
|
105
|
+
hunk_lines: list[str] = []
|
|
106
|
+
omitted = 0
|
|
107
|
+
|
|
108
|
+
def flush_hunk() -> None:
|
|
109
|
+
nonlocal omitted
|
|
110
|
+
if not hunk_lines:
|
|
111
|
+
return
|
|
112
|
+
if len(kept) + len(hunk_lines) > max_lines and len(hunk_lines) > 30:
|
|
113
|
+
# Keep hunk header + first/last change lines
|
|
114
|
+
header = [hunk_lines[0]] if hunk_lines[0].startswith("@@") else []
|
|
115
|
+
changes = [
|
|
116
|
+
ln
|
|
117
|
+
for ln in hunk_lines
|
|
118
|
+
if ln.startswith(("+", "-")) and not ln.startswith(("+++", "---"))
|
|
119
|
+
]
|
|
120
|
+
body = header + changes[:12] + (["..."] if len(changes) > 12 else []) + changes[-6:]
|
|
121
|
+
kept.extend(body)
|
|
122
|
+
omitted += len(hunk_lines) - len(body)
|
|
123
|
+
else:
|
|
124
|
+
kept.extend(hunk_lines)
|
|
125
|
+
|
|
126
|
+
for line in lines:
|
|
127
|
+
if line.startswith("diff --git") or line.startswith("--- ") or line.startswith("+++ "):
|
|
128
|
+
flush_hunk()
|
|
129
|
+
hunk_lines = []
|
|
130
|
+
kept.append(line)
|
|
131
|
+
elif line.startswith("@@"):
|
|
132
|
+
flush_hunk()
|
|
133
|
+
hunk_lines = [line]
|
|
134
|
+
else:
|
|
135
|
+
hunk_lines.append(line)
|
|
136
|
+
flush_hunk()
|
|
137
|
+
|
|
138
|
+
if omitted:
|
|
139
|
+
kept.append(
|
|
140
|
+
f"... [{omitted} diff lines omitted — use: graphstack run --raw -- git diff]"
|
|
141
|
+
)
|
|
142
|
+
return "\n".join(kept)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def compact_git_log(text: str, *, max_entries: int = 30) -> str:
|
|
146
|
+
lines = [ln for ln in text.splitlines() if ln.strip()]
|
|
147
|
+
if len(lines) <= max_entries:
|
|
148
|
+
return "\n".join(lines)
|
|
149
|
+
|
|
150
|
+
compacted: list[str] = []
|
|
151
|
+
for line in lines[:max_entries]:
|
|
152
|
+
if re.match(r"^[0-9a-f]{7,40}\s", line):
|
|
153
|
+
compacted.append(line)
|
|
154
|
+
elif re.match(r"^commit [0-9a-f]{40}", line):
|
|
155
|
+
continue # skip full commit header blocks when verbose
|
|
156
|
+
elif line.startswith("Author:") or line.startswith("Date:"):
|
|
157
|
+
continue
|
|
158
|
+
elif is_critical_line(line):
|
|
159
|
+
compacted.append(line)
|
|
160
|
+
else:
|
|
161
|
+
compacted.append(line[:120])
|
|
162
|
+
|
|
163
|
+
compacted.append(
|
|
164
|
+
f"... [{len(lines) - max_entries} older entries omitted — "
|
|
165
|
+
"use: graphstack run --raw -- git log ...]"
|
|
166
|
+
)
|
|
167
|
+
return "\n".join(compacted)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Route argv to the right compactor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .base import CompactResult, safe_compact
|
|
6
|
+
from .generic import compact_generic, compact_pytest
|
|
7
|
+
from .git import compact_git_diff, compact_git_log, compact_git_status
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _normalize_argv(argv: list[str]) -> list[str]:
|
|
11
|
+
return [a for a in argv if a]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def compact_command_output(argv: list[str], raw_stdout: str, raw_stderr: str = "") -> CompactResult:
|
|
15
|
+
"""Compact *raw_stdout* for the given command argv. stderr is appended verbatim."""
|
|
16
|
+
argv = _normalize_argv(argv)
|
|
17
|
+
combined_for_match = " ".join(argv).lower()
|
|
18
|
+
stdout = raw_stdout or ""
|
|
19
|
+
|
|
20
|
+
if not argv:
|
|
21
|
+
return CompactResult(stdout.rstrip("\n"), "passthrough", fell_back_to_raw=True)
|
|
22
|
+
|
|
23
|
+
name = argv[0].lower()
|
|
24
|
+
sub = argv[1].lower() if len(argv) > 1 else ""
|
|
25
|
+
|
|
26
|
+
if name in ("git", "git.exe") and sub == "status":
|
|
27
|
+
compacted = compact_git_status(stdout)
|
|
28
|
+
result = safe_compact(stdout, "git-status", compacted)
|
|
29
|
+
elif name in ("git", "git.exe") and sub == "diff":
|
|
30
|
+
compacted = compact_git_diff(stdout)
|
|
31
|
+
result = safe_compact(stdout, "git-diff", compacted)
|
|
32
|
+
elif name in ("git", "git.exe") and sub in ("log", "reflog"):
|
|
33
|
+
compacted = compact_git_log(stdout)
|
|
34
|
+
result = safe_compact(stdout, "git-log", compacted)
|
|
35
|
+
elif name in ("pytest", "pytest.exe") or "pytest" in combined_for_match:
|
|
36
|
+
compacted = compact_pytest(stdout)
|
|
37
|
+
result = safe_compact(stdout, "pytest", compacted)
|
|
38
|
+
else:
|
|
39
|
+
compacted = compact_generic(stdout)
|
|
40
|
+
result = safe_compact(stdout, "generic", compacted)
|
|
41
|
+
|
|
42
|
+
if raw_stderr.strip():
|
|
43
|
+
suffix = raw_stderr.rstrip("\n")
|
|
44
|
+
text = result.text + ("\n" if result.text else "") + suffix
|
|
45
|
+
return CompactResult(text, result.used_compactor, result.fell_back_to_raw)
|
|
46
|
+
|
|
47
|
+
return result
|
graphstack/constants.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Path and configuration constants used across the package.
|
|
2
|
+
|
|
3
|
+
Paths are resolved relative to the *current working directory* (the project
|
|
4
|
+
root) at call time — never module import time. This keeps the package
|
|
5
|
+
testable in temporary directories.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
HANDOFF_DIR = Path("handoff")
|
|
13
|
+
BOARD_DIR = HANDOFF_DIR / "board"
|
|
14
|
+
TODO_DIR = BOARD_DIR / "todo"
|
|
15
|
+
DOING_DIR = BOARD_DIR / "doing"
|
|
16
|
+
DONE_DIR = BOARD_DIR / "done"
|
|
17
|
+
|
|
18
|
+
GRAPHIFY_OUT = Path("graphify-out")
|
|
19
|
+
GRAPH_REPORT = GRAPHIFY_OUT / "GRAPH_REPORT.md"
|
|
20
|
+
GRAPH_JSON = GRAPHIFY_OUT / "graph.json"
|
|
21
|
+
GRAPH_HTML = GRAPHIFY_OUT / "graph.html"
|
|
22
|
+
|
|
23
|
+
STATE_JSON = HANDOFF_DIR / "STATE.json"
|
|
24
|
+
GATE_OFF_FILE = HANDOFF_DIR / ".gate-off"
|
|
25
|
+
|
|
26
|
+
# Paths that never count as "code" for the process gate. Anything else
|
|
27
|
+
# (plus root-level *.md files, handled in gate.is_code_path) is gated.
|
|
28
|
+
NON_CODE_PREFIXES = (
|
|
29
|
+
"handoff/",
|
|
30
|
+
"graphify-out/",
|
|
31
|
+
".cursor/",
|
|
32
|
+
".claude/",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
STALE_GRAPH_HOURS = 24
|
|
36
|
+
|
|
37
|
+
# Required keys for GNAP board task JSON files (see handoff/board/README.md).
|
|
38
|
+
TASK_REQUIRED_KEYS = ("id", "title", "status", "created_at")
|