oh-aicoding-tool 0.1.2 → 0.1.5

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 (55) hide show
  1. package/README.md +79 -80
  2. package/bin/cli.js +257 -384
  3. package/package.json +27 -55
  4. package/CODEX_LANGFUSE_PLAN.md +0 -62
  5. package/bin/langfuse-cli.js +0 -718
  6. package/codex_langfuse_notify.py +0 -591
  7. package/langfuse_hook.py +0 -603
  8. package/opencode-ohai-report/.claude/commands/report-ai-issue.md +0 -60
  9. package/opencode-ohai-report/.opencode/commands/report-ai-issue.md +0 -30
  10. package/opencode-ohai-report/.opencode/plugins/oh-ai-report.ts +0 -569
  11. package/opencode-ohai-report/README.md +0 -45
  12. package/opencode-ohai-report/bin/cli.js +0 -421
  13. package/opencode-ohai-report/docs/opencode-ai-issue-collection-architecture.md +0 -313
  14. package/opencode-ohai-report/docs/opencode-ai-issue-collection-best-practices.md +0 -476
  15. package/opencode-ohai-report/docs/opencode-ai-issue-collection-phase1-summary.md +0 -405
  16. package/opencode-ohai-report/examples/issue_output.json +0 -4
  17. package/opencode-ohai-report/package.json +0 -40
  18. package/opencode-ohai-report/scripts/claude_report_hook.py +0 -257
  19. package/opencode-ohai-report/scripts/create_issue.py +0 -34
  20. package/opencode-ohai-report/scripts/install-claude-plugin.ps1 +0 -254
  21. package/opencode-ohai-report/scripts/install-opencode-plugin.ps1 +0 -264
  22. package/opencode-ohai-report/scripts/install-opencode-plugin.sh +0 -218
  23. package/opencode-ohai-report/scripts/merge-claude-settings.py +0 -99
  24. package/opencode-ohai-report/tools/ohai-report/README.md +0 -151
  25. package/opencode-ohai-report/tools/ohai-report/examples/issue-input.json +0 -26
  26. package/opencode-ohai-report/tools/ohai-report/ohai_report/__init__.py +0 -5
  27. package/opencode-ohai-report/tools/ohai-report/ohai_report/__main__.py +0 -9
  28. package/opencode-ohai-report/tools/ohai-report/ohai_report/cli.py +0 -319
  29. package/opencode-ohai-report/tools/ohai-report/ohai_report/git_context.py +0 -32
  30. package/opencode-ohai-report/tools/ohai-report/ohai_report/gitcode_defaults.py +0 -14
  31. package/opencode-ohai-report/tools/ohai-report/ohai_report/issue_markdown.py +0 -313
  32. package/opencode-ohai-report/tools/ohai-report/ohai_report/metadata.py +0 -360
  33. package/opencode-ohai-report/tools/ohai-report/ohai_report/observability/__init__.py +0 -1
  34. package/opencode-ohai-report/tools/ohai-report/ohai_report/observability/langfuse.py +0 -38
  35. package/opencode-ohai-report/tools/ohai-report/ohai_report/payload.py +0 -64
  36. package/opencode-ohai-report/tools/ohai-report/ohai_report/schema.py +0 -80
  37. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/__init__.py +0 -1
  38. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/base.py +0 -15
  39. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/gitcode.py +0 -405
  40. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/local.py +0 -21
  41. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/webhook.py +0 -354
  42. package/opencode-ohai-report/tools/ohai-report/ohai_report/webhook_defaults.py +0 -9
  43. package/opencode-ohai-report/tools/ohai-report/ohai_report/workspace.py +0 -61
  44. package/opencode-ohai-report/tools/ohai-report/ohai_report.py +0 -10
  45. package/opencode-ohai-report/tools/ohai-report/schemas/report_issue.schema.json +0 -166
  46. package/scripts/codex-langfuse-check.mjs +0 -101
  47. package/scripts/codex-langfuse-setup.mjs +0 -181
  48. package/scripts/langfuse-check.mjs +0 -90
  49. package/scripts/langfuse-setup.mjs +0 -278
  50. package/scripts/opencode-langfuse-check.mjs +0 -94
  51. package/scripts/opencode-langfuse-run.mjs +0 -96
  52. package/scripts/opencode-langfuse-setup.mjs +0 -478
  53. package/scripts/resolve-opencode-cli.mjs +0 -58
  54. package/setup-langfuse.bat +0 -163
  55. package/setup-langfuse.sh +0 -130
