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,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)
|