devrel-origin 0.2.14__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 (98) hide show
  1. devrel_origin/__init__.py +15 -0
  2. devrel_origin/cli/__init__.py +92 -0
  3. devrel_origin/cli/_common.py +243 -0
  4. devrel_origin/cli/analytics.py +28 -0
  5. devrel_origin/cli/argus.py +497 -0
  6. devrel_origin/cli/auth.py +227 -0
  7. devrel_origin/cli/config.py +108 -0
  8. devrel_origin/cli/content.py +259 -0
  9. devrel_origin/cli/cost.py +108 -0
  10. devrel_origin/cli/cro.py +298 -0
  11. devrel_origin/cli/deliverables.py +65 -0
  12. devrel_origin/cli/docs.py +91 -0
  13. devrel_origin/cli/doctor.py +178 -0
  14. devrel_origin/cli/experiment.py +29 -0
  15. devrel_origin/cli/growth.py +97 -0
  16. devrel_origin/cli/init.py +472 -0
  17. devrel_origin/cli/intel.py +27 -0
  18. devrel_origin/cli/kb.py +96 -0
  19. devrel_origin/cli/listen.py +31 -0
  20. devrel_origin/cli/marketing.py +66 -0
  21. devrel_origin/cli/migrate.py +45 -0
  22. devrel_origin/cli/run.py +46 -0
  23. devrel_origin/cli/sales.py +57 -0
  24. devrel_origin/cli/schedule.py +62 -0
  25. devrel_origin/cli/synthesize.py +28 -0
  26. devrel_origin/cli/triage.py +29 -0
  27. devrel_origin/cli/video.py +35 -0
  28. devrel_origin/core/__init__.py +58 -0
  29. devrel_origin/core/agent_config.py +75 -0
  30. devrel_origin/core/argus.py +964 -0
  31. devrel_origin/core/atlas.py +1450 -0
  32. devrel_origin/core/base.py +372 -0
  33. devrel_origin/core/cyra.py +563 -0
  34. devrel_origin/core/dex.py +708 -0
  35. devrel_origin/core/echo.py +614 -0
  36. devrel_origin/core/growth/__init__.py +27 -0
  37. devrel_origin/core/growth/recommendations.py +219 -0
  38. devrel_origin/core/growth/target_kinds.py +51 -0
  39. devrel_origin/core/iris.py +513 -0
  40. devrel_origin/core/kai.py +1367 -0
  41. devrel_origin/core/llm.py +542 -0
  42. devrel_origin/core/llm_backends.py +274 -0
  43. devrel_origin/core/mox.py +514 -0
  44. devrel_origin/core/nova.py +349 -0
  45. devrel_origin/core/pax.py +1205 -0
  46. devrel_origin/core/rex.py +532 -0
  47. devrel_origin/core/sage.py +486 -0
  48. devrel_origin/core/sentinel.py +385 -0
  49. devrel_origin/core/types.py +98 -0
  50. devrel_origin/core/video/__init__.py +22 -0
  51. devrel_origin/core/video/assembler.py +131 -0
  52. devrel_origin/core/video/browser_recorder.py +118 -0
  53. devrel_origin/core/video/desktop_recorder.py +254 -0
  54. devrel_origin/core/video/overlay_renderer.py +143 -0
  55. devrel_origin/core/video/script_parser.py +147 -0
  56. devrel_origin/core/video/tts_engine.py +82 -0
  57. devrel_origin/core/vox.py +268 -0
  58. devrel_origin/core/watchdog.py +321 -0
  59. devrel_origin/project/__init__.py +1 -0
  60. devrel_origin/project/config.py +75 -0
  61. devrel_origin/project/cost_sink.py +61 -0
  62. devrel_origin/project/init.py +104 -0
  63. devrel_origin/project/paths.py +75 -0
  64. devrel_origin/project/state.py +241 -0
  65. devrel_origin/project/templates/__init__.py +4 -0
  66. devrel_origin/project/templates/config.toml +24 -0
  67. devrel_origin/project/templates/devrel.gitignore +10 -0
  68. devrel_origin/project/templates/slop-blocklist.md +45 -0
  69. devrel_origin/project/templates/style.md +24 -0
  70. devrel_origin/project/templates/voice.md +29 -0
  71. devrel_origin/quality/__init__.py +66 -0
  72. devrel_origin/quality/editorial.py +357 -0
  73. devrel_origin/quality/persona.py +84 -0
  74. devrel_origin/quality/readability.py +148 -0
  75. devrel_origin/quality/slop.py +167 -0
  76. devrel_origin/quality/style.py +110 -0
  77. devrel_origin/quality/voice.py +15 -0
  78. devrel_origin/tools/__init__.py +9 -0
  79. devrel_origin/tools/analytics.py +304 -0
  80. devrel_origin/tools/api_client.py +393 -0
  81. devrel_origin/tools/apollo_client.py +305 -0
  82. devrel_origin/tools/code_validator.py +428 -0
  83. devrel_origin/tools/github_tools.py +297 -0
  84. devrel_origin/tools/instantly_client.py +412 -0
  85. devrel_origin/tools/kb_harvester.py +340 -0
  86. devrel_origin/tools/mcp_server.py +578 -0
  87. devrel_origin/tools/notifications.py +245 -0
  88. devrel_origin/tools/run_report.py +193 -0
  89. devrel_origin/tools/scheduler.py +231 -0
  90. devrel_origin/tools/search_tools.py +321 -0
  91. devrel_origin/tools/self_improve.py +168 -0
  92. devrel_origin/tools/sheets.py +236 -0
  93. devrel_origin-0.2.14.dist-info/METADATA +354 -0
  94. devrel_origin-0.2.14.dist-info/RECORD +98 -0
  95. devrel_origin-0.2.14.dist-info/WHEEL +5 -0
  96. devrel_origin-0.2.14.dist-info/entry_points.txt +2 -0
  97. devrel_origin-0.2.14.dist-info/licenses/LICENSE +21 -0
  98. devrel_origin-0.2.14.dist-info/top_level.txt +1 -0
