goodturn 26.5.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.
Files changed (47) hide show
  1. goodturn-26.5.0/.gitignore +114 -0
  2. goodturn-26.5.0/CHANGELOG.md +17 -0
  3. goodturn-26.5.0/PKG-INFO +11 -0
  4. goodturn-26.5.0/pyproject.toml +23 -0
  5. goodturn-26.5.0/src/goodturn/__init__.py +3 -0
  6. goodturn-26.5.0/src/goodturn/__main__.py +6 -0
  7. goodturn-26.5.0/src/goodturn/_parse_duration.py +34 -0
  8. goodturn-26.5.0/src/goodturn/_yaml.py +111 -0
  9. goodturn-26.5.0/src/goodturn/advisory.py +452 -0
  10. goodturn-26.5.0/src/goodturn/cli.py +32 -0
  11. goodturn-26.5.0/src/goodturn/cmd_eval.py +101 -0
  12. goodturn-26.5.0/src/goodturn/cmd_harvest.py +782 -0
  13. goodturn-26.5.0/src/goodturn/cmd_outbox.py +102 -0
  14. goodturn-26.5.0/src/goodturn/cmd_publish.py +173 -0
  15. goodturn-26.5.0/src/goodturn/cmd_review.py +93 -0
  16. goodturn-26.5.0/src/goodturn/cmd_status.py +63 -0
  17. goodturn-26.5.0/src/goodturn/config.py +91 -0
  18. goodturn-26.5.0/src/goodturn/discovery.py +366 -0
  19. goodturn-26.5.0/src/goodturn/eval.py +354 -0
  20. goodturn-26.5.0/src/goodturn/forward_outbox.py +390 -0
  21. goodturn-26.5.0/src/goodturn/harness.py +43 -0
  22. goodturn-26.5.0/src/goodturn/harvest_types.py +67 -0
  23. goodturn-26.5.0/src/goodturn/harvester.py +617 -0
  24. goodturn-26.5.0/src/goodturn/insight.py +358 -0
  25. goodturn-26.5.0/src/goodturn/llm.py +266 -0
  26. goodturn-26.5.0/src/goodturn/outbox.py +311 -0
  27. goodturn-26.5.0/src/goodturn/parser.py +334 -0
  28. goodturn-26.5.0/src/goodturn/prompts.py +50 -0
  29. goodturn-26.5.0/src/goodturn/quality_bar.md +80 -0
  30. goodturn-26.5.0/src/goodturn/reviewer.py +319 -0
  31. goodturn-26.5.0/src/goodturn/session_analyzer.py +122 -0
  32. goodturn-26.5.0/src/goodturn/submitter.py +176 -0
  33. goodturn-26.5.0/src/goodturn/sweep_state.py +262 -0
  34. goodturn-26.5.0/tests/conftest.py +1 -0
  35. goodturn-26.5.0/tests/test_advisory.py +416 -0
  36. goodturn-26.5.0/tests/test_cli.py +10 -0
  37. goodturn-26.5.0/tests/test_cmd_outbox.py +11 -0
  38. goodturn-26.5.0/tests/test_cmd_status.py +11 -0
  39. goodturn-26.5.0/tests/test_eval.py +185 -0
  40. goodturn-26.5.0/tests/test_harvester.py +307 -0
  41. goodturn-26.5.0/tests/test_insight.py +478 -0
  42. goodturn-26.5.0/tests/test_llm.py +272 -0
  43. goodturn-26.5.0/tests/test_outbox.py +634 -0
  44. goodturn-26.5.0/tests/test_parser.py +280 -0
  45. goodturn-26.5.0/tests/test_quick_harvest.py +831 -0
  46. goodturn-26.5.0/tests/test_session_analyzer.py +371 -0
  47. goodturn-26.5.0/tests/test_sweep_lock.py +174 -0
