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.
- goodturn-26.5.0/.gitignore +114 -0
- goodturn-26.5.0/CHANGELOG.md +17 -0
- goodturn-26.5.0/PKG-INFO +11 -0
- goodturn-26.5.0/pyproject.toml +23 -0
- goodturn-26.5.0/src/goodturn/__init__.py +3 -0
- goodturn-26.5.0/src/goodturn/__main__.py +6 -0
- goodturn-26.5.0/src/goodturn/_parse_duration.py +34 -0
- goodturn-26.5.0/src/goodturn/_yaml.py +111 -0
- goodturn-26.5.0/src/goodturn/advisory.py +452 -0
- goodturn-26.5.0/src/goodturn/cli.py +32 -0
- goodturn-26.5.0/src/goodturn/cmd_eval.py +101 -0
- goodturn-26.5.0/src/goodturn/cmd_harvest.py +782 -0
- goodturn-26.5.0/src/goodturn/cmd_outbox.py +102 -0
- goodturn-26.5.0/src/goodturn/cmd_publish.py +173 -0
- goodturn-26.5.0/src/goodturn/cmd_review.py +93 -0
- goodturn-26.5.0/src/goodturn/cmd_status.py +63 -0
- goodturn-26.5.0/src/goodturn/config.py +91 -0
- goodturn-26.5.0/src/goodturn/discovery.py +366 -0
- goodturn-26.5.0/src/goodturn/eval.py +354 -0
- goodturn-26.5.0/src/goodturn/forward_outbox.py +390 -0
- goodturn-26.5.0/src/goodturn/harness.py +43 -0
- goodturn-26.5.0/src/goodturn/harvest_types.py +67 -0
- goodturn-26.5.0/src/goodturn/harvester.py +617 -0
- goodturn-26.5.0/src/goodturn/insight.py +358 -0
- goodturn-26.5.0/src/goodturn/llm.py +266 -0
- goodturn-26.5.0/src/goodturn/outbox.py +311 -0
- goodturn-26.5.0/src/goodturn/parser.py +334 -0
- goodturn-26.5.0/src/goodturn/prompts.py +50 -0
- goodturn-26.5.0/src/goodturn/quality_bar.md +80 -0
- goodturn-26.5.0/src/goodturn/reviewer.py +319 -0
- goodturn-26.5.0/src/goodturn/session_analyzer.py +122 -0
- goodturn-26.5.0/src/goodturn/submitter.py +176 -0
- goodturn-26.5.0/src/goodturn/sweep_state.py +262 -0
- goodturn-26.5.0/tests/conftest.py +1 -0
- goodturn-26.5.0/tests/test_advisory.py +416 -0
- goodturn-26.5.0/tests/test_cli.py +10 -0
- goodturn-26.5.0/tests/test_cmd_outbox.py +11 -0
- goodturn-26.5.0/tests/test_cmd_status.py +11 -0
- goodturn-26.5.0/tests/test_eval.py +185 -0
- goodturn-26.5.0/tests/test_harvester.py +307 -0
- goodturn-26.5.0/tests/test_insight.py +478 -0
- goodturn-26.5.0/tests/test_llm.py +272 -0
- goodturn-26.5.0/tests/test_outbox.py +634 -0
- goodturn-26.5.0/tests/test_parser.py +280 -0
- goodturn-26.5.0/tests/test_quick_harvest.py +831 -0
- goodturn-26.5.0/tests/test_session_analyzer.py +371 -0
- 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)
|
goodturn-26.5.0/PKG-INFO
ADDED
|
@@ -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,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
|