devin-qa-pack 0.1.2__tar.gz → 0.2.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.
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/PKG-INFO +25 -1
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/README.md +24 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/pyproject.toml +1 -1
- devin_qa_pack-0.2.0/src/devin_qa_pack/__init__.py +1 -0
- devin_qa_pack-0.2.0/src/devin_qa_pack/adapters/base.py +46 -0
- devin_qa_pack-0.2.0/src/devin_qa_pack/adapters/devin.py +23 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/claims.py +36 -59
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/cli.py +117 -15
- devin_qa_pack-0.2.0/src/devin_qa_pack/evidence/__init__.py +364 -0
- devin_qa_pack-0.2.0/src/devin_qa_pack/evidence/codex_cli.py +477 -0
- devin_qa_pack-0.2.0/src/devin_qa_pack/evidence/copilot_cli.py +344 -0
- devin_qa_pack-0.2.0/src/devin_qa_pack/evidence/devin_cli.py +162 -0
- devin_qa_pack-0.2.0/src/devin_qa_pack/evidence/vscode_chat.py +546 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/html_report.py +27 -4
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/intent.py +61 -59
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/mcp_server.py +76 -4
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/report.py +66 -2
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/session_end.py +8 -4
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/verify.py +19 -23
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack.egg-info/PKG-INFO +25 -1
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack.egg-info/SOURCES.txt +7 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_claims.py +20 -16
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_cli.py +10 -0
- devin_qa_pack-0.2.0/tests/test_evidence.py +821 -0
- devin_qa_pack-0.2.0/tests/test_evidence_corpus.py +248 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_intent.py +42 -46
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_mcp.py +18 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_verify.py +16 -14
- devin_qa_pack-0.1.2/src/devin_qa_pack/__init__.py +0 -1
- devin_qa_pack-0.1.2/src/devin_qa_pack/adapters/base.py +0 -26
- devin_qa_pack-0.1.2/src/devin_qa_pack/adapters/devin.py +0 -28
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/setup.cfg +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/adapters/__init__.py +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/adapters/aider.py +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/adapters/claude_code.py +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/adapters/mcp.py +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack/paths.py +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack.egg-info/dependency_links.txt +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack.egg-info/entry_points.txt +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack.egg-info/requires.txt +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/src/devin_qa_pack.egg-info/top_level.txt +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_adapters.py +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_html_report.py +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_offline_core.py +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_paths.py +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_precommit_hook.py +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_release_workflow.py +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_report.py +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_session_end.py +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_skill.py +0 -0
- {devin_qa_pack-0.1.2 → devin_qa_pack-0.2.0}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devin-qa-pack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: QA audit for Devin sessions: verifies deliverable claims (tests, commits, files, pushes) against tool_call_state ground truth.
|
|
5
5
|
Author: Icaro0310
|
|
6
6
|
License: MIT
|
|
@@ -173,6 +173,30 @@ evidence and the source excerpt for every checked claim.
|
|
|
173
173
|
`$XDG_DATA_HOME/devin/cli/sessions.db` on Linux (default
|
|
174
174
|
`~/.local/share/devin/cli/sessions.db`). Always read-only.
|
|
175
175
|
|
|
176
|
+
### Surfaces (`--surface`)
|
|
177
|
+
|
|
178
|
+
`audit`/`report` are surface-agnostic. Without an explicit
|
|
179
|
+
`--sessions-db` the default `--surface auto` discovers **every**
|
|
180
|
+
supported agent store — Devin CLI plus Copilot CLI
|
|
181
|
+
(`$COPILOT_DIR`/`$COPILOT_HOME`/`~/.copilot` session-state), Codex CLI
|
|
182
|
+
(`$CODEX_HOME`/`~/.codex` state db + rollout JSONL) and VS Code chat
|
|
183
|
+
(`User/workspaceStorage|globalStorage` chat sessions, incl.
|
|
184
|
+
`.vscode-server`). A surface whose store is absent simply contributes
|
|
185
|
+
nothing — discovery never errors on a missing store.
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
devin-qa-pack audit --all # every detected surface
|
|
189
|
+
devin-qa-pack audit --surface copilot-cli --all
|
|
190
|
+
devin-qa-pack audit --surface auto --session vscode-chat:abc123
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
In provider mode session ids are namespaced `surface:native_id`
|
|
194
|
+
(e.g. `codex-cli:019ae…`); `--session` accepts a bare native prefix or a
|
|
195
|
+
`surface:`-scoped ref. Passing `--sessions-db` *alone* keeps the Devin
|
|
196
|
+
fast path (bare ids, no other surfaces) — pass `--surface` to combine
|
|
197
|
+
a pinned store with the rest. The `qa_audit` MCP tool takes the same
|
|
198
|
+
`surface` argument.
|
|
199
|
+
|
|
176
200
|
No Devin installed? Try it on a synthetic fixture:
|
|
177
201
|
|
|
178
202
|
```bash
|
|
@@ -158,6 +158,30 @@ evidence and the source excerpt for every checked claim.
|
|
|
158
158
|
`$XDG_DATA_HOME/devin/cli/sessions.db` on Linux (default
|
|
159
159
|
`~/.local/share/devin/cli/sessions.db`). Always read-only.
|
|
160
160
|
|
|
161
|
+
### Surfaces (`--surface`)
|
|
162
|
+
|
|
163
|
+
`audit`/`report` are surface-agnostic. Without an explicit
|
|
164
|
+
`--sessions-db` the default `--surface auto` discovers **every**
|
|
165
|
+
supported agent store — Devin CLI plus Copilot CLI
|
|
166
|
+
(`$COPILOT_DIR`/`$COPILOT_HOME`/`~/.copilot` session-state), Codex CLI
|
|
167
|
+
(`$CODEX_HOME`/`~/.codex` state db + rollout JSONL) and VS Code chat
|
|
168
|
+
(`User/workspaceStorage|globalStorage` chat sessions, incl.
|
|
169
|
+
`.vscode-server`). A surface whose store is absent simply contributes
|
|
170
|
+
nothing — discovery never errors on a missing store.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
devin-qa-pack audit --all # every detected surface
|
|
174
|
+
devin-qa-pack audit --surface copilot-cli --all
|
|
175
|
+
devin-qa-pack audit --surface auto --session vscode-chat:abc123
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
In provider mode session ids are namespaced `surface:native_id`
|
|
179
|
+
(e.g. `codex-cli:019ae…`); `--session` accepts a bare native prefix or a
|
|
180
|
+
`surface:`-scoped ref. Passing `--sessions-db` *alone* keeps the Devin
|
|
181
|
+
fast path (bare ids, no other surfaces) — pass `--surface` to combine
|
|
182
|
+
a pinned store with the rest. The `qa_audit` MCP tool takes the same
|
|
183
|
+
`surface` argument.
|
|
184
|
+
|
|
161
185
|
No Devin installed? Try it on a synthetic fixture:
|
|
162
186
|
|
|
163
187
|
```bash
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devin-qa-pack"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "QA audit for Devin sessions: verifies deliverable claims (tests, commits, files, pushes) against tool_call_state ground truth."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""The normalized session view every adapter produces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from devin_qa_pack.claims import Claim, extract_claims
|
|
8
|
+
from devin_qa_pack.evidence import SessionEvidence
|
|
9
|
+
from devin_qa_pack.verify import ParsedToolCall, parse_tool_calls
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class SourceSession:
|
|
14
|
+
"""One session's claims plus its recorded tool calls.
|
|
15
|
+
|
|
16
|
+
``raw_call_count`` is the number of evidence rows *before* decoding —
|
|
17
|
+
when rows exist but none parse, ground truth is unreadable and claims
|
|
18
|
+
resolve ``unverifiable`` rather than ``disputed``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
source: str # adapter name: "devin-cli" | "aider" | "claude-code" | ...
|
|
22
|
+
session_id: str
|
|
23
|
+
title: str | None
|
|
24
|
+
working_directory: str
|
|
25
|
+
claims: tuple[Claim, ...]
|
|
26
|
+
calls: tuple[ParsedToolCall, ...]
|
|
27
|
+
raw_call_count: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def source_view(
|
|
31
|
+
evidence: SessionEvidence,
|
|
32
|
+
session_id: str | None = None,
|
|
33
|
+
title: str | None = None,
|
|
34
|
+
) -> SourceSession:
|
|
35
|
+
"""Bridge :class:`SessionEvidence` into the audit-side view: claims
|
|
36
|
+
extracted from ``messages``, payloads parsed from ``tool_calls``."""
|
|
37
|
+
sid = session_id or evidence.native_id
|
|
38
|
+
return SourceSession(
|
|
39
|
+
source=evidence.surface,
|
|
40
|
+
session_id=sid,
|
|
41
|
+
title=title,
|
|
42
|
+
working_directory=evidence.working_dir or "",
|
|
43
|
+
claims=tuple(extract_claims(evidence.messages, sid)),
|
|
44
|
+
calls=tuple(parse_tool_calls(evidence.tool_calls)),
|
|
45
|
+
raw_call_count=len(evidence.tool_calls),
|
|
46
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""The Devin adapter — the native ``sessions.db`` source.
|
|
2
|
+
|
|
3
|
+
Claims come from ``message_nodes`` (agent-role text); ground truth comes
|
|
4
|
+
from ``tool_call_state`` rows decoded via devin-internals-spec parsers —
|
|
5
|
+
both through the devin-cli evidence provider.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import replace
|
|
11
|
+
|
|
12
|
+
from devin_internals.parsers import SessionsStore
|
|
13
|
+
from devin_internals.parsers.sessions import Session
|
|
14
|
+
|
|
15
|
+
from devin_qa_pack.adapters.base import SourceSession, source_view
|
|
16
|
+
from devin_qa_pack.evidence.devin_cli import session_evidence
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def session_view(store: SessionsStore, session: Session) -> SourceSession:
|
|
20
|
+
"""The store-backed view: bare session id + title (the legacy path —
|
|
21
|
+
provider audits namespace ids as ``devin-cli:<id>`` instead)."""
|
|
22
|
+
view = source_view(session_evidence(store, session), session.id)
|
|
23
|
+
return replace(view, title=session.title)
|
|
@@ -1,25 +1,22 @@
|
|
|
1
|
-
"""Extract deliverable claims from a session's
|
|
1
|
+
"""Extract deliverable claims from a session's evidence messages.
|
|
2
2
|
|
|
3
3
|
A *claim* is an assertion the agent made about delivered work: tests
|
|
4
4
|
passed, a commit exists, a file was created/changed, work was pushed.
|
|
5
5
|
Only agent-role messages are scanned — a user prompt quoting the same
|
|
6
6
|
words is not a delivery claim.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
that is not JSON at all is scanned as plain text.
|
|
8
|
+
The input is the canonical :class:`EvidenceMessage` — store-specific
|
|
9
|
+
payload decoding (``chat_message`` JSON for the Devin store) happens in
|
|
10
|
+
the evidence providers, so this module only ever sees plain role+text.
|
|
12
11
|
"""
|
|
13
12
|
|
|
14
13
|
from __future__ import annotations
|
|
15
14
|
|
|
16
|
-
import json
|
|
17
15
|
import re
|
|
18
16
|
from collections.abc import Iterable
|
|
19
17
|
from dataclasses import dataclass
|
|
20
|
-
from typing import Any
|
|
21
18
|
|
|
22
|
-
from
|
|
19
|
+
from devin_qa_pack.evidence import EvidenceMessage, normalize_role
|
|
23
20
|
|
|
24
21
|
TESTS = "tests"
|
|
25
22
|
COMMIT = "commit"
|
|
@@ -101,41 +98,10 @@ class Claim:
|
|
|
101
98
|
kind: str
|
|
102
99
|
detail: str
|
|
103
100
|
session_id: str
|
|
104
|
-
node_id: int
|
|
101
|
+
node_id: int | str
|
|
105
102
|
excerpt: str
|
|
106
103
|
|
|
107
104
|
|
|
108
|
-
def _decode_message(chat_message: str) -> tuple[str | None, str]:
|
|
109
|
-
"""Return ``(role, text)``. Non-JSON payloads become ``(None, raw)``."""
|
|
110
|
-
try:
|
|
111
|
-
data = json.loads(chat_message)
|
|
112
|
-
except (json.JSONDecodeError, TypeError):
|
|
113
|
-
return None, chat_message
|
|
114
|
-
if isinstance(data, str):
|
|
115
|
-
return None, data
|
|
116
|
-
if not isinstance(data, dict):
|
|
117
|
-
return None, ""
|
|
118
|
-
role = data.get("role") or data.get("author") or data.get("sender")
|
|
119
|
-
return (str(role).lower() if role is not None else None), _text_of(data)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def _text_of(data: dict[str, Any]) -> str:
|
|
123
|
-
for key in ("text", "content", "message", "body", "markdown"):
|
|
124
|
-
value = data.get(key)
|
|
125
|
-
if isinstance(value, str):
|
|
126
|
-
return value
|
|
127
|
-
if isinstance(value, list):
|
|
128
|
-
parts = [
|
|
129
|
-
block.get("text", "")
|
|
130
|
-
for block in value
|
|
131
|
-
if isinstance(block, dict)
|
|
132
|
-
]
|
|
133
|
-
text = "\n".join(p for p in parts if p)
|
|
134
|
-
if text:
|
|
135
|
-
return text
|
|
136
|
-
return ""
|
|
137
|
-
|
|
138
|
-
|
|
139
105
|
def _excerpt(line: str) -> str:
|
|
140
106
|
line = " ".join(line.split())
|
|
141
107
|
return line if len(line) <= _MAX_EXCERPT else line[: _MAX_EXCERPT - 1] + "…"
|
|
@@ -176,7 +142,7 @@ def _claims_in_line(line: str) -> Iterable[tuple[str, str]]:
|
|
|
176
142
|
|
|
177
143
|
def _collect(
|
|
178
144
|
session_id: str,
|
|
179
|
-
node_id: int,
|
|
145
|
+
node_id: int | str,
|
|
180
146
|
text: str,
|
|
181
147
|
claims: list[Claim],
|
|
182
148
|
seen: set[tuple[str, str]],
|
|
@@ -197,15 +163,28 @@ def _collect(
|
|
|
197
163
|
)
|
|
198
164
|
|
|
199
165
|
|
|
200
|
-
def extract_claims(
|
|
201
|
-
|
|
166
|
+
def extract_claims(
|
|
167
|
+
messages: Iterable[EvidenceMessage], session_id: str = ""
|
|
168
|
+
) -> list[Claim]:
|
|
169
|
+
"""All claims across ``messages``, deduplicated by ``(kind, detail)``.
|
|
170
|
+
|
|
171
|
+
``Claim.node_id`` cites the surface's real node id
|
|
172
|
+
(``EvidenceMessage.source_id``, e.g. the devin-cli
|
|
173
|
+
``message_nodes.node_id``) when the provider supplies one; otherwise
|
|
174
|
+
the message's positional index — same convention as
|
|
175
|
+
:func:`claims_from_pairs`.
|
|
176
|
+
"""
|
|
202
177
|
claims: list[Claim] = []
|
|
203
178
|
seen: set[tuple[str, str]] = set()
|
|
204
|
-
for
|
|
205
|
-
role
|
|
206
|
-
if role in _SKIP_ROLES:
|
|
179
|
+
for position, message in enumerate(messages, 1):
|
|
180
|
+
if message.role in _SKIP_ROLES:
|
|
207
181
|
continue
|
|
208
|
-
|
|
182
|
+
node_id = (
|
|
183
|
+
message.source_id
|
|
184
|
+
if message.source_id is not None
|
|
185
|
+
else position
|
|
186
|
+
)
|
|
187
|
+
_collect(session_id, node_id, message.text, claims, seen)
|
|
209
188
|
return claims
|
|
210
189
|
|
|
211
190
|
|
|
@@ -213,15 +192,13 @@ def claims_from_pairs(
|
|
|
213
192
|
pairs: Iterable[tuple[str | None, str]],
|
|
214
193
|
session_id: str,
|
|
215
194
|
) -> list[Claim]:
|
|
216
|
-
"""Claims over pre-decoded ``(role, text)`` pairs — the adapter input.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
_collect(session_id, node_id, text, claims, seen)
|
|
227
|
-
return claims
|
|
195
|
+
"""Claims over pre-decoded ``(role, text)`` pairs — the adapter input."""
|
|
196
|
+
return extract_claims(
|
|
197
|
+
(
|
|
198
|
+
EvidenceMessage(
|
|
199
|
+
role=normalize_role(role), text=text, ts_ms=None
|
|
200
|
+
)
|
|
201
|
+
for role, text in pairs
|
|
202
|
+
),
|
|
203
|
+
session_id,
|
|
204
|
+
)
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"""Thin CLI wrapper — all logic lives in the library modules.
|
|
2
2
|
|
|
3
|
-
``devin-qa-pack audit`` reads
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
``devin-qa-pack audit`` reads session evidence through surface
|
|
4
|
+
providers (``--surface``, default ``auto`` = every detected store:
|
|
5
|
+
devin-cli sessions.db, copilot-cli, codex-cli, vscode-chat), verifies
|
|
6
|
+
deliverable claims against tool-call ground truth and prints a verdict
|
|
7
|
+
per session. An explicit ``--sessions-db`` alone keeps the Devin fast
|
|
8
|
+
path (bare ids, exactly the pre-surface behavior). Read-only. Exit
|
|
9
|
+
codes: 0 = every audited session PASS · 1 = audit ran, some session
|
|
10
|
+
PARTIAL/UNVERIFIED · 2 = the audit could not run (missing/unknown
|
|
11
|
+
store, unknown session, or no session matched the scope).
|
|
9
12
|
|
|
10
13
|
``devin-qa-pack session-end`` is the SessionEnd hook variant: it audits
|
|
11
14
|
only the session that just ended, writes the verdict to a JSON side
|
|
@@ -30,6 +33,13 @@ from devin_internals.parsers import SessionsStore
|
|
|
30
33
|
from devin_internals.parsers.sessions import Session
|
|
31
34
|
from devin_internals.schema import SchemaError
|
|
32
35
|
|
|
36
|
+
from devin_qa_pack import __version__
|
|
37
|
+
from devin_qa_pack.evidence import (
|
|
38
|
+
Surface,
|
|
39
|
+
collect_evidences,
|
|
40
|
+
match_evidence,
|
|
41
|
+
)
|
|
42
|
+
from devin_qa_pack.evidence.devin_cli import session_evidence
|
|
33
43
|
from devin_qa_pack.html_report import render_html
|
|
34
44
|
from devin_qa_pack.intent import (
|
|
35
45
|
FLAGGED,
|
|
@@ -37,14 +47,17 @@ from devin_qa_pack.intent import (
|
|
|
37
47
|
audit_intent,
|
|
38
48
|
intent_dict,
|
|
39
49
|
render_intent,
|
|
50
|
+
workspace_dir_names,
|
|
40
51
|
)
|
|
41
52
|
from devin_qa_pack.paths import default_sessions_db
|
|
42
53
|
from devin_qa_pack.report import (
|
|
43
54
|
PASS,
|
|
44
55
|
audit_all,
|
|
56
|
+
audit_evidence,
|
|
45
57
|
audit_session,
|
|
46
58
|
audit_source,
|
|
47
59
|
audits_payload,
|
|
60
|
+
filter_evidences,
|
|
48
61
|
render_text,
|
|
49
62
|
)
|
|
50
63
|
from devin_qa_pack.session_end import run_session_end, summary_line
|
|
@@ -56,12 +69,26 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
56
69
|
description="Audit Devin sessions: verify deliverable claims "
|
|
57
70
|
"(tests, commits, files, pushes) against tool_call_state ground truth.",
|
|
58
71
|
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
74
|
+
)
|
|
59
75
|
sub = parser.add_subparsers(dest="command")
|
|
60
76
|
audit = sub.add_parser("audit", help="verify claims per session")
|
|
61
77
|
audit.add_argument(
|
|
62
78
|
"--sessions-db",
|
|
63
79
|
metavar="PATH",
|
|
64
|
-
help="path to sessions.db (default: auto-detect Devin data dir)"
|
|
80
|
+
help="path to sessions.db (default: auto-detect Devin data dir); "
|
|
81
|
+
"on its own this stays the Devin fast path — pass --surface to "
|
|
82
|
+
"combine it with other surfaces",
|
|
83
|
+
)
|
|
84
|
+
audit.add_argument(
|
|
85
|
+
"--surface",
|
|
86
|
+
choices=Surface.CHOICES,
|
|
87
|
+
default=None,
|
|
88
|
+
metavar="S",
|
|
89
|
+
help="evidence surface: devin-cli | copilot-cli | codex-cli | "
|
|
90
|
+
"vscode-chat | auto (default: auto — every detected store; "
|
|
91
|
+
"provider-mode ids are namespaced as surface:native_id)",
|
|
65
92
|
)
|
|
66
93
|
scope = audit.add_mutually_exclusive_group(required=True)
|
|
67
94
|
scope.add_argument("--session", metavar="ID", help="audit one session "
|
|
@@ -108,6 +135,15 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
108
135
|
metavar="PATH",
|
|
109
136
|
help="path to sessions.db (default: auto-detect Devin data dir)",
|
|
110
137
|
)
|
|
138
|
+
report.add_argument(
|
|
139
|
+
"--surface",
|
|
140
|
+
choices=Surface.CHOICES,
|
|
141
|
+
default=None,
|
|
142
|
+
metavar="S",
|
|
143
|
+
help="evidence surface to report on (default: auto — every "
|
|
144
|
+
"detected store; explicit --sessions-db alone keeps the Devin "
|
|
145
|
+
"fast path)",
|
|
146
|
+
)
|
|
111
147
|
report.add_argument(
|
|
112
148
|
"--out", metavar="PATH", default="qa-report.html",
|
|
113
149
|
help="HTML output path (default: ./qa-report.html)")
|
|
@@ -223,6 +259,60 @@ def _audit_scope(store: SessionsStore, args: argparse.Namespace):
|
|
|
223
259
|
return audit_all(store, limit=getattr(args, "limit", None), **kw)
|
|
224
260
|
|
|
225
261
|
|
|
262
|
+
def _provider_mode(args: argparse.Namespace) -> bool:
|
|
263
|
+
"""True when audit goes through evidence providers rather than the
|
|
264
|
+
legacy sessions.db path: an explicit ``--surface``, or auto mode
|
|
265
|
+
without a pinned ``--sessions-db``."""
|
|
266
|
+
return getattr(args, "surface", None) is not None or (
|
|
267
|
+
not getattr(args, "sessions_db", None)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _provider_audits(args: argparse.Namespace, kw: dict):
|
|
272
|
+
"""``(audits, label)`` for provider mode; ``(None, None)`` after a
|
|
273
|
+
printed error."""
|
|
274
|
+
surface = args.surface or Surface.AUTO
|
|
275
|
+
evidences, warnings = collect_evidences(
|
|
276
|
+
surface, sessions_db=getattr(args, "sessions_db", None)
|
|
277
|
+
)
|
|
278
|
+
for warning in warnings:
|
|
279
|
+
print(f"warning: {warning}", file=sys.stderr)
|
|
280
|
+
session_arg = getattr(args, "session", None)
|
|
281
|
+
if session_arg:
|
|
282
|
+
found = match_evidence(evidences, session_arg)
|
|
283
|
+
if isinstance(found, list):
|
|
284
|
+
# ref (workspace-hash-bearing canonical id or ephemeral key)
|
|
285
|
+
# is selectable; surface:native_id alone can repeat.
|
|
286
|
+
refs = ", ".join(
|
|
287
|
+
e.ref or f"{e.surface}:{e.native_id}" for e in found
|
|
288
|
+
)
|
|
289
|
+
print(
|
|
290
|
+
f"error: session '{session_arg}' is ambiguous: {refs}",
|
|
291
|
+
file=sys.stderr,
|
|
292
|
+
)
|
|
293
|
+
return None, None
|
|
294
|
+
if found is None:
|
|
295
|
+
print(
|
|
296
|
+
f"error: unknown session '{session_arg}'", file=sys.stderr
|
|
297
|
+
)
|
|
298
|
+
return None, None
|
|
299
|
+
evidences = [found]
|
|
300
|
+
else:
|
|
301
|
+
evidences = filter_evidences(
|
|
302
|
+
evidences,
|
|
303
|
+
within=getattr(args, "within", None),
|
|
304
|
+
limit=getattr(args, "limit", None),
|
|
305
|
+
)
|
|
306
|
+
db_arg = getattr(args, "sessions_db", None)
|
|
307
|
+
label = (
|
|
308
|
+
str(Path(db_arg).expanduser())
|
|
309
|
+
if db_arg
|
|
310
|
+
else ("surfaces: auto" if surface == Surface.AUTO
|
|
311
|
+
else f"surface: {surface}")
|
|
312
|
+
)
|
|
313
|
+
return [audit_evidence(e, **kw) for e in evidences], label
|
|
314
|
+
|
|
315
|
+
|
|
226
316
|
def _load_transcript(path_arg: str, fmt: str | None, cwd: str | None):
|
|
227
317
|
"""Resolve ``--transcript`` into a SourceSession via the adapters."""
|
|
228
318
|
from devin_qa_pack.adapters import aider, claude_code
|
|
@@ -286,6 +376,12 @@ def _cmd_audit(args: argparse.Namespace) -> int:
|
|
|
286
376
|
)
|
|
287
377
|
]
|
|
288
378
|
db = Path(args.transcript).expanduser()
|
|
379
|
+
elif _provider_mode(args):
|
|
380
|
+
kw = {"online": args.online,
|
|
381
|
+
"allow_domains": tuple(args.allow_domain)}
|
|
382
|
+
audits, db = _provider_audits(args, kw)
|
|
383
|
+
if audits is None:
|
|
384
|
+
return 2
|
|
289
385
|
else:
|
|
290
386
|
db, store = _open_store(args.sessions_db)
|
|
291
387
|
if store is None:
|
|
@@ -315,13 +411,20 @@ def _cmd_audit(args: argparse.Namespace) -> int:
|
|
|
315
411
|
|
|
316
412
|
|
|
317
413
|
def _cmd_report(args: argparse.Namespace) -> int:
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
audits = _audit_scope(store, args)
|
|
414
|
+
if _provider_mode(args):
|
|
415
|
+
kw = {"online": args.online,
|
|
416
|
+
"allow_domains": tuple(args.allow_domain)}
|
|
417
|
+
audits, db = _provider_audits(args, kw)
|
|
323
418
|
if audits is None:
|
|
324
419
|
return 2
|
|
420
|
+
else:
|
|
421
|
+
db, store = _open_store(args.sessions_db)
|
|
422
|
+
if store is None:
|
|
423
|
+
return 2
|
|
424
|
+
with store:
|
|
425
|
+
audits = _audit_scope(store, args)
|
|
426
|
+
if audits is None:
|
|
427
|
+
return 2
|
|
325
428
|
out = Path(args.out).expanduser()
|
|
326
429
|
try:
|
|
327
430
|
out.write_text(render_html(audits, db), encoding="utf-8")
|
|
@@ -342,9 +445,8 @@ def _cmd_intent(args: argparse.Namespace) -> int:
|
|
|
342
445
|
if session is None:
|
|
343
446
|
return 2
|
|
344
447
|
audit = audit_intent(
|
|
345
|
-
session,
|
|
346
|
-
|
|
347
|
-
store.tool_call_state(session.id),
|
|
448
|
+
session_evidence(store, session),
|
|
449
|
+
repo_names=workspace_dir_names(session),
|
|
348
450
|
)
|
|
349
451
|
if args.json:
|
|
350
452
|
print(json.dumps(intent_dict(audit), indent=2))
|