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,69 @@
1
+ """Tests for the OS helpers in ``platform_utils``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ import pytest
8
+
9
+ from graphstack import platform_utils
10
+
11
+
12
+ def test_find_python_returns_non_empty_list() -> None:
13
+ result = platform_utils.find_python()
14
+ assert isinstance(result, list)
15
+ assert len(result) >= 1
16
+ assert all(isinstance(s, str) and s for s in result)
17
+
18
+
19
+ def test_find_python_falls_back_to_sys_executable(monkeypatch: pytest.MonkeyPatch) -> None:
20
+ monkeypatch.setattr(platform_utils.shutil, "which", lambda _name: None)
21
+ monkeypatch.setattr(platform_utils, "IS_WINDOWS", False)
22
+ assert platform_utils.find_python() == [sys.executable]
23
+
24
+
25
+ def test_find_python_prefers_python3_when_available(monkeypatch: pytest.MonkeyPatch) -> None:
26
+ available = {"python3": "/usr/bin/python3"}
27
+ monkeypatch.setattr(
28
+ platform_utils.shutil,
29
+ "which",
30
+ lambda name: available.get(name),
31
+ )
32
+ assert platform_utils.find_python() == ["python3"]
33
+
34
+
35
+ def test_utc_now_iso_is_well_formed() -> None:
36
+ stamp = platform_utils.utc_now_iso()
37
+ # Looks like 2026-05-16T17:00:00+00:00 (or ...Z on some Pythons).
38
+ assert "T" in stamp
39
+ assert stamp.endswith("+00:00") or stamp.endswith("Z")
40
+
41
+
42
+ def test_emoji_safe_passthrough_on_utf(monkeypatch: pytest.MonkeyPatch) -> None:
43
+ class _Stream:
44
+ encoding = "utf-8"
45
+
46
+ monkeypatch.setattr(platform_utils.sys, "stdout", _Stream())
47
+ text = "✅ done — 📋"
48
+ assert platform_utils.emoji_safe(text) == text
49
+
50
+
51
+ def test_emoji_safe_downgrades_on_legacy_encoding(monkeypatch: pytest.MonkeyPatch) -> None:
52
+ class _Stream:
53
+ encoding = "cp1254"
54
+
55
+ monkeypatch.setattr(platform_utils.sys, "stdout", _Stream())
56
+ out = platform_utils.emoji_safe("✅ ✗ ⚠️")
57
+ assert "[ok]" in out
58
+ assert "[x]" in out
59
+ assert "[!]" in out
60
+ assert "✅" not in out
61
+
62
+
63
+ def test_echo_never_raises_on_unprintable(
64
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
65
+ ) -> None:
66
+ platform_utils.echo("plain ASCII line")
67
+ platform_utils.echo("Türkçe ışık ✅")
68
+ captured = capsys.readouterr()
69
+ assert "plain ASCII line" in captured.out
@@ -0,0 +1,56 @@
1
+ """Tests for the machine-readable session state (handoff/STATE.json)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from graphstack import state
11
+
12
+
13
+ def test_set_then_get_round_trips(project_root: Path,
14
+ capsys: pytest.CaptureFixture[str]) -> None:
15
+ assert state.run(["set", "--role", "builder", "--task", "t1",
16
+ "--note", "cycle 1"]) == 0
17
+ path = project_root / "handoff" / "STATE.json"
18
+ assert path.is_file()
19
+ data = json.loads(path.read_text(encoding="utf-8"))
20
+ assert data["role"] == "builder"
21
+ assert data["task_id"] == "t1"
22
+ assert data["note"] == "cycle 1"
23
+ assert data["updated_at"]
24
+
25
+ capsys.readouterr()
26
+ assert state.run(["get", "--json"]) == 0
27
+ out = capsys.readouterr().out
28
+ assert json.loads(out)["role"] == "builder"
29
+
30
+
31
+ def test_get_without_state_returns_error(project_root: Path,
32
+ capsys: pytest.CaptureFixture[str]) -> None:
33
+ assert state.run(["get"]) == 1
34
+ assert "no STATE.json" in capsys.readouterr().out
35
+
36
+
37
+ def test_clear_is_idempotent(project_root: Path) -> None:
38
+ state.run(["set", "--role", "qa"])
39
+ assert state.run(["clear"]) == 0
40
+ assert not (project_root / "handoff" / "STATE.json").exists()
41
+ assert state.run(["clear"]) == 0
42
+
43
+
44
+ def test_unknown_role_still_writes_with_warning(
45
+ project_root: Path, capsys: pytest.CaptureFixture[str]
46
+ ) -> None:
47
+ assert state.run(["set", "--role", "wizard"]) == 0
48
+ out = capsys.readouterr().out
49
+ assert "Unknown role" in out
50
+ assert (project_root / "handoff" / "STATE.json").is_file()
51
+
52
+
53
+ def test_load_state_handles_corrupt_json(project_root: Path) -> None:
54
+ path = project_root / "handoff" / "STATE.json"
55
+ path.write_text("{not json", encoding="utf-8")
56
+ assert state.load_state() is None
@@ -0,0 +1,204 @@
1
+ """Tests for graphstack validate / doctor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from graphstack.validate import run_checks
11
+
12
+
13
+ def _minimal_layout(root: Path) -> None:
14
+ paths = (
15
+ ".cursor/rules/graphstack.mdc",
16
+ "orchestrator/ORCHESTRATOR.md",
17
+ "orchestrator/TOKEN_OPTIMIZER.md",
18
+ ".cursor/skills/architect/ARCHITECT.md",
19
+ ".cursor/skills/builder/BUILDER.md",
20
+ "handoff/BRIEF.md",
21
+ "handoff/STATE.md",
22
+ "handoff/board/README.md",
23
+ "handoff/board/todo",
24
+ "handoff/board/doing",
25
+ "handoff/board/done",
26
+ )
27
+ for rel in paths:
28
+ p = root / rel
29
+ if rel.endswith(("todo", "doing", "done")):
30
+ p.mkdir(parents=True, exist_ok=True)
31
+ else:
32
+ p.parent.mkdir(parents=True, exist_ok=True)
33
+ p.write_text("# stub\n", encoding="utf-8")
34
+
35
+
36
+ def test_validate_reports_template_brief_as_warning(
37
+ project_root: Path, monkeypatch: pytest.MonkeyPatch
38
+ ) -> None:
39
+ _minimal_layout(project_root)
40
+ (project_root / "handoff" / "BRIEF.md").write_text(
41
+ "# Brief: [Feature/Change Name]\n**Status:** Draft\n",
42
+ encoding="utf-8",
43
+ )
44
+ report = run_checks()
45
+ assert any(f.code == "brief_template" and f.level == "warn" for f in report.findings)
46
+
47
+
48
+ def test_validate_strict_template_brief_is_error(
49
+ project_root: Path, monkeypatch: pytest.MonkeyPatch
50
+ ) -> None:
51
+ _minimal_layout(project_root)
52
+ (project_root / "handoff" / "BRIEF.md").write_text(
53
+ "# Brief: [Feature/Change Name]\n",
54
+ encoding="utf-8",
55
+ )
56
+ report = run_checks(strict=True)
57
+ assert any(f.code == "brief_template" and f.level == "error" for f in report.findings)
58
+
59
+
60
+ def test_validate_invalid_board_json_is_error(
61
+ project_root: Path, monkeypatch: pytest.MonkeyPatch
62
+ ) -> None:
63
+ _minimal_layout(project_root)
64
+ bad = project_root / "handoff" / "board" / "todo" / "bad.json"
65
+ bad.write_text("{not json", encoding="utf-8")
66
+ report = run_checks()
67
+ assert any(f.code == "task_invalid_json" for f in report.errors)
68
+
69
+
70
+ def test_validate_task_missing_keys(
71
+ project_root: Path, monkeypatch: pytest.MonkeyPatch
72
+ ) -> None:
73
+ _minimal_layout(project_root)
74
+ task = project_root / "handoff" / "board" / "doing" / "t1.json"
75
+ task.write_text(json.dumps({"id": "t1"}), encoding="utf-8")
76
+ report = run_checks()
77
+ assert any(f.code == "task_missing_keys" for f in report.errors)
78
+
79
+
80
+ def test_graph_stale_when_commit_mismatch(
81
+ project_root: Path, monkeypatch: pytest.MonkeyPatch
82
+ ) -> None:
83
+ import subprocess
84
+
85
+ from graphstack import validate as validate_mod
86
+
87
+ _minimal_layout(project_root)
88
+ graph_dir = project_root / "graphify-out"
89
+ graph_dir.mkdir()
90
+ (graph_dir / "GRAPH_REPORT.md").write_text(
91
+ "Built from commit: `deadbeef00000000000000000000000000000000`\n",
92
+ encoding="utf-8",
93
+ )
94
+
95
+ def fake_git(*args: str, capture: bool = True) -> subprocess.CompletedProcess[str]:
96
+ if args == ("rev-parse", "HEAD"):
97
+ return subprocess.CompletedProcess(
98
+ args, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", ""
99
+ )
100
+ return subprocess.CompletedProcess(args, 1, "", "")
101
+
102
+ monkeypatch.setattr(validate_mod, "run_git", fake_git)
103
+ monkeypatch.setattr(validate_mod, "git_available", lambda: True)
104
+ report = run_checks(fail_stale=True)
105
+ assert any(f.code == "graph_stale" for f in report.errors)
106
+
107
+
108
+ def test_graph_fresh_when_built_commit_is_ancestor_of_head(
109
+ project_root: Path, monkeypatch: pytest.MonkeyPatch
110
+ ) -> None:
111
+ import subprocess
112
+
113
+ from graphstack import validate as validate_mod
114
+
115
+ _minimal_layout(project_root)
116
+ graph_dir = project_root / "graphify-out"
117
+ graph_dir.mkdir()
118
+ old = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
119
+ head = "cccccccccccccccccccccccccccccccccccccccc"
120
+ (graph_dir / "GRAPH_REPORT.md").write_text(
121
+ f"Built from commit: `{old}`\n",
122
+ encoding="utf-8",
123
+ )
124
+
125
+ def fake_git(*args: str, capture: bool = True) -> subprocess.CompletedProcess[str]:
126
+ if args == ("rev-parse", "HEAD"):
127
+ return subprocess.CompletedProcess(args, 0, f"{head}\n", "")
128
+ if args == ("rev-parse", "HEAD~1"):
129
+ return subprocess.CompletedProcess(args, 1, "", "unknown")
130
+ if args[:2] == ("merge-base", "--is-ancestor"):
131
+ return subprocess.CompletedProcess(args, 0, "", "")
132
+ if args[:2] == ("rev-list", "--max-count"):
133
+ return subprocess.CompletedProcess(args, 0, f"{head}\n", "")
134
+ if args[:3] == ("log", "-1", "--format=%H"):
135
+ return subprocess.CompletedProcess(args, 0, f"{head}\n", "")
136
+ return subprocess.CompletedProcess(args, 1, "", "")
137
+
138
+ monkeypatch.setattr(validate_mod, "run_git", fake_git)
139
+ monkeypatch.setattr(validate_mod, "git_available", lambda: True)
140
+ report = run_checks(fail_stale=True)
141
+ assert any(f.code == "graph_fresh" for f in report.findings)
142
+ assert not report.errors
143
+
144
+
145
+ def test_graph_fresh_when_built_from_parent_commit(
146
+ project_root: Path, monkeypatch: pytest.MonkeyPatch
147
+ ) -> None:
148
+ import subprocess
149
+
150
+ from graphstack import validate as validate_mod
151
+
152
+ _minimal_layout(project_root)
153
+ graph_dir = project_root / "graphify-out"
154
+ graph_dir.mkdir()
155
+ parent = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
156
+ (graph_dir / "GRAPH_REPORT.md").write_text(
157
+ f"Built from commit: `{parent}`\n",
158
+ encoding="utf-8",
159
+ )
160
+
161
+ def fake_git(*args: str, capture: bool = True) -> subprocess.CompletedProcess[str]:
162
+ if args == ("rev-parse", "HEAD"):
163
+ return subprocess.CompletedProcess(
164
+ args, 0, "cccccccccccccccccccccccccccccccccccccccc\n", ""
165
+ )
166
+ if args == ("rev-parse", "HEAD~1"):
167
+ return subprocess.CompletedProcess(args, 0, f"{parent}\n", "")
168
+ return subprocess.CompletedProcess(args, 1, "", "")
169
+
170
+ monkeypatch.setattr(validate_mod, "run_git", fake_git)
171
+ monkeypatch.setattr(validate_mod, "git_available", lambda: True)
172
+ report = run_checks(fail_stale=True)
173
+ assert any(f.code == "graph_fresh" for f in report.findings)
174
+ assert not report.errors
175
+
176
+
177
+ def test_validate_framework_warns_on_dirty_handoff(project_root: Path) -> None:
178
+ _minimal_layout(project_root)
179
+ (project_root / ".graphstack-framework").write_text("framework\n", encoding="utf-8")
180
+ (project_root / "handoff" / "BRIEF.md").write_text(
181
+ "# Brief: Real Feature\n**Status:** Ready for Builder\n",
182
+ encoding="utf-8",
183
+ )
184
+ done = project_root / "handoff" / "board" / "done"
185
+ (done / "stale-task.json").write_text(
186
+ json.dumps({"id": "stale-task", "title": "x", "status": "done",
187
+ "created_at": "2026-01-01T00:00:00+00:00"}),
188
+ encoding="utf-8",
189
+ )
190
+ report = run_checks()
191
+ codes = {f.code for f in report.findings if f.level == "warn"}
192
+ assert "framework_brief_dirty" in codes
193
+ assert "framework_board_dirty" in codes
194
+
195
+
196
+ def test_validate_framework_clean_handoff_no_warnings(project_root: Path) -> None:
197
+ _minimal_layout(project_root)
198
+ (project_root / ".graphstack-framework").write_text("framework\n", encoding="utf-8")
199
+ (project_root / "handoff" / "BRIEF.md").write_text(
200
+ "# Brief: [Feature/Change Name]\n**Date:** YYYY-MM-DD\n",
201
+ encoding="utf-8",
202
+ )
203
+ report = run_checks()
204
+ assert not any(f.code.startswith("framework_") for f in report.findings)