@@ -0,0 +1,114 @@
1
+ .omp/extensions/*.bak
2
+ .claude/extensions/*.bak
3
+
4
+
5
+ client/.sveltekit
6
+ server/theory/*.json
7
+ .hypothesis/
8
+ prof/
9
+ .cache
10
+ .parcel-cache
11
+ _stuff/
12
+ *x.ipynb
13
+ tmp/
14
+ tmp_*.db*
15
+ **/attic/*.json
16
+ tmp*.json
17
+ nohup.out
18
+ ff_uploads/
19
+ tmp_schema*.sql
20
+ minio_data/
21
+ biz_data/tmp_*
22
+ nb/tmp_*
23
+ nb/.ipynb_checkpoints
24
+ nb/fonts/
25
+ nb/_cells/
26
+
27
+ # python
28
+
29
+ venv
30
+ .venv
31
+ __pycache__
32
+ docs/_build
33
+ tmp.py
34
+ htmlcov/
35
+ .coverage.*
36
+ test.db
37
+ app.db
38
+ node_modules
39
+ stats.html
40
+
41
+ *.py[cod]
42
+ .pytest_cache
43
+
44
+ .~lock.*
45
+
46
+
47
+ # emacs
48
+ *~
49
+ ._*
50
+ .\#*
51
+ \#*\#
52
+
53
+ # C extensions
54
+ *.so
55
+
56
+ # Packages
57
+ *.egg
58
+ *.egg-info
59
+ dist
60
+ build
61
+ eggs
62
+ parts
63
+ bin
64
+ var
65
+ sdist
66
+ develop-eggs
67
+ .installed.cfg
68
+ lib64
69
+
70
+ # Installer logs
71
+ pip-log.txt
72
+
73
+ # Unit test / coverage reports
74
+ .coverage
75
+ .tox
76
+ nosetests.xml
77
+
78
+ # Translations
79
+ *.mo
80
+
81
+ # Mr Developer
82
+ .mr.developer.cfg
83
+ .project
84
+ .pydevproject
85
+
86
+ # Vim
87
+ *.sw[op]
88
+
89
+ .cache/
90
+
91
+
92
+ # Editor/tool artifacts
93
+ .cursor/
94
+ .omp/*
95
+ !.omp/extensions/
96
+ !.omp/mcp.json
97
+ !.omp/settings.json
98
+
99
+ # Secrets
100
+ .env
101
+
102
+ # Benchmark results (generated)
103
+ bench/results/
104
+
105
+ # Bulk data & caches
106
+ biz_data/r29_cache/
107
+ biz_data/r29_diary_urls.json
108
+ biz_data/diary_*
109
+ biz_data/gtm_*
110
+
111
+ # Notebook cell extracts
112
+ nb/_cells/omp-session-*.html
113
+ web/node_modules
114
+ web/.env
@@ -0,0 +1,17 @@
1
+ # goodturn Changelog
2
+
3
+ ## 26.5.0
4
+
5
+ _(May 2, 2026)_
6
+
7
+ - Initial PyPI release
8
+ - Session harvester: extract lessons and fixes from AI coding sessions (Claude Code, OMP)
9
+ - Session analyzer: multi-depth analysis (scan/focused/thorough) with signal detection
10
+ - Insight detector: regex-based learning-moment detection (corrections, realizations, frustration, repeated errors)
11
+ - Security advisory scanner: typosquatting, insecure HTTP, hardcoded credentials, dangerous code patterns
12
+ - Outbox system: draft/review/submit workflow with deduplication and frontmatter management
13
+ - `gt eval` command for batch session evaluation with per-session scoring
14
+ - `gt review` command for interactive post review before submission
15
+ - CI signal pipeline with GitHub OIDC zero-config auth
16
+ - `stale` signal type for outdated solutions
17
+ - CalVer versioning (YY.MM.MICRO)
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: goodturn
3
+ Version: 26.5.0
4
+ Summary: Harvest and contribute knowledge from AI coding sessions
5
+ License-Expression: BSD-3-Clause
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: face>=24.0.0
8
+ Requires-Dist: httpx<1.0,>=0.25
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
11
+ Requires-Dist: pytest>=7.4; extra == 'dev'
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "goodturn"
7
+ version = "26.5.0"
8
+ description = "Harvest and contribute knowledge from AI coding sessions"
9
+ requires-python = ">=3.10"
10
+ license = "BSD-3-Clause"
11
+ dependencies = [
12
+ "face>=24.0.0",
13
+ "httpx>=0.25,<1.0",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ dev = ["pytest>=7.4", "pytest-xdist>=3.5"]
18
+
19
+ [project.scripts]
20
+ goodturn = "goodturn.cli:main"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/goodturn"]
@@ -0,0 +1,3 @@
1
+ """GoodTurn — harvest and contribute knowledge from AI coding sessions."""
2
+
3
+ __version__ = "26.5.0"
@@ -0,0 +1,6 @@
1
+ """Support `python -m goodturn`."""
2
+
3
+ from goodturn.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,34 @@
1
+ """Regex-based duration string parser.
2
+
3
+ Replaces ``boltons.timeutils.parse_timedelta`` with a stdlib-only
4
+ implementation that handles the patterns we actually use: ``7d``,
5
+ ``2w``, ``1d12h``, ``3h``, ``30m``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from datetime import timedelta
12
+
13
+ _DURATION_RE = re.compile(
14
+ r"(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$"
15
+ )
16
+
17
+
18
+ def parse_duration(s: str) -> timedelta:
19
+ """Parse a human duration string into a :class:`~datetime.timedelta`.
20
+
21
+ Supported units: weeks (``w``), days (``d``), hours (``h``),
22
+ minutes (``m``). Units may be combined: ``1d12h``.
23
+
24
+ Raises :class:`ValueError` on unparseable input.
25
+ """
26
+ s = s.strip()
27
+ m = _DURATION_RE.fullmatch(s)
28
+ if m is None or not any(m.groups()):
29
+ raise ValueError(f"cannot parse duration: {s!r}")
30
+ weeks = int(m.group(1) or 0)
31
+ days = int(m.group(2) or 0)
32
+ hours = int(m.group(3) or 0)
33
+ minutes = int(m.group(4) or 0)
34
+ return timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes)
@@ -0,0 +1,111 @@
1
+ """Minimal YAML serialization and parsing for frontmatter-style metadata.
2
+
3
+ Handles only the constrained schema we produce: flat scalars, flow
4
+ sequences ``[a, b]``, and inline dicts ``{k: v}``. No full YAML parser
5
+ needed.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+
11
+ def _yaml_scalar(value: object) -> str:
12
+ """Format a scalar for YAML output. Strings are double-quoted."""
13
+ if isinstance(value, bool):
14
+ return "true" if value else "false"
15
+ if isinstance(value, (int, float)):
16
+ return str(value)
17
+ # String: double-quote, escaping internal quotes and backslashes.
18
+ s = str(value).replace("\\", "\\\\").replace('"', '\\"')
19
+ return f'"{s}"'
20
+
21
+
22
+ def _yaml_list(items: list[str]) -> str:
23
+ """Format a list as YAML flow sequence ``[a, b, c]``."""
24
+ inner = ", ".join(_yaml_scalar(i) for i in items)
25
+ return f"[{inner}]"
26
+
27
+
28
+ def _yaml_dict(d: dict[str, str]) -> str:
29
+ """Format a dict as inline YAML ``{"k": "v", ...}``."""
30
+ parts = ", ".join(
31
+ f"{_yaml_scalar(k)}: {_yaml_scalar(v)}" for k, v in d.items()
32
+ )
33
+ return f"{{{parts}}}"
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Parsing
38
+ # ---------------------------------------------------------------------------
39
+
40
+
41
+ def _parse_frontmatter(text: str) -> tuple[dict[str, str], str]:
42
+ """Parse a ``---``-delimited YAML frontmatter block from *text*.
43
+
44
+ Returns ``(metadata_dict, body)`` where metadata values are raw strings.
45
+ Only handles the constrained schema we produce: flat scalars and
46
+ bracket-delimited string lists.
47
+ """
48
+ if not text.startswith("---"):
49
+ return {}, text
50
+
51
+ # Find closing '---' (must be on its own line after the opening).
52
+ end = text.find("\n---", 3)
53
+ if end == -1:
54
+ return {}, text
55
+
56
+ fm_block = text[4:end] # skip opening '---\n'
57
+ body = text[end + 4 :] # skip '\n---'
58
+ if body.startswith("\n"):
59
+ body = body[1:]
60
+
61
+ meta: dict[str, str] = {}
62
+ for line in fm_block.strip().split("\n"):
63
+ colon = line.find(":")
64
+ if colon == -1:
65
+ continue
66
+ key = line[:colon].strip()
67
+ val = line[colon + 1 :].strip()
68
+ meta[key] = val
69
+ return meta, body
70
+
71
+
72
+ def _unquote(s: str) -> str:
73
+ """Remove surrounding double quotes and unescape."""
74
+ if len(s) >= 2 and s[0] == '"' and s[-1] == '"':
75
+ return s[1:-1].replace('\\"', '"').replace("\\\\", "\\")
76
+ return s
77
+
78
+
79
+ def _parse_yaml_list(s: str) -> list[str]:
80
+ """Parse a YAML flow sequence ``[a, b, c]`` into a list of strings."""
81
+ s = s.strip()
82
+ if not s.startswith("[") or not s.endswith("]"):
83
+ return [_unquote(s)] if s else []
84
+ inner = s[1:-1].strip()
85
+ if not inner:
86
+ return []
87
+ return [_unquote(item.strip()) for item in inner.split(",")]
88
+
89
+
90
+ def _parse_yaml_dict(s: str) -> dict[str, str]:
91
+ """Parse an inline YAML dict ``{key: "val", key2: "val2"}``.
92
+
93
+ Returns an empty dict for missing or malformed input.
94
+ """
95
+ s = s.strip()
96
+ if not s.startswith("{") or not s.endswith("}"):
97
+ return {}
98
+ inner = s[1:-1].strip()
99
+ if not inner:
100
+ return {}
101
+ result: dict[str, str] = {}
102
+ for part in inner.split(","):
103
+ part = part.strip()
104
+ colon = part.find(":")
105
+ if colon == -1:
106
+ continue
107
+ key = _unquote(part[:colon].strip())
108
+ val = _unquote(part[colon + 1 :].strip())
109
+ if key:
110
+ result[key] = val
111
+ return result