@@ -0,0 +1,219 @@
1
+ """Pillar-agnostic Recommendation dataclass + persistence + lifecycle queries.
2
+
3
+ This module is the contract every Growth-pipeline auditor (and Argus) writes
4
+ through. Each pillar produces `Recommendation` instances and calls
5
+ `persist_recommendation` to land them in `analytics_recommendations`. Lifecycle
6
+ helpers (`find_open_by_target`, `mark_applied`, `find_stale`) drive the
7
+ recommendation closed-loop that Mox consumes for brief generation.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import sqlite3
14
+ from collections import defaultdict
15
+ from dataclasses import dataclass
16
+ from datetime import date, timedelta
17
+ from pathlib import Path
18
+ from typing import Callable, Optional
19
+
20
+ from devrel_origin.core.growth.target_kinds import (
21
+ Pillar,
22
+ TargetKind,
23
+ validate_target_kind_for_pillar,
24
+ )
25
+
26
+
27
+ @dataclass
28
+ class Recommendation:
29
+ """A single structured action recommendation emitted by a Growth auditor.
30
+
31
+ Maps 1:1 to a row in `analytics_recommendations`.
32
+ """
33
+
34
+ pillar: Pillar
35
+ action: str
36
+ target: str
37
+ target_kind: TargetKind
38
+ confidence: float
39
+ source_ids: list[str]
40
+ first_seen_period: str
41
+ applied_at: Optional[str] = None
42
+ rationale: Optional[str] = None
43
+
44
+ def __post_init__(self) -> None:
45
+ if isinstance(self.pillar, str):
46
+ self.pillar = Pillar(self.pillar)
47
+ if isinstance(self.target_kind, str):
48
+ self.target_kind = TargetKind(self.target_kind)
49
+
50
+
51
+ def persist_recommendation(db_path: Path, report_id: int, rec: Recommendation) -> None:
52
+ """Insert a Recommendation into `analytics_recommendations`.
53
+
54
+ Validates `(pillar, target_kind)` before INSERT: accidental cross-pillar
55
+ target_kinds are caught here, not at calibration time.
56
+ """
57
+ validate_target_kind_for_pillar(rec.pillar, rec.target_kind)
58
+
59
+ with sqlite3.connect(db_path) as conn:
60
+ conn.execute(
61
+ """
62
+ INSERT INTO analytics_recommendations
63
+ (report_id, period_end, action, target, target_type, rationale,
64
+ confidence, source_ids_json, first_seen_period, applied_at,
65
+ pillar, target_kind)
66
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
67
+ """,
68
+ (
69
+ report_id,
70
+ rec.first_seen_period,
71
+ rec.action,
72
+ rec.target,
73
+ rec.target_kind.value,
74
+ rec.rationale or "",
75
+ rec.confidence,
76
+ json.dumps(rec.source_ids),
77
+ rec.first_seen_period,
78
+ rec.applied_at,
79
+ rec.pillar.value,
80
+ rec.target_kind.value,
81
+ ),
82
+ )
83
+ conn.commit()
84
+
85
+
86
+ def _row_to_recommendation(row: tuple) -> Recommendation:
87
+ return Recommendation(
88
+ pillar=Pillar(row[0]),
89
+ action=row[1],
90
+ target=row[2],
91
+ target_kind=TargetKind(row[3]),
92
+ confidence=row[4],
93
+ source_ids=json.loads(row[5] or "[]"),
94
+ first_seen_period=row[6],
95
+ applied_at=row[7],
96
+ rationale=row[8] if len(row) > 8 else None,
97
+ )
98
+
99
+
100
+ def find_open_by_target(db_path: Path, pillar: Pillar) -> list[Recommendation]:
101
+ """Return all unapplied recommendations for a pillar, newest-first."""
102
+ with sqlite3.connect(db_path) as conn:
103
+ cur = conn.execute(
104
+ """
105
+ SELECT pillar, action, target, target_kind, confidence,
106
+ source_ids_json, first_seen_period, applied_at, rationale
107
+ FROM analytics_recommendations
108
+ WHERE pillar = ? AND applied_at IS NULL
109
+ ORDER BY first_seen_period DESC
110
+ """,
111
+ (pillar.value,),
112
+ )
113
+ return [_row_to_recommendation(row) for row in cur.fetchall()]
114
+
115
+
116
+ def mark_applied(
117
+ db_path: Path,
118
+ pillar: Pillar,
119
+ *,
120
+ action: str,
121
+ target: str,
122
+ target_kind: TargetKind,
123
+ ) -> None:
124
+ """Stamp a recommendation as applied (Mox shipped the change)."""
125
+ with sqlite3.connect(db_path) as conn:
126
+ conn.execute(
127
+ """
128
+ UPDATE analytics_recommendations
129
+ SET applied_at = datetime('now')
130
+ WHERE pillar = ? AND action = ? AND target = ? AND target_kind = ?
131
+ AND applied_at IS NULL
132
+ """,
133
+ (pillar.value, action, target, target_kind.value),
134
+ )
135
+ conn.commit()
136
+
137
+
138
+ def find_stale(
139
+ db_path: Path,
140
+ pillar: Pillar,
141
+ *,
142
+ current_period: str,
143
+ stale_after_periods: int = 2,
144
+ ) -> list[Recommendation]:
145
+ """Return open recommendations whose first_seen_period is N+ periods old.
146
+
147
+ `period` here is calendar weeks; `stale_after_periods=2` means
148
+ "first_seen at least 14 days before current_period" qualifies as stale.
149
+ """
150
+ cutoff = date.fromisoformat(current_period) - timedelta(weeks=stale_after_periods)
151
+ with sqlite3.connect(db_path) as conn:
152
+ cur = conn.execute(
153
+ """
154
+ SELECT pillar, action, target, target_kind, confidence,
155
+ source_ids_json, first_seen_period, applied_at, rationale
156
+ FROM analytics_recommendations
157
+ WHERE pillar = ? AND applied_at IS NULL
158
+ AND first_seen_period <= ?
159
+ ORDER BY first_seen_period ASC
160
+ """,
161
+ (pillar.value, cutoff.isoformat()),
162
+ )
163
+ return [_row_to_recommendation(row) for row in cur.fetchall()]
164
+
165
+
166
+ def calibrate(
167
+ db_path: Path,
168
+ pillar: Pillar,
169
+ *,
170
+ outcome_scorer: Callable[[Recommendation], str],
171
+ ) -> dict[str, dict[str, float | int]]:
172
+ """Per-action hit-rate calibration for one pillar's applied recommendations.
173
+
174
+ `outcome_scorer(rec)` returns one of {'improved', 'unchanged', 'regressed'}.
175
+ Each pillar implements its own scorer based on subsequent fact-table rows
176
+ (for example, SEO checks if keyword position improved; CRO checks if
177
+ conversion rate rose). This helper just aggregates.
178
+
179
+ Returns: {action: {applied_count, hit_rate, lift_vs_coinflip,
180
+ avg_confidence, high_conf_hit_rate}}
181
+ """
182
+ with sqlite3.connect(db_path) as conn:
183
+ cur = conn.execute(
184
+ """
185
+ SELECT pillar, action, target, target_kind, confidence,
186
+ source_ids_json, first_seen_period, applied_at, rationale
187
+ FROM analytics_recommendations
188
+ WHERE pillar = ? AND applied_at IS NOT NULL
189
+ """,
190
+ (pillar.value,),
191
+ )
192
+ rows = cur.fetchall()
193
+
194
+ by_action: dict[str, list[tuple[Recommendation, str]]] = defaultdict(list)
195
+ for row in rows:
196
+ rec = _row_to_recommendation(row)
197
+ outcome = outcome_scorer(rec)
198
+ by_action[rec.action].append((rec, outcome))
199
+
200
+ result: dict[str, dict[str, float | int]] = {}
201
+ for action, items in by_action.items():
202
+ n = len(items)
203
+ improved = sum(1 for _, o in items if o == "improved")
204
+ hit_rate = improved / n if n else 0.0
205
+ avg_conf = sum(r.confidence for r, _ in items) / n if n else 0.0
206
+ # high-conf = top half by confidence
207
+ sorted_items = sorted(items, key=lambda t: t[0].confidence, reverse=True)
208
+ high_half = sorted_items[: max(1, n // 2)]
209
+ high_improved = sum(1 for _, o in high_half if o == "improved")
210
+ high_hit = high_improved / len(high_half) if high_half else 0.0
211
+
212
+ result[action] = {
213
+ "applied_count": n,
214
+ "hit_rate": hit_rate,
215
+ "lift_vs_coinflip": hit_rate - 0.5,
216
+ "avg_confidence": avg_conf,
217
+ "high_conf_hit_rate": high_hit,
218
+ }
219
+ return result
@@ -0,0 +1,51 @@
1
+ """Pillar + TargetKind enums and the (pillar, target_kind) collision guard.
2
+
3
+ Stored as TEXT in SQLite (`analytics_recommendations.pillar` and
4
+ `.target_kind`) but typed at the Python boundary so accidental
5
+ free-form strings are caught at write time, not at calibration.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import Enum
11
+
12
+
13
+ class Pillar(str, Enum):
14
+ ARGUS = "argus"
15
+ SEO = "seo"
16
+ GEO = "geo"
17
+ CRO = "cro"
18
+
19
+
20
+ class TargetKind(str, Enum):
21
+ CONTENT_ID = "content_id"
22
+ URL = "url"
23
+ KEYWORD = "keyword"
24
+ FUNNEL_STEP = "funnel_step"
25
+ BRAND_QUERY = "brand_query"
26
+ COMPETITOR = "competitor"
27
+
28
+
29
+ # Per-pillar allowlists. Cross-cutting kinds (URL is in both SEO + GEO)
30
+ # are the reason we don't just key off pillar alone in the schema.
31
+ _VALID: dict[Pillar, frozenset[TargetKind]] = {
32
+ Pillar.ARGUS: frozenset({TargetKind.CONTENT_ID}),
33
+ Pillar.SEO: frozenset({TargetKind.URL, TargetKind.KEYWORD}),
34
+ Pillar.GEO: frozenset({TargetKind.BRAND_QUERY, TargetKind.URL, TargetKind.COMPETITOR}),
35
+ Pillar.CRO: frozenset({TargetKind.FUNNEL_STEP}),
36
+ }
37
+
38
+
39
+ def validate_target_kind_for_pillar(pillar: Pillar, kind: TargetKind) -> None:
40
+ """Raise ValueError if `kind` is not a legal target for `pillar`.
41
+
42
+ Called by `persist_recommendation` before INSERT. Keeps the
43
+ cross-pillar query namespace coherent: a `target_kind='url'` row
44
+ is unambiguously SEO or GEO and never anything else.
45
+ """
46
+ if kind not in _VALID[pillar]:
47
+ valid_names = sorted(k.value for k in _VALID[pillar])
48
+ raise ValueError(
49
+ f"target_kind={kind.value!r} not valid for pillar={pillar.value!r}; "
50
+ f"valid kinds: {valid_names}"
51
+ )