@@ -1,360 +0,0 @@
1
- """Metadata resolution for agent/session/Langfuse indexes."""
2
-
3
- from __future__ import annotations
4
-
5
- import datetime as dt
6
- import os
7
- import pathlib
8
- from typing import Any
9
-
10
- from .git_context import git_context
11
- from .observability.langfuse import trace_url_from_env
12
- from .schema import MetadataUpdateInput
13
- from .workspace import data_dir, read_json, write_json
14
-
15
-
16
- DEFAULT_STORED_SESSION_MAX_AGE_SECONDS = 30 * 60
17
-
18
-
19
- def _home_dir() -> pathlib.Path:
20
- raw = (os.environ.get("USERPROFILE") or os.environ.get("HOME") or "").strip()
21
- if raw:
22
- return pathlib.Path(raw)
23
- try:
24
- return pathlib.Path.home()
25
- except RuntimeError:
26
- return pathlib.Path.cwd()
27
-
28
-
29
- def opencode_global_config_dir() -> pathlib.Path:
30
- """与 OpenCode 安装脚本一致:优先 ``XDG_CONFIG_HOME/opencode``,否则 Windows ``%USERPROFILE%\\.config\\opencode``,再否则 ``~/.config/opencode``。"""
31
- xdg = (os.environ.get("XDG_CONFIG_HOME") or "").strip()
32
- if xdg:
33
- return pathlib.Path(xdg) / "opencode"
34
- return _home_dir() / ".config" / "opencode"
35
-
36
-
37
- def _user_email_from_json_dict(data: dict[str, Any]) -> str:
38
- raw = data.get("user_email", data.get("userEmail"))
39
- if isinstance(raw, str):
40
- t = raw.strip()
41
- if t and "@" in t:
42
- return t
43
- return ""
44
-
45
-
46
- def read_user_email_from_sidecar_json_files(workspace_root: pathlib.Path) -> str:
47
- """独立 JSON 中的公司邮箱(便于用户直接编辑)。
48
-
49
- 读取顺序(后者不覆盖前者已读到的值):
50
-
51
- 1. ``<workspace>/.ohai-report/user_email.json`` — 按仓库覆盖
52
- 2. ``~/.config/opencode/ohai-report/email.json`` — 安装脚本写入的全局默认
53
- 3. ``~/.config/opencode/ohai-report-user.json`` — 旧版安装脚本,仍兼容
54
- """
55
- candidates = [
56
- workspace_root / ".ohai-report" / "user_email.json",
57
- opencode_global_config_dir() / "ohai-report" / "email.json",
58
- opencode_global_config_dir() / "ohai-report-user.json",
59
- ]
60
- for path in candidates:
61
- em = _user_email_from_json_dict(read_json(path))
62
- if em:
63
- return em
64
- return ""
65
-
66
-
67
- def is_placeholder_tracing_user_id(value: str) -> bool:
68
- """排除明显非业务用户的占位(如环境里的 USER_ID=000000)。"""
69
- t = (value or "").strip()
70
- if not t:
71
- return True
72
- low = t.lower()
73
- if low in ("unknown", "null", "undefined", "none", "n/a", "na"):
74
- return True
75
- if len(t) <= 12 and all(c == "0" for c in t):
76
- return True
77
- return False
78
-
79
-
80
- def tracing_user_id_from_env(env: dict[str, str]) -> str:
81
- """仅使用显式追踪相关变量,不把 OS 登录名当作 user_id(避免误显示)。"""
82
- for key in ("OHAI_USER_ID", "OHAI_EMPLOYEE_ID", "OPENCODE_EMPLOYEE_ID", "LANGFUSE_USER_ID", "USER_ID"):
83
- raw = (env.get(key) or "").strip()
84
- if raw and not is_placeholder_tracing_user_id(raw):
85
- return raw
86
- return ""
87
-
88
-
89
- def tracing_user_email_from_env(env: dict[str, str]) -> str:
90
- """公司/上报邮箱:显式邮箱变量优先;否则若追踪类 user id 形如邮箱则复用。"""
91
- for key in ("OHAI_USER_EMAIL", "USER_EMAIL", "GIT_AUTHOR_EMAIL"):
92
- raw = (env.get(key) or "").strip()
93
- if raw and "@" in raw:
94
- return raw
95
- for key in ("OHAI_USER_ID", "LANGFUSE_USER_ID"):
96
- raw = (env.get(key) or "").strip()
97
- if raw and "@" in raw and "." in raw.split("@", 1)[-1]:
98
- return raw
99
- return ""
100
-
101
-
102
- def _traceparent_from_env(env: dict[str, str]) -> tuple[str, str]:
103
- """解析 W3C traceparent(00-<trace32>-<parent16>-<flags>),补全 trace / span 关联。"""
104
- tp = (env.get("TRACEPARENT") or env.get("traceparent") or "").strip()
105
- if not tp:
106
- return "", ""
107
- parts = tp.split("-")
108
- if len(parts) < 4:
109
- return "", ""
110
- ver, trace, span = parts[0], parts[1], parts[2]
111
- if len(ver) != 2 or any(c not in "0123456789abcdefABCDEF" for c in ver):
112
- return "", ""
113
- if len(trace) != 32 or any(c not in "0123456789abcdefABCDEF" for c in trace):
114
- return "", ""
115
- if len(span) != 16 or any(c not in "0123456789abcdefABCDEF" for c in span):
116
- return trace.lower(), ""
117
- return trace.lower(), span.lower()
118
-
119
-
120
- def env_metadata() -> dict[str, Any]:
121
- """Read correlation IDs from env (Langfuse / OpenCode / OHAI 桥接).
122
-
123
- Langfuse 或宿主进程可将当前 trace 上下文写入下列变量,子进程执行
124
- ``ohai_report.py create --metadata auto`` 时即可带入 Issue「日志信息」。
125
- """
126
- env = os.environ
127
- session_id = (
128
- env.get("OHAI_SESSION_ID")
129
- or env.get("OPENCODE_SESSION_ID")
130
- or env.get("LANGFUSE_SESSION_ID")
131
- or env.get("SESSION_ID")
132
- or ""
133
- )
134
- trace_id = env.get("LANGFUSE_TRACE_ID") or env.get("TRACE_ID") or env.get("OTEL_TRACE_ID") or ""
135
- observation_id = (
136
- env.get("LANGFUSE_OBSERVATION_ID")
137
- or env.get("OHAI_OBSERVATION_ID")
138
- or env.get("OBSERVATION_ID")
139
- or env.get("OTEL_SPAN_ID")
140
- or ""
141
- )
142
- tp_trace, tp_span = _traceparent_from_env(dict(env))
143
- if not trace_id and tp_trace:
144
- trace_id = tp_trace
145
- if not observation_id and tp_span:
146
- observation_id = tp_span
147
- message_id = env.get("OPENCODE_MESSAGE_ID") or env.get("OHAI_MESSAGE_ID") or ""
148
- opencode_subagent = env.get("OHAI_OPENCODE_SUBAGENT") or env.get("OPENCODE_SUBAGENT") or ""
149
- model = env.get("OPENCODE_MODEL") or env.get("OHAI_MODEL") or ""
150
- return {
151
- "session_id": session_id,
152
- "trace_id": trace_id,
153
- "observation_id": observation_id,
154
- "message_id": message_id,
155
- "opencode_subagent": opencode_subagent,
156
- "model": model,
157
- "langfuse_url": env.get("LANGFUSE_URL") or env.get("LANGFUSE_TRACE_URL") or "",
158
- "user": {
159
- "id": tracing_user_id_from_env(env),
160
- "email": tracing_user_email_from_env(env),
161
- "name": (
162
- env.get("OHAI_USER_NAME")
163
- or env.get("OHAI_EMPLOYEE_ID")
164
- or env.get("OPENCODE_EMPLOYEE_ID")
165
- or env.get("USERNAME")
166
- or env.get("USER")
167
- or ""
168
- ),
169
- "team": env.get("OHAI_TEAM") or env.get("TEAM") or "",
170
- },
171
- }
172
-
173
-
174
- def _prefer_env_then_stored(env: dict[str, Any], stored: dict[str, Any], key: str) -> str:
175
- """进程内 Langfuse 注入优先于磁盘上可能过期的快照。"""
176
- ev = str(env.get(key) or "").strip()
177
- if ev:
178
- return ev
179
- return str(stored.get(key) or "").strip()
180
-
181
-
182
- def _stored_session_max_age() -> dt.timedelta:
183
- raw = (os.environ.get("OHAI_STORED_SESSION_MAX_AGE_SECONDS") or "").strip()
184
- if raw:
185
- try:
186
- seconds = int(raw)
187
- except ValueError:
188
- seconds = DEFAULT_STORED_SESSION_MAX_AGE_SECONDS
189
- if seconds > 0:
190
- return dt.timedelta(seconds=seconds)
191
- return dt.timedelta(seconds=DEFAULT_STORED_SESSION_MAX_AGE_SECONDS)
192
-
193
-
194
- def _parse_metadata_time(value: Any) -> dt.datetime | None:
195
- if not isinstance(value, str) or not value.strip():
196
- return None
197
- try:
198
- parsed = dt.datetime.fromisoformat(value.strip())
199
- except ValueError:
200
- return None
201
- if parsed.tzinfo is None:
202
- return parsed.astimezone()
203
- return parsed
204
-
205
-
206
- def _same_stored_workspace(root: pathlib.Path, stored: dict[str, Any]) -> bool:
207
- context = stored.get("context") if isinstance(stored.get("context"), dict) else {}
208
- cwd = str(context.get("cwd") or "").strip()
209
- if not cwd:
210
- return True
211
- try:
212
- return pathlib.Path(cwd).resolve() == root.resolve()
213
- except OSError:
214
- return pathlib.Path(cwd) == root
215
-
216
-
217
- def _fresh_stored_session_id(root: pathlib.Path, stored: dict[str, Any]) -> str:
218
- sid = str(stored.get("session_id") or "").strip()
219
- if not sid or not _same_stored_workspace(root, stored):
220
- return ""
221
-
222
- updated_at = _parse_metadata_time(stored.get("updated_at"))
223
- if updated_at is None:
224
- return ""
225
- age = dt.datetime.now(updated_at.tzinfo).astimezone(updated_at.tzinfo) - updated_at
226
- if age < dt.timedelta(0) or age > _stored_session_max_age():
227
- return ""
228
- return sid
229
-
230
-
231
- def resolve_metadata(root: pathlib.Path, source: str, mode: str) -> dict[str, Any]:
232
- base: dict[str, Any] = {
233
- "source": source,
234
- "agent": source,
235
- "session_id": "",
236
- "trace_id": "",
237
- "observation_id": "",
238
- "message_id": "",
239
- "opencode_subagent": "",
240
- "model": "",
241
- "langfuse_url": "",
242
- "user": {"id": "", "name": "", "team": "", "email": ""},
243
- "context": git_context(root),
244
- }
245
- if mode != "auto":
246
- return base
247
-
248
- stored = read_json(data_dir(root) / "metadata.json")
249
- env = env_metadata()
250
-
251
- stored_session_id = _fresh_stored_session_id(root, stored) if source == "opencode" else ""
252
- base["session_id"] = str(env.get("session_id") or "").strip() or stored_session_id
253
- for key in ("trace_id", "observation_id", "message_id", "opencode_subagent", "model"):
254
- base[key] = _prefer_env_then_stored(env, stored, key)
255
-
256
- lu_stored = str(stored.get("langfuse_url") or "").strip()
257
- lu_env = str(env.get("langfuse_url") or "").strip()
258
- base["langfuse_url"] = lu_stored or lu_env
259
-
260
- if base.get("trace_id") and not (base.get("langfuse_url") or "").strip():
261
- built_url = trace_url_from_env(str(base["trace_id"]))
262
- if built_url:
263
- base["langfuse_url"] = built_url
264
-
265
- stored_user = stored.get("user") if isinstance(stored.get("user"), dict) else {}
266
- env_user = env.get("user") if isinstance(env.get("user"), dict) else {}
267
- e_uid = str(env_user.get("id") or "").strip()
268
- if e_uid and is_placeholder_tracing_user_id(e_uid):
269
- e_uid = ""
270
- s_uid = str(stored_user.get("id") or "").strip()
271
- if s_uid and is_placeholder_tracing_user_id(s_uid):
272
- s_uid = ""
273
- e_mail = str(env_user.get("email") or "").strip()
274
- s_mail = str(stored_user.get("email") or "").strip()
275
- base["user"] = {
276
- "id": e_uid or s_uid,
277
- "name": str(stored_user.get("name") or env_user.get("name") or "").strip(),
278
- "team": str(stored_user.get("team") or env_user.get("team") or "").strip(),
279
- "email": e_mail or s_mail,
280
- }
281
-
282
- if not str(base["user"].get("email") or "").strip():
283
- fe = read_user_email_from_sidecar_json_files(root)
284
- if fe:
285
- base["user"]["email"] = fe
286
-
287
- stored_context = stored.get("context") if isinstance(stored.get("context"), dict) else {}
288
- base["context"].update({k: v for k, v in stored_context.items() if v})
289
- return base
290
-
291
-
292
- def update_metadata(root: pathlib.Path, value: MetadataUpdateInput) -> pathlib.Path:
293
- path = data_dir(root) / "metadata.json"
294
- stored = read_json(path)
295
- context = stored.get("context") if isinstance(stored.get("context"), dict) else {}
296
- context.update(git_context(root))
297
- if value.cwd:
298
- context["cwd"] = value.cwd
299
-
300
- env_meta = env_metadata()
301
-
302
- user = stored.get("user") if isinstance(stored.get("user"), dict) else {}
303
- env_user = env_meta["user"]
304
- user.update({k: v for k, v in env_user.items() if v})
305
- if (value.user_id or "").strip():
306
- tid = str(value.user_id).strip()
307
- user["id"] = tid
308
- if "@" in tid and "." in tid.split("@", 1)[-1]:
309
- user["email"] = tid
310
-
311
- sid = (value.session_id or "").strip() or str(env_meta.get("session_id") or "").strip()
312
- if not sid and value.source == "opencode":
313
- sid = _fresh_stored_session_id(root, stored)
314
- tid = (value.trace_id or "").strip() or str(env_meta.get("trace_id") or "").strip()
315
- if not tid:
316
- tid = str(stored.get("trace_id") or "").strip()
317
- oid = (value.observation_id or "").strip() or str(env_meta.get("observation_id") or "").strip()
318
- if not oid:
319
- oid = str(stored.get("observation_id") or "").strip()
320
- if value.clear_message_subagent:
321
- mid = ""
322
- sub = ""
323
- else:
324
- mid = (value.message_id or "").strip() or str(env_meta.get("message_id") or "").strip()
325
- if not mid:
326
- mid = str(stored.get("message_id") or "").strip()
327
- sub = (value.opencode_subagent or "").strip() or str(env_meta.get("opencode_subagent") or "").strip()
328
- if not sub:
329
- sub = str(stored.get("opencode_subagent") or "").strip()
330
-
331
- mdl_cli = (value.model or "").strip()
332
- if mdl_cli:
333
- mdl = mdl_cli
334
- else:
335
- mdl = str(env_meta.get("model") or "").strip()
336
- if not mdl:
337
- mdl = str(stored.get("model") or "").strip()
338
-
339
- if not str(user.get("email") or "").strip():
340
- fe = read_user_email_from_sidecar_json_files(root)
341
- if fe:
342
- user["email"] = fe
343
-
344
- next_value = {
345
- **stored,
346
- "source": value.source,
347
- "agent": value.source,
348
- "session_id": sid,
349
- "trace_id": tid,
350
- "observation_id": oid,
351
- "message_id": mid,
352
- "opencode_subagent": sub,
353
- "model": mdl,
354
- "langfuse_url": (value.langfuse_url or "").strip() or stored.get("langfuse_url", ""),
355
- "user": user,
356
- "context": context,
357
- "updated_at": dt.datetime.now(dt.timezone.utc).astimezone().isoformat(timespec="seconds"),
358
- }
359
- write_json(path, next_value)
360
- return path
@@ -1 +0,0 @@
1
- """Observability helpers (Langfuse URL generation, trace hints)."""
@@ -1,38 +0,0 @@
1
- """Langfuse URL helpers — trace links without embedding logs in Issues."""
2
-
3
- from __future__ import annotations
4
-
5
- import os
6
- import urllib.parse
7
-
8
-
9
- def build_trace_url(base_url: str, project_id: str, trace_id: str) -> str:
10
- """Return a canonical Langfuse trace URL for typical Cloud / self-hosted UI paths."""
11
- root = base_url.strip().rstrip("/")
12
- if not root or not project_id or not trace_id:
13
- return ""
14
- enc_trace = urllib.parse.quote(trace_id, safe="")
15
- return f"{root}/project/{project_id}/traces/{enc_trace}"
16
-
17
-
18
- def trace_url_from_env(trace_id: str) -> str:
19
- """If LANGFUSE_BASE_URL (or HOST) and LANGFUSE_PROJECT_ID are set, derive trace URL."""
20
- if not trace_id:
21
- return ""
22
- raw_base = (
23
- os.environ.get("LANGFUSE_BASE_URL")
24
- or os.environ.get("LANGFUSE_HOST")
25
- or os.environ.get("LANGFUSE_ORIGIN")
26
- or ""
27
- )
28
- project_id = (
29
- os.environ.get("LANGFUSE_PROJECT_ID")
30
- or os.environ.get("LANGFUSE_PUBLIC_KEY")
31
- or ""
32
- ).strip()
33
- if not raw_base.strip() or not project_id:
34
- return ""
35
- root = raw_base.strip().rstrip("/")
36
- if root.endswith("/traces"):
37
- return f"{root}/{urllib.parse.quote(trace_id, safe='')}"
38
- return build_trace_url(root, project_id, trace_id)
@@ -1,64 +0,0 @@
1
- """Payload assembly for issue reports."""
2
-
3
- from __future__ import annotations
4
-
5
- import datetime as dt
6
- import pathlib
7
- from typing import Any
8
-
9
- from .metadata import resolve_metadata
10
- from .schema import CreateIssueInput
11
-
12
-
13
- def generate_issue_id(now: dt.datetime) -> str:
14
- return f"AI-DEV-{now.strftime('%Y%m%d%H%M%S%f')}"
15
-
16
-
17
- def build_issue_payload(root: pathlib.Path, value: CreateIssueInput) -> dict[str, Any]:
18
- now = dt.datetime.now(dt.timezone.utc).astimezone()
19
- metadata = resolve_metadata(root, value.source, value.metadata)
20
- issue = dict(value.issue)
21
- if value.metadata == "auto":
22
- om = str(metadata.get("model") or "").strip()
23
- if om:
24
- issue["model"] = om
25
- user = metadata["user"] if isinstance(metadata.get("user"), dict) else {}
26
- uid = str(user.get("id") or "").strip()
27
- if not uid:
28
- uid = str(issue.get("user_id") or "").strip()
29
- uem = str(user.get("email") or "").strip()
30
- if not uem:
31
- uem = str(issue.get("user_email") or "").strip()
32
- if not uem and uid and "@" in uid and "." in uid.split("@", 1)[-1]:
33
- uem = uid
34
- obs = str(metadata.get("observation_id") or "").strip()
35
- mid = str(metadata.get("message_id") or "").strip()
36
- sub = str(metadata.get("opencode_subagent") or "").strip()
37
- user_out = dict(user)
38
- user_out.setdefault("email", "")
39
- if uem:
40
- user_out["email"] = uem
41
- return {
42
- "issue_id": generate_issue_id(now),
43
- "created_at": now.isoformat(timespec="seconds"),
44
- "source": metadata["source"],
45
- "agent": metadata["agent"],
46
- "session_id": metadata["session_id"],
47
- "trace_id": metadata["trace_id"],
48
- "observation_id": obs,
49
- "message_id": mid,
50
- "opencode_subagent": sub,
51
- "user_id": uid,
52
- "user_email": uem,
53
- "langfuse_url": metadata["langfuse_url"],
54
- "user": user_out,
55
- "issue": issue,
56
- "context": metadata["context"],
57
- "privacy": {
58
- "uploads_logs": False,
59
- "uploads_diff": False,
60
- "uploads_transcript": False,
61
- "note": "Issue only stores template fields and correlation IDs in the log section.",
62
- },
63
- "status": "created-local",
64
- }
@@ -1,80 +0,0 @@
1
- """Data contracts and validation for ohai-report."""
2
-
3
- from __future__ import annotations
4
-
5
- from dataclasses import dataclass
6
- import json
7
- import pathlib
8
- from typing import Any
9
-
10
-
11
- def schema_path() -> pathlib.Path:
12
- return pathlib.Path(__file__).resolve().parents[1] / "schemas" / "report_issue.schema.json"
13
-
14
-
15
- def load_issue_schema() -> dict[str, Any]:
16
- with schema_path().open("r", encoding="utf-8") as fp:
17
- value = json.load(fp)
18
- if not isinstance(value, dict) or not isinstance(value.get("fields"), dict):
19
- raise ValueError("invalid issue schema: missing fields object")
20
- return value
21
-
22
-
23
- def issue_fields() -> dict[str, dict[str, Any]]:
24
- schema = load_issue_schema()
25
- return schema["fields"]
26
-
27
-
28
- @dataclass(frozen=True)
29
- class CreateIssueInput:
30
- source: str
31
- issue: dict[str, Any]
32
- metadata: str
33
-
34
-
35
- @dataclass(frozen=True)
36
- class MetadataUpdateInput:
37
- source: str
38
- session_id: str = ""
39
- trace_id: str = ""
40
- langfuse_url: str = ""
41
- observation_id: str = ""
42
- user_id: str = ""
43
- message_id: str = ""
44
- opencode_subagent: str = ""
45
- model: str = ""
46
- clear_message_subagent: bool = False
47
- cwd: str = ""
48
-
49
-
50
- def validate_issue(issue: dict[str, Any]) -> list[str]:
51
- errors: list[str] = []
52
-
53
- for name, spec in issue_fields().items():
54
- item = issue.get(name)
55
- if spec.get("required") and not item:
56
- errors.append(f"missing required field: {name}")
57
- continue
58
- if not item:
59
- continue
60
-
61
- if spec.get("type") == "string" and not isinstance(item, str):
62
- errors.append(f"invalid type for field {name}: expected string")
63
- continue
64
- enum = spec.get("enum")
65
- if enum and item not in enum:
66
- errors.append(f"invalid value for field {name}: {item}")
67
- max_length = spec.get("maxLength")
68
- if isinstance(max_length, int) and isinstance(item, str) and len(item) > max_length:
69
- errors.append(f"field too long: {name} maxLength={max_length}")
70
- return errors
71
-
72
-
73
- def validate_create_input(value: CreateIssueInput) -> list[str]:
74
- errors: list[str] = []
75
- if not value.source:
76
- errors.append("missing required field: source")
77
- errors.extend(validate_issue(value.issue))
78
- if value.metadata not in {"auto", "none"}:
79
- errors.append(f"invalid metadata mode: {value.metadata}")
80
- return errors
@@ -1,15 +0,0 @@
1
- """Sink result type for issue persistence backends."""
2
-
3
- from __future__ import annotations
4
-
5
- import pathlib
6
- from dataclasses import dataclass, field
7
- from typing import Any
8
-
9
-
10
- @dataclass(frozen=True)
11
- class SinkResult:
12
- issue_id: str
13
- path: pathlib.Path | None = None
14
- url: str = ""
15
- extra: dict[str, Any] = field(default_factory=dict)