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.
Files changed (57) hide show
  1. graphstack/__init__.py +12 -0
  2. graphstack/__main__.py +10 -0
  3. graphstack/assets/docs/CURSOR_PROMPTS.md +215 -0
  4. graphstack/assets/handoff/BOOTSTRAP.md +73 -0
  5. graphstack/assets/handoff/BRIEF.md +66 -0
  6. graphstack/assets/handoff/REVIEW.md +7 -0
  7. graphstack/assets/handoff/board/README.md +60 -0
  8. graphstack/assets/orchestrator/ORCHESTRATOR.md +416 -0
  9. graphstack/assets/orchestrator/TOKEN_OPTIMIZER.md +319 -0
  10. graphstack/assets/scripts/board.ps1 +37 -0
  11. graphstack/assets/scripts/board.sh +22 -0
  12. graphstack/assets/scripts/gate-hook.ps1 +41 -0
  13. graphstack/assets/scripts/gate-hook.sh +26 -0
  14. graphstack/assets/scripts/post-commit +20 -0
  15. graphstack/assets/scripts/post-commit.ps1 +44 -0
  16. graphstack/board.py +361 -0
  17. graphstack/bootstrap.py +50 -0
  18. graphstack/cli.py +99 -0
  19. graphstack/compact/__init__.py +9 -0
  20. graphstack/compact/__pycache__/__init__.cpython-311.pyc +0 -0
  21. graphstack/compact/__pycache__/base.cpython-311.pyc +0 -0
  22. graphstack/compact/__pycache__/generic.cpython-311.pyc +0 -0
  23. graphstack/compact/__pycache__/git.cpython-311.pyc +0 -0
  24. graphstack/compact/__pycache__/registry.cpython-311.pyc +0 -0
  25. graphstack/compact/base.py +115 -0
  26. graphstack/compact/generic.py +90 -0
  27. graphstack/compact/git.py +167 -0
  28. graphstack/compact/registry.py +47 -0
  29. graphstack/constants.py +38 -0
  30. graphstack/gate.py +429 -0
  31. graphstack/graph.py +143 -0
  32. graphstack/hook.py +144 -0
  33. graphstack/init_cmd.py +113 -0
  34. graphstack/installer.py +366 -0
  35. graphstack/platform_utils.py +127 -0
  36. graphstack/run.py +103 -0
  37. graphstack/state.py +117 -0
  38. graphstack/tests/__init__.py +0 -0
  39. graphstack/tests/conftest.py +30 -0
  40. graphstack/tests/test_assets.py +35 -0
  41. graphstack/tests/test_board.py +166 -0
  42. graphstack/tests/test_compact.py +93 -0
  43. graphstack/tests/test_gate.py +406 -0
  44. graphstack/tests/test_graph.py +60 -0
  45. graphstack/tests/test_hook.py +57 -0
  46. graphstack/tests/test_init.py +58 -0
  47. graphstack/tests/test_installer.py +73 -0
  48. graphstack/tests/test_platform_utils.py +69 -0
  49. graphstack/tests/test_state.py +56 -0
  50. graphstack/tests/test_validate.py +204 -0
  51. graphstack/validate.py +469 -0
  52. mertcapkin_graphstack-4.5.1.dist-info/METADATA +720 -0
  53. mertcapkin_graphstack-4.5.1.dist-info/RECORD +57 -0
  54. mertcapkin_graphstack-4.5.1.dist-info/WHEEL +5 -0
  55. mertcapkin_graphstack-4.5.1.dist-info/entry_points.txt +2 -0
  56. mertcapkin_graphstack-4.5.1.dist-info/licenses/LICENSE +21 -0
  57. 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
@@ -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")