devtime-ei 0.1.3__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.
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/PKG-INFO +30 -4
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/README.md +28 -2
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/pyproject.toml +2 -2
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/__init__.py +1 -1
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/cli.py +113 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/db/schema.sql +15 -0
- devtime_ei-0.2.0/src/devtime/intelligence/verification.py +464 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/mcp/transport.py +45 -1
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/scanner/extractors/nextjs.py +14 -2
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime_ei.egg-info/PKG-INFO +30 -4
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime_ei.egg-info/SOURCES.txt +1 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/LICENSE +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/setup.cfg +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/ai/__init__.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/ai/local.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/ai/prompts.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/ai/providers.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/assets/devtimeignore.starter +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/config.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/db/__init__.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/db/connection.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/db/migrations.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/db/repository.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/demo.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/fixtures/__init__.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/fixtures/assertions.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/fixtures/loader.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/fixtures/runner.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/intelligence/__init__.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/intelligence/claims.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/intelligence/concepts.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/intelligence/context_pack.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/intelligence/evidence.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/intelligence/lineage.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/intelligence/risk.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/intelligence/scoring.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/mcp/__init__.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/mcp/schemas.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/mcp/server.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/mcp/tools.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/output/__init__.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/output/json_export.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/output/markdown.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/output/terminal.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/paths.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/privacy.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/.devtimeignore +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/README.md +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/docs/decisions/0001-use-jwt.md +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/package.json +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/src/admin/permissions.ts +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/src/auth/login.ts +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/src/auth/middleware.ts +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/src/auth/tokens.ts +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/src/billing/stripe-webhook.ts +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/src/billing/subscription-service.ts +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/src/export/export-csv.ts +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/src/jobs/email-worker.ts +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/src/jobs/queues.ts +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/tests/auth-login.test.ts +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/tests/stripe-signature.test.ts +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/scanner/__init__.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/scanner/extractors/__init__.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/scanner/extractors/base.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/scanner/extractors/config_files.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/scanner/extractors/docs.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/scanner/extractors/python.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/scanner/extractors/tests.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/scanner/extractors/typescript.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/scanner/file_walker.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/scanner/ignore.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/scanner/language.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/scanner/signals.py +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime_ei.egg-info/dependency_links.txt +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime_ei.egg-info/entry_points.txt +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime_ei.egg-info/requires.txt +0 -0
- {devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime_ei.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devtime-ei
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Local-first Engineering Intelligence for software repositories
|
|
5
5
|
Author-email: Aviad Shakargi <aviad94@gmail.com>
|
|
6
6
|
Maintainer-email: Aviad Shakargi <aviad94@gmail.com>
|
|
@@ -8,7 +8,7 @@ License: Apache-2.0
|
|
|
8
8
|
Project-URL: Homepage, https://github.com/Shakargy/devtime
|
|
9
9
|
Project-URL: Repository, https://github.com/Shakargy/devtime
|
|
10
10
|
Project-URL: Issues, https://github.com/Shakargy/devtime/issues
|
|
11
|
-
Project-URL: Release Notes, https://github.com/Shakargy/devtime/releases/tag/v0.
|
|
11
|
+
Project-URL: Release Notes, https://github.com/Shakargy/devtime/releases/tag/v0.2.0
|
|
12
12
|
Project-URL: Demo, https://youtu.be/1Hiu3Y9J_SI
|
|
13
13
|
Keywords: devtools,cli,static-analysis,repository-analysis,engineering-intelligence,local-first
|
|
14
14
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -204,6 +204,7 @@ Anything outside these six is out of scope for V0. See [LIMITATIONS.md](LIMITATI
|
|
|
204
204
|
| `dtc context <concept>` | Create a governed Context Pack for agents or humans. |
|
|
205
205
|
| `dtc risk --diff` | Review a git diff for risky changes using local evidence (advisory). |
|
|
206
206
|
| `dtc decision add` | Add a local decision record that can reduce uncertainty. |
|
|
207
|
+
| `dtc verify [claim]` | Verify a repository claim against evidence: status, contradictions, freshness (experimental). |
|
|
207
208
|
|
|
208
209
|
(Also available: `dtc evidence`, `dtc debt`, `dtc status`, `dtc doctor --privacy`,
|
|
209
210
|
`dtc export`, `dtc reset`, `dtc mcp start`.)
|
|
@@ -246,9 +247,10 @@ Or in any MCP client that reads `.mcp.json`:
|
|
|
246
247
|
}
|
|
247
248
|
```
|
|
248
249
|
|
|
249
|
-
The agent gets
|
|
250
|
+
The agent gets four read-only tools: `list_concepts`, `explain_concept`,
|
|
250
251
|
`get_context_pack` (governed context with do-not-change-without-review paths, tests
|
|
251
|
-
to run, and agent guidance)
|
|
252
|
+
to run, and agent guidance), and `verify_claim` (claim status, contradictions, and
|
|
253
|
+
missing evidence, computed fresh and never persisted). Local stdio only - no network listener, no write tools,
|
|
252
254
|
no source code returned, only evidence file paths.
|
|
253
255
|
|
|
254
256
|
DevTime is listed in the official MCP Registry as `io.github.Shakargy/devtime`.
|
|
@@ -294,6 +296,30 @@ python -m venv .venv
|
|
|
294
296
|
pip install -e ".[dev]"
|
|
295
297
|
```
|
|
296
298
|
|
|
299
|
+
## Verify claims (experimental)
|
|
300
|
+
|
|
301
|
+
DevTime is growing into a verification layer: ask whether a statement about the
|
|
302
|
+
repository is actually supported.
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
dtc verify billing-webhook-signature
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
Billing Webhook Signature Verification
|
|
310
|
+
Claim: Incoming billing webhooks verify the payment provider's signature.
|
|
311
|
+
Status: CONTRADICTED
|
|
312
|
+
|
|
313
|
+
Contradictions:
|
|
314
|
+
- The billing webhook endpoint cannot verify signatures because it is a disabled stub.
|
|
315
|
+
claimed: apps/web/pages/api/stripe/webhook.ts is named and routed as a billing webhook endpoint.
|
|
316
|
+
observed: The handler's only behavior is a 404/501 response.
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Statuses are SUPPORTED, WEAK, CONTRADICTED, or UNKNOWN; contradictions always
|
|
320
|
+
show both sides; changed evidence marks a claim STALE. One built-in claim ships
|
|
321
|
+
in v0.2. See **[VERIFICATION.md](VERIFICATION.md)**.
|
|
322
|
+
|
|
297
323
|
## Example output
|
|
298
324
|
|
|
299
325
|
```
|
|
@@ -165,6 +165,7 @@ Anything outside these six is out of scope for V0. See [LIMITATIONS.md](LIMITATI
|
|
|
165
165
|
| `dtc context <concept>` | Create a governed Context Pack for agents or humans. |
|
|
166
166
|
| `dtc risk --diff` | Review a git diff for risky changes using local evidence (advisory). |
|
|
167
167
|
| `dtc decision add` | Add a local decision record that can reduce uncertainty. |
|
|
168
|
+
| `dtc verify [claim]` | Verify a repository claim against evidence: status, contradictions, freshness (experimental). |
|
|
168
169
|
|
|
169
170
|
(Also available: `dtc evidence`, `dtc debt`, `dtc status`, `dtc doctor --privacy`,
|
|
170
171
|
`dtc export`, `dtc reset`, `dtc mcp start`.)
|
|
@@ -207,9 +208,10 @@ Or in any MCP client that reads `.mcp.json`:
|
|
|
207
208
|
}
|
|
208
209
|
```
|
|
209
210
|
|
|
210
|
-
The agent gets
|
|
211
|
+
The agent gets four read-only tools: `list_concepts`, `explain_concept`,
|
|
211
212
|
`get_context_pack` (governed context with do-not-change-without-review paths, tests
|
|
212
|
-
to run, and agent guidance)
|
|
213
|
+
to run, and agent guidance), and `verify_claim` (claim status, contradictions, and
|
|
214
|
+
missing evidence, computed fresh and never persisted). Local stdio only - no network listener, no write tools,
|
|
213
215
|
no source code returned, only evidence file paths.
|
|
214
216
|
|
|
215
217
|
DevTime is listed in the official MCP Registry as `io.github.Shakargy/devtime`.
|
|
@@ -255,6 +257,30 @@ python -m venv .venv
|
|
|
255
257
|
pip install -e ".[dev]"
|
|
256
258
|
```
|
|
257
259
|
|
|
260
|
+
## Verify claims (experimental)
|
|
261
|
+
|
|
262
|
+
DevTime is growing into a verification layer: ask whether a statement about the
|
|
263
|
+
repository is actually supported.
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
dtc verify billing-webhook-signature
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
Billing Webhook Signature Verification
|
|
271
|
+
Claim: Incoming billing webhooks verify the payment provider's signature.
|
|
272
|
+
Status: CONTRADICTED
|
|
273
|
+
|
|
274
|
+
Contradictions:
|
|
275
|
+
- The billing webhook endpoint cannot verify signatures because it is a disabled stub.
|
|
276
|
+
claimed: apps/web/pages/api/stripe/webhook.ts is named and routed as a billing webhook endpoint.
|
|
277
|
+
observed: The handler's only behavior is a 404/501 response.
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Statuses are SUPPORTED, WEAK, CONTRADICTED, or UNKNOWN; contradictions always
|
|
281
|
+
show both sides; changed evidence marks a claim STALE. One built-in claim ships
|
|
282
|
+
in v0.2. See **[VERIFICATION.md](VERIFICATION.md)**.
|
|
283
|
+
|
|
258
284
|
## Example output
|
|
259
285
|
|
|
260
286
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "devtime-ei"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Local-first Engineering Intelligence for software repositories"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -50,7 +50,7 @@ dev = [
|
|
|
50
50
|
Homepage = "https://github.com/Shakargy/devtime"
|
|
51
51
|
Repository = "https://github.com/Shakargy/devtime"
|
|
52
52
|
Issues = "https://github.com/Shakargy/devtime/issues"
|
|
53
|
-
"Release Notes" = "https://github.com/Shakargy/devtime/releases/tag/v0.
|
|
53
|
+
"Release Notes" = "https://github.com/Shakargy/devtime/releases/tag/v0.2.0"
|
|
54
54
|
Demo = "https://youtu.be/1Hiu3Y9J_SI"
|
|
55
55
|
|
|
56
56
|
[project.scripts]
|
|
@@ -78,6 +78,119 @@ def demo_init(
|
|
|
78
78
|
console.print(' dtc explain "Billing Webhooks"')
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
@app.command()
|
|
82
|
+
def verify(
|
|
83
|
+
claim: str = typer.Argument(None, help="Claim id to verify. Omit to verify all built-in claims."),
|
|
84
|
+
list_claims: bool = typer.Option(False, "--list", help="List built-in claims with last status and freshness."),
|
|
85
|
+
as_json: bool = typer.Option(False, "--json", help="Stable machine-readable output."),
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Verify a repository claim against scanned evidence (v0.2 experimental)."""
|
|
88
|
+
import json as _json
|
|
89
|
+
|
|
90
|
+
from devtime.db import connection
|
|
91
|
+
from devtime.intelligence import verification as ver
|
|
92
|
+
|
|
93
|
+
if not paths.is_initialized():
|
|
94
|
+
console.print("[red]Not initialized.[/red] Run dtc init first.")
|
|
95
|
+
raise typer.Exit(code=2)
|
|
96
|
+
|
|
97
|
+
conn = connection.connect()
|
|
98
|
+
try:
|
|
99
|
+
if list_claims:
|
|
100
|
+
rows = []
|
|
101
|
+
for slug, definition in ver.BUILTIN_CLAIMS.items():
|
|
102
|
+
latest = ver.load_latest_verification(conn, slug)
|
|
103
|
+
freshness, changed = ver.freshness_for(conn, slug)
|
|
104
|
+
rows.append(
|
|
105
|
+
{
|
|
106
|
+
"claim_id": slug,
|
|
107
|
+
"name": definition.name,
|
|
108
|
+
"statement": definition.statement,
|
|
109
|
+
"last_status": latest[0]["status"] if latest else None,
|
|
110
|
+
"freshness": freshness,
|
|
111
|
+
"changed_evidence": changed,
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
if as_json:
|
|
115
|
+
console.print_json(_json.dumps({"schema_version": "1", "claims": rows}))
|
|
116
|
+
else:
|
|
117
|
+
console.print("[bold]Built-in claims[/bold]\n")
|
|
118
|
+
for r in rows:
|
|
119
|
+
status_txt = r["last_status"] or "never verified"
|
|
120
|
+
console.print(f" {r['claim_id']}")
|
|
121
|
+
console.print(f" {r['statement']}")
|
|
122
|
+
console.print(f" last status: {status_txt} freshness: {r['freshness']}")
|
|
123
|
+
for p in r["changed_evidence"]:
|
|
124
|
+
console.print(f" changed since verification: {p}", markup=False)
|
|
125
|
+
console.print("")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
slugs = [claim] if claim else list(ver.BUILTIN_CLAIMS.keys())
|
|
129
|
+
results = []
|
|
130
|
+
for slug in slugs:
|
|
131
|
+
try:
|
|
132
|
+
result = ver.verify_claim(conn, slug)
|
|
133
|
+
except KeyError:
|
|
134
|
+
console.print(f"[red]Unknown claim:[/red] {slug}")
|
|
135
|
+
console.print("Run [bold]dtc verify --list[/bold] to see built-in claims.")
|
|
136
|
+
raise typer.Exit(code=1)
|
|
137
|
+
ver.save_verification(conn, result)
|
|
138
|
+
results.append(result)
|
|
139
|
+
|
|
140
|
+
if as_json:
|
|
141
|
+
console.print_json(
|
|
142
|
+
_json.dumps(
|
|
143
|
+
{
|
|
144
|
+
"schema_version": "1",
|
|
145
|
+
"command": "verify",
|
|
146
|
+
"results": [r.to_dict() for r in results],
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
for result in results:
|
|
153
|
+
_print_verification(result)
|
|
154
|
+
finally:
|
|
155
|
+
conn.close()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _print_verification(result) -> None:
|
|
159
|
+
color = {
|
|
160
|
+
"SUPPORTED": "green",
|
|
161
|
+
"WEAK": "yellow",
|
|
162
|
+
"CONTRADICTED": "red",
|
|
163
|
+
"UNKNOWN": "cyan",
|
|
164
|
+
}.get(result.status, "white")
|
|
165
|
+
console.print(f"[bold]{result.claim_name}[/bold]")
|
|
166
|
+
console.print(f"Claim: {result.statement}")
|
|
167
|
+
console.print(f"Status: [{color}]{result.status}[/{color}]")
|
|
168
|
+
console.print("")
|
|
169
|
+
console.print("Why:")
|
|
170
|
+
for line in result.why:
|
|
171
|
+
console.print(f" - {line}", markup=False)
|
|
172
|
+
if result.supporting:
|
|
173
|
+
console.print("\nSupporting evidence:")
|
|
174
|
+
for e in result.supporting[:6]:
|
|
175
|
+
loc = f":{e.start_line}" if e.start_line else ""
|
|
176
|
+
console.print(f" - {e.path}{loc} [{e.strength}]", markup=False)
|
|
177
|
+
console.print(f" {e.observation}", markup=False)
|
|
178
|
+
if result.contradictions:
|
|
179
|
+
console.print("\n[red]Contradictions:[/red]")
|
|
180
|
+
for c in result.contradictions:
|
|
181
|
+
console.print(f" - {c.summary}", markup=False)
|
|
182
|
+
console.print(f" claimed: {c.claimed_side}", markup=False)
|
|
183
|
+
console.print(f" observed: {c.observed_side}", markup=False)
|
|
184
|
+
if result.missing:
|
|
185
|
+
console.print("\nMissing evidence:")
|
|
186
|
+
for m in result.missing:
|
|
187
|
+
console.print(f" - {m}", markup=False)
|
|
188
|
+
console.print("\nLimitations:")
|
|
189
|
+
for lim in result.limitations:
|
|
190
|
+
console.print(f" - {lim}", markup=False)
|
|
191
|
+
console.print("")
|
|
192
|
+
|
|
193
|
+
|
|
81
194
|
@app.command()
|
|
82
195
|
def status() -> None:
|
|
83
196
|
"""Show local storage, AI, cloud, telemetry, MCP, and scan status."""
|
|
@@ -135,6 +135,21 @@ CREATE TABLE IF NOT EXISTS risk_findings (
|
|
|
135
135
|
created_at TEXT NOT NULL
|
|
136
136
|
);
|
|
137
137
|
|
|
138
|
+
-- v0.2.0: immutable claim verification results (the verification slice).
|
|
139
|
+
-- Databases created before v0.2.0 get this table lazily via
|
|
140
|
+
-- verification.ensure_verifications_table (idempotent, no migration needed).
|
|
141
|
+
CREATE TABLE IF NOT EXISTS verifications (
|
|
142
|
+
id TEXT PRIMARY KEY,
|
|
143
|
+
repository_id TEXT NOT NULL,
|
|
144
|
+
claim_slug TEXT NOT NULL,
|
|
145
|
+
status TEXT NOT NULL,
|
|
146
|
+
scan_id TEXT,
|
|
147
|
+
result_json TEXT NOT NULL,
|
|
148
|
+
evidence_fingerprints_json TEXT NOT NULL DEFAULT '[]',
|
|
149
|
+
engine_version TEXT NOT NULL,
|
|
150
|
+
created_at TEXT NOT NULL
|
|
151
|
+
);
|
|
152
|
+
|
|
138
153
|
CREATE TABLE IF NOT EXISTS context_packs (
|
|
139
154
|
id TEXT PRIMARY KEY,
|
|
140
155
|
concept_id TEXT NOT NULL,
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"""Claim verification engine (v0.2.0 - the verification slice).
|
|
2
|
+
|
|
3
|
+
DevTime's evolution target: the verification layer for repository understanding.
|
|
4
|
+
A claim is a statement about the repository; verification evaluates it against
|
|
5
|
+
persisted scan evidence and answers with a status, both-sided contradictions,
|
|
6
|
+
missing evidence, coverage limitations, and freshness - never with confidence
|
|
7
|
+
the evidence cannot back.
|
|
8
|
+
|
|
9
|
+
V0.2 scope, deliberately narrow:
|
|
10
|
+
- Built-in claims only (no user-defined claim files yet).
|
|
11
|
+
- One claim domain: billing webhook signature verification.
|
|
12
|
+
- Four statuses: SUPPORTED, WEAK, CONTRADICTED, UNKNOWN.
|
|
13
|
+
- Freshness from file fingerprints: FRESH, STALE, NEEDS_VERIFICATION.
|
|
14
|
+
- Deterministic and rule-driven. No AI, no network, no code execution.
|
|
15
|
+
|
|
16
|
+
Statuses (documented meaning, per repository evidence policy - not formal proof):
|
|
17
|
+
SUPPORTED required behavior evidence exists in the current scan.
|
|
18
|
+
WEAK the claim's surface exists, but the proving evidence is missing.
|
|
19
|
+
CONTRADICTED credible evidence conflicts with the claim; both sides are shown.
|
|
20
|
+
UNKNOWN the repository shows no relevant surface, or coverage cannot
|
|
21
|
+
responsibly decide.
|
|
22
|
+
|
|
23
|
+
Freshness is separate from truth:
|
|
24
|
+
FRESH supporting evidence files are unchanged since verification.
|
|
25
|
+
STALE at least one evidence file changed or disappeared.
|
|
26
|
+
NEEDS_VERIFICATION the claim has never been verified in this repository.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import sqlite3
|
|
33
|
+
import uuid
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
|
|
37
|
+
from devtime import __version__
|
|
38
|
+
|
|
39
|
+
# Statuses
|
|
40
|
+
SUPPORTED = "SUPPORTED"
|
|
41
|
+
WEAK = "WEAK"
|
|
42
|
+
CONTRADICTED = "CONTRADICTED"
|
|
43
|
+
UNKNOWN = "UNKNOWN"
|
|
44
|
+
|
|
45
|
+
# Freshness
|
|
46
|
+
FRESH = "FRESH"
|
|
47
|
+
STALE = "STALE"
|
|
48
|
+
NEEDS_VERIFICATION = "NEEDS_VERIFICATION"
|
|
49
|
+
|
|
50
|
+
# Billing vocabulary (kept aligned with concepts.py; duplicated deliberately so
|
|
51
|
+
# the verification engine has no import-time coupling to concept detection).
|
|
52
|
+
_PROVIDER_TOKENS = (
|
|
53
|
+
"stripe", "paypal", "braintree", "chargebee", "lemonsqueezy", "paddle", "razorpay",
|
|
54
|
+
)
|
|
55
|
+
_PAYMENT_TOKENS = _PROVIDER_TOKENS + (
|
|
56
|
+
"billing", "invoice", "payment", "payments", "checkout", "charge",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class EvidenceRef:
|
|
62
|
+
"""One piece of evidence with provenance."""
|
|
63
|
+
|
|
64
|
+
path: str
|
|
65
|
+
observation: str
|
|
66
|
+
kind: str
|
|
67
|
+
strength: str # strong | moderate | weak
|
|
68
|
+
start_line: int | None = None
|
|
69
|
+
end_line: int | None = None
|
|
70
|
+
sha256: str | None = None
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> dict:
|
|
73
|
+
d = {
|
|
74
|
+
"path": self.path,
|
|
75
|
+
"observation": self.observation,
|
|
76
|
+
"kind": self.kind,
|
|
77
|
+
"strength": self.strength,
|
|
78
|
+
}
|
|
79
|
+
if self.start_line is not None:
|
|
80
|
+
d["start_line"] = self.start_line
|
|
81
|
+
if self.end_line is not None:
|
|
82
|
+
d["end_line"] = self.end_line
|
|
83
|
+
return d
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class Contradiction:
|
|
88
|
+
"""A conflict, always with both sides."""
|
|
89
|
+
|
|
90
|
+
summary: str
|
|
91
|
+
claimed_side: str
|
|
92
|
+
observed_side: str
|
|
93
|
+
evidence: list[EvidenceRef] = field(default_factory=list)
|
|
94
|
+
|
|
95
|
+
def to_dict(self) -> dict:
|
|
96
|
+
return {
|
|
97
|
+
"summary": self.summary,
|
|
98
|
+
"claimed_side": self.claimed_side,
|
|
99
|
+
"observed_side": self.observed_side,
|
|
100
|
+
"evidence": [e.to_dict() for e in self.evidence],
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class VerificationResult:
|
|
106
|
+
claim_slug: str
|
|
107
|
+
claim_name: str
|
|
108
|
+
statement: str
|
|
109
|
+
status: str
|
|
110
|
+
why: list[str]
|
|
111
|
+
supporting: list[EvidenceRef]
|
|
112
|
+
contradictions: list[Contradiction]
|
|
113
|
+
missing: list[str]
|
|
114
|
+
limitations: list[str]
|
|
115
|
+
scan_id: str | None
|
|
116
|
+
verified_at: str
|
|
117
|
+
engine_version: str
|
|
118
|
+
|
|
119
|
+
def to_dict(self) -> dict:
|
|
120
|
+
return {
|
|
121
|
+
"schema_version": "1",
|
|
122
|
+
"claim_id": self.claim_slug,
|
|
123
|
+
"claim_name": self.claim_name,
|
|
124
|
+
"statement": self.statement,
|
|
125
|
+
"status": self.status,
|
|
126
|
+
"why": self.why,
|
|
127
|
+
"supporting_evidence": [e.to_dict() for e in self.supporting],
|
|
128
|
+
"contradictions": [c.to_dict() for c in self.contradictions],
|
|
129
|
+
"missing_evidence": self.missing,
|
|
130
|
+
"limitations": self.limitations,
|
|
131
|
+
"scan_id": self.scan_id,
|
|
132
|
+
"verified_at": self.verified_at,
|
|
133
|
+
"engine_version": self.engine_version,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True)
|
|
138
|
+
class ClaimDefinition:
|
|
139
|
+
slug: str
|
|
140
|
+
name: str
|
|
141
|
+
statement: str
|
|
142
|
+
category: str
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
BUILTIN_CLAIMS: dict[str, ClaimDefinition] = {
|
|
146
|
+
"billing-webhook-signature": ClaimDefinition(
|
|
147
|
+
slug="billing-webhook-signature",
|
|
148
|
+
name="Billing Webhook Signature Verification",
|
|
149
|
+
statement="Incoming billing webhooks verify the payment provider's signature.",
|
|
150
|
+
category="billing",
|
|
151
|
+
),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_LIMITATIONS = [
|
|
155
|
+
"Heuristic scanner: evidence comes from static patterns, not execution.",
|
|
156
|
+
"Signature verification is recognized for known provider patterns "
|
|
157
|
+
"(e.g. Stripe constructEvent); custom schemes may not be detected.",
|
|
158
|
+
"Coverage follows scanner language support; see LIMITATIONS.md.",
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _now() -> str:
|
|
163
|
+
return datetime.now(timezone.utc).isoformat()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# --------------------------------------------------------------------------- #
|
|
167
|
+
# Signal loading (persisted scan evidence)
|
|
168
|
+
# --------------------------------------------------------------------------- #
|
|
169
|
+
|
|
170
|
+
def _latest_scan_id(conn: sqlite3.Connection) -> str | None:
|
|
171
|
+
row = conn.execute(
|
|
172
|
+
"SELECT id FROM scans WHERE status = 'completed' "
|
|
173
|
+
"ORDER BY started_at DESC LIMIT 1"
|
|
174
|
+
).fetchone()
|
|
175
|
+
return row["id"] if row else None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _load_signals(conn: sqlite3.Connection, scan_id: str) -> list[sqlite3.Row]:
|
|
179
|
+
return conn.execute(
|
|
180
|
+
"SELECT s.kind, s.name, s.value, s.start_line, s.end_line, s.confidence, "
|
|
181
|
+
" s.metadata_json, f.path, f.sha256 "
|
|
182
|
+
"FROM signals s JOIN files f ON f.id = s.file_id "
|
|
183
|
+
"WHERE s.scan_id = ?",
|
|
184
|
+
(scan_id,),
|
|
185
|
+
).fetchall()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _hay(row: sqlite3.Row) -> str:
|
|
189
|
+
return " ".join(
|
|
190
|
+
str(x).lower()
|
|
191
|
+
for x in (row["name"], row["value"], row["path"], row["metadata_json"])
|
|
192
|
+
if x is not None
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _is_billingish(hay: str) -> bool:
|
|
197
|
+
return any(t in hay for t in _PAYMENT_TOKENS)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# --------------------------------------------------------------------------- #
|
|
201
|
+
# The engine
|
|
202
|
+
# --------------------------------------------------------------------------- #
|
|
203
|
+
|
|
204
|
+
def verify_claim(conn: sqlite3.Connection, slug: str) -> VerificationResult:
|
|
205
|
+
"""Verify one built-in claim against the latest completed scan."""
|
|
206
|
+
definition = BUILTIN_CLAIMS.get(slug)
|
|
207
|
+
if definition is None:
|
|
208
|
+
raise KeyError(slug)
|
|
209
|
+
|
|
210
|
+
scan_id = _latest_scan_id(conn)
|
|
211
|
+
if scan_id is None:
|
|
212
|
+
return VerificationResult(
|
|
213
|
+
claim_slug=definition.slug,
|
|
214
|
+
claim_name=definition.name,
|
|
215
|
+
statement=definition.statement,
|
|
216
|
+
status=UNKNOWN,
|
|
217
|
+
why=["No completed scan exists. Run dtc scan first."],
|
|
218
|
+
supporting=[],
|
|
219
|
+
contradictions=[],
|
|
220
|
+
missing=["A completed repository scan."],
|
|
221
|
+
limitations=_LIMITATIONS,
|
|
222
|
+
scan_id=None,
|
|
223
|
+
verified_at=_now(),
|
|
224
|
+
engine_version=__version__,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
rows = _load_signals(conn, scan_id)
|
|
228
|
+
return _verify_billing_webhook_signature(definition, rows, scan_id)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _verify_billing_webhook_signature(
|
|
232
|
+
definition: ClaimDefinition, rows: list[sqlite3.Row], scan_id: str
|
|
233
|
+
) -> VerificationResult:
|
|
234
|
+
verifications: list[EvidenceRef] = []
|
|
235
|
+
webhook_routes: list[EvidenceRef] = []
|
|
236
|
+
stub_webhooks: list[EvidenceRef] = []
|
|
237
|
+
signature_tests: list[EvidenceRef] = []
|
|
238
|
+
provider_deps: list[EvidenceRef] = []
|
|
239
|
+
|
|
240
|
+
for row in rows:
|
|
241
|
+
hay = _hay(row)
|
|
242
|
+
kind = row["kind"]
|
|
243
|
+
|
|
244
|
+
if kind == "webhook_signature_verification":
|
|
245
|
+
verifications.append(
|
|
246
|
+
_ref(row, "Verifies the provider's webhook signature.", "strong")
|
|
247
|
+
)
|
|
248
|
+
elif kind == "route" and "webhook" in hay and _is_billingish(hay):
|
|
249
|
+
webhook_routes.append(
|
|
250
|
+
_ref(row, "Billing webhook route is handled here.", "moderate")
|
|
251
|
+
)
|
|
252
|
+
elif kind == "disabled_endpoint" and "webhook" in hay and _is_billingish(hay):
|
|
253
|
+
stub_webhooks.append(
|
|
254
|
+
_ref(
|
|
255
|
+
row,
|
|
256
|
+
"Webhook endpoint is a disabled stub: its only behavior is a "
|
|
257
|
+
"404/501 response.",
|
|
258
|
+
"strong",
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
elif kind == "test" and "signature" in hay and _is_billingish(hay):
|
|
262
|
+
signature_tests.append(
|
|
263
|
+
_ref(row, "Test exercises webhook signature behavior.", "moderate")
|
|
264
|
+
)
|
|
265
|
+
elif kind == "dependency" and any(p in hay for p in _PROVIDER_TOKENS):
|
|
266
|
+
provider_deps.append(
|
|
267
|
+
_ref(row, "Payment provider dependency is declared.", "weak")
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
why: list[str] = []
|
|
271
|
+
missing: list[str] = []
|
|
272
|
+
contradictions: list[Contradiction] = []
|
|
273
|
+
supporting: list[EvidenceRef] = []
|
|
274
|
+
|
|
275
|
+
if stub_webhooks and not verifications:
|
|
276
|
+
for stub in stub_webhooks:
|
|
277
|
+
contradictions.append(
|
|
278
|
+
Contradiction(
|
|
279
|
+
summary="The billing webhook endpoint cannot verify signatures "
|
|
280
|
+
"because it is a disabled stub.",
|
|
281
|
+
claimed_side=f"{stub.path} is named and routed as a billing "
|
|
282
|
+
"webhook endpoint, which implies webhook handling.",
|
|
283
|
+
observed_side="The handler's only behavior is a 404/501 "
|
|
284
|
+
"response; no webhook processing or signature verification "
|
|
285
|
+
"path exists in this repository snapshot.",
|
|
286
|
+
evidence=[stub],
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
why.append(
|
|
290
|
+
"A billing webhook endpoint exists in name, but it is a disabled stub."
|
|
291
|
+
)
|
|
292
|
+
why.append("No signature verification evidence was found anywhere in the scan.")
|
|
293
|
+
status = CONTRADICTED
|
|
294
|
+
supporting = signature_tests # tests may still exist for other editions
|
|
295
|
+
missing.append("An active webhook handler that verifies provider signatures.")
|
|
296
|
+
elif verifications:
|
|
297
|
+
status = SUPPORTED
|
|
298
|
+
supporting = verifications + webhook_routes + signature_tests
|
|
299
|
+
why.append("Signature verification behavior evidence was found.")
|
|
300
|
+
if webhook_routes:
|
|
301
|
+
why.append("Billing webhook route handling was found.")
|
|
302
|
+
if signature_tests:
|
|
303
|
+
why.append("A test exercises signature behavior.")
|
|
304
|
+
else:
|
|
305
|
+
missing.append("A test that exercises webhook signature verification.")
|
|
306
|
+
# A stub existing next to real verification is worth surfacing, not failing.
|
|
307
|
+
for stub in stub_webhooks:
|
|
308
|
+
contradictions.append(
|
|
309
|
+
Contradiction(
|
|
310
|
+
summary="One webhook endpoint is a disabled stub while "
|
|
311
|
+
"verification exists elsewhere.",
|
|
312
|
+
claimed_side=f"{stub.path} is routed as a billing webhook.",
|
|
313
|
+
observed_side="Its only behavior is a 404/501 response.",
|
|
314
|
+
evidence=[stub],
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
elif webhook_routes or provider_deps:
|
|
318
|
+
status = WEAK
|
|
319
|
+
supporting = webhook_routes + provider_deps + signature_tests
|
|
320
|
+
why.append(
|
|
321
|
+
"Billing webhook surface exists (routes or provider dependencies), "
|
|
322
|
+
"but no signature verification evidence was found."
|
|
323
|
+
)
|
|
324
|
+
missing.append("Signature verification behavior (e.g. constructEvent).")
|
|
325
|
+
if not signature_tests:
|
|
326
|
+
missing.append("A test that exercises webhook signature verification.")
|
|
327
|
+
else:
|
|
328
|
+
status = UNKNOWN
|
|
329
|
+
why.append(
|
|
330
|
+
"No billing webhook surface was found in the scanned files. "
|
|
331
|
+
"The claim does not apply, or the surface is outside scanner coverage."
|
|
332
|
+
)
|
|
333
|
+
missing.append("Any billing webhook route, handler, or provider dependency.")
|
|
334
|
+
|
|
335
|
+
return VerificationResult(
|
|
336
|
+
claim_slug=definition.slug,
|
|
337
|
+
claim_name=definition.name,
|
|
338
|
+
statement=definition.statement,
|
|
339
|
+
status=status,
|
|
340
|
+
why=why,
|
|
341
|
+
supporting=supporting,
|
|
342
|
+
contradictions=contradictions,
|
|
343
|
+
missing=missing,
|
|
344
|
+
limitations=_LIMITATIONS,
|
|
345
|
+
scan_id=scan_id,
|
|
346
|
+
verified_at=_now(),
|
|
347
|
+
engine_version=__version__,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _ref(row: sqlite3.Row, observation: str, strength: str) -> EvidenceRef:
|
|
352
|
+
return EvidenceRef(
|
|
353
|
+
path=row["path"],
|
|
354
|
+
observation=observation,
|
|
355
|
+
kind=row["kind"],
|
|
356
|
+
strength=strength,
|
|
357
|
+
start_line=row["start_line"],
|
|
358
|
+
end_line=row["end_line"],
|
|
359
|
+
sha256=row["sha256"],
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# --------------------------------------------------------------------------- #
|
|
364
|
+
# Persistence and freshness
|
|
365
|
+
# --------------------------------------------------------------------------- #
|
|
366
|
+
|
|
367
|
+
_VERIFICATIONS_TABLE = """
|
|
368
|
+
CREATE TABLE IF NOT EXISTS verifications (
|
|
369
|
+
id TEXT PRIMARY KEY,
|
|
370
|
+
repository_id TEXT NOT NULL,
|
|
371
|
+
claim_slug TEXT NOT NULL,
|
|
372
|
+
status TEXT NOT NULL,
|
|
373
|
+
scan_id TEXT,
|
|
374
|
+
result_json TEXT NOT NULL,
|
|
375
|
+
evidence_fingerprints_json TEXT NOT NULL DEFAULT '[]',
|
|
376
|
+
engine_version TEXT NOT NULL,
|
|
377
|
+
created_at TEXT NOT NULL
|
|
378
|
+
);
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def ensure_verifications_table(conn: sqlite3.Connection) -> None:
|
|
383
|
+
"""Idempotent: safe for databases initialized before v0.2.0."""
|
|
384
|
+
conn.execute(_VERIFICATIONS_TABLE)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def save_verification(conn: sqlite3.Connection, result: VerificationResult) -> str:
|
|
388
|
+
"""Store an immutable verification result with evidence fingerprints."""
|
|
389
|
+
ensure_verifications_table(conn)
|
|
390
|
+
repo = conn.execute("SELECT id FROM repositories LIMIT 1").fetchone()
|
|
391
|
+
repo_id = repo["id"] if repo else "unknown"
|
|
392
|
+
fingerprints = [
|
|
393
|
+
{"path": e.path, "sha256": e.sha256}
|
|
394
|
+
for e in result.supporting
|
|
395
|
+
if e.sha256
|
|
396
|
+
]
|
|
397
|
+
# Contradiction evidence participates in freshness too: if the stub changes,
|
|
398
|
+
# the contradiction must be re-checked.
|
|
399
|
+
for c in result.contradictions:
|
|
400
|
+
fingerprints += [
|
|
401
|
+
{"path": e.path, "sha256": e.sha256} for e in c.evidence if e.sha256
|
|
402
|
+
]
|
|
403
|
+
vid = f"ver-{uuid.uuid4().hex[:10]}"
|
|
404
|
+
conn.execute(
|
|
405
|
+
"INSERT INTO verifications"
|
|
406
|
+
"(id, repository_id, claim_slug, status, scan_id, result_json, "
|
|
407
|
+
" evidence_fingerprints_json, engine_version, created_at) "
|
|
408
|
+
"VALUES (?,?,?,?,?,?,?,?,?)",
|
|
409
|
+
(
|
|
410
|
+
vid,
|
|
411
|
+
repo_id,
|
|
412
|
+
result.claim_slug,
|
|
413
|
+
result.status,
|
|
414
|
+
result.scan_id,
|
|
415
|
+
json.dumps(result.to_dict()),
|
|
416
|
+
json.dumps(fingerprints),
|
|
417
|
+
result.engine_version,
|
|
418
|
+
result.verified_at,
|
|
419
|
+
),
|
|
420
|
+
)
|
|
421
|
+
conn.commit()
|
|
422
|
+
return vid
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def load_latest_verification(
|
|
426
|
+
conn: sqlite3.Connection, slug: str
|
|
427
|
+
) -> tuple[dict, list[dict], str] | None:
|
|
428
|
+
"""Return (result_dict, fingerprints, created_at) for the newest verification."""
|
|
429
|
+
ensure_verifications_table(conn)
|
|
430
|
+
row = conn.execute(
|
|
431
|
+
"SELECT result_json, evidence_fingerprints_json, created_at "
|
|
432
|
+
"FROM verifications WHERE claim_slug = ? ORDER BY created_at DESC LIMIT 1",
|
|
433
|
+
(slug,),
|
|
434
|
+
).fetchone()
|
|
435
|
+
if row is None:
|
|
436
|
+
return None
|
|
437
|
+
return (
|
|
438
|
+
json.loads(row["result_json"]),
|
|
439
|
+
json.loads(row["evidence_fingerprints_json"]),
|
|
440
|
+
row["created_at"],
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def freshness_for(conn: sqlite3.Connection, slug: str) -> tuple[str, list[str]]:
|
|
445
|
+
"""Compare stored evidence fingerprints against current file hashes.
|
|
446
|
+
|
|
447
|
+
Returns (freshness, changed_paths). Never flags unrelated file changes:
|
|
448
|
+
only files that were evidence for this claim participate.
|
|
449
|
+
"""
|
|
450
|
+
latest = load_latest_verification(conn, slug)
|
|
451
|
+
if latest is None:
|
|
452
|
+
return NEEDS_VERIFICATION, []
|
|
453
|
+
_, fingerprints, _ = latest
|
|
454
|
+
changed: list[str] = []
|
|
455
|
+
for fp in fingerprints:
|
|
456
|
+
row = conn.execute(
|
|
457
|
+
"SELECT sha256 FROM files WHERE path = ? ORDER BY last_seen_scan_id DESC LIMIT 1",
|
|
458
|
+
(fp["path"],),
|
|
459
|
+
).fetchone()
|
|
460
|
+
if row is None or row["sha256"] != fp["sha256"]:
|
|
461
|
+
changed.append(fp["path"])
|
|
462
|
+
if changed:
|
|
463
|
+
return STALE, sorted(set(changed))
|
|
464
|
+
return FRESH, []
|
|
@@ -32,7 +32,12 @@ SERVER_INSTRUCTIONS = (
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
# The subset of the planned tool surface that is implemented and exposed.
|
|
35
|
-
IMPLEMENTED_TOOLS = (
|
|
35
|
+
IMPLEMENTED_TOOLS = (
|
|
36
|
+
"list_concepts",
|
|
37
|
+
"explain_concept",
|
|
38
|
+
"get_context_pack",
|
|
39
|
+
"verify_claim",
|
|
40
|
+
)
|
|
36
41
|
|
|
37
42
|
_NOT_INITIALIZED = {
|
|
38
43
|
"error": "not_initialized",
|
|
@@ -91,6 +96,45 @@ def build_server():
|
|
|
91
96
|
return _NOT_INITIALIZED
|
|
92
97
|
return tools.get_context_pack(concept, mode=mode)
|
|
93
98
|
|
|
99
|
+
@server.tool()
|
|
100
|
+
def verify_claim(claim_id: str = "") -> dict:
|
|
101
|
+
"""Verify a repository claim against scanned evidence (read-only compute).
|
|
102
|
+
|
|
103
|
+
Returns status (SUPPORTED / WEAK / CONTRADICTED / UNKNOWN), why,
|
|
104
|
+
supporting evidence with file paths, both-sided contradictions, missing
|
|
105
|
+
evidence, and coverage limitations. Call with no claim_id to list the
|
|
106
|
+
built-in claims. Results are computed fresh and NOT persisted (this
|
|
107
|
+
server stays read-only); use `dtc verify` in a terminal to record one.
|
|
108
|
+
"""
|
|
109
|
+
if not paths.is_initialized():
|
|
110
|
+
return _NOT_INITIALIZED
|
|
111
|
+
from devtime.db import connection
|
|
112
|
+
from devtime.intelligence import verification as ver
|
|
113
|
+
|
|
114
|
+
conn = connection.connect()
|
|
115
|
+
try:
|
|
116
|
+
if not claim_id:
|
|
117
|
+
return {
|
|
118
|
+
"builtin_claims": [
|
|
119
|
+
{
|
|
120
|
+
"claim_id": d.slug,
|
|
121
|
+
"name": d.name,
|
|
122
|
+
"statement": d.statement,
|
|
123
|
+
}
|
|
124
|
+
for d in ver.BUILTIN_CLAIMS.values()
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
try:
|
|
128
|
+
result = ver.verify_claim(conn, claim_id)
|
|
129
|
+
except KeyError:
|
|
130
|
+
return {
|
|
131
|
+
"error": "unknown_claim",
|
|
132
|
+
"hint": "Call verify_claim with no claim_id to list built-in claims.",
|
|
133
|
+
}
|
|
134
|
+
return result.to_dict()
|
|
135
|
+
finally:
|
|
136
|
+
conn.close()
|
|
137
|
+
|
|
94
138
|
return server
|
|
95
139
|
|
|
96
140
|
|
|
@@ -114,9 +114,21 @@ def _extract_pages_api(file: WalkedFile) -> list[Signal]:
|
|
|
114
114
|
text = read_text(file)
|
|
115
115
|
if not _PAGES_HANDLER_RE.search(text):
|
|
116
116
|
return []
|
|
117
|
-
if _is_disabled_stub(text):
|
|
118
|
-
return []
|
|
119
117
|
route_path = derive_pages_route_path(file.rel_path)
|
|
118
|
+
if _is_disabled_stub(text):
|
|
119
|
+
# v0.2.0: a stub is not a route, but it IS a fact worth remembering - the
|
|
120
|
+
# verification engine uses it as contradiction evidence ("the endpoint
|
|
121
|
+
# exists in name; its only behavior is a 404/501"). Concept detection
|
|
122
|
+
# ignores this kind entirely.
|
|
123
|
+
return [
|
|
124
|
+
signal(
|
|
125
|
+
"disabled_endpoint",
|
|
126
|
+
name=f"STUB {route_path}",
|
|
127
|
+
file=file,
|
|
128
|
+
confidence=0.8,
|
|
129
|
+
metadata={"path": route_path, "framework": "nextjs-pages"},
|
|
130
|
+
)
|
|
131
|
+
]
|
|
120
132
|
return [
|
|
121
133
|
signal(
|
|
122
134
|
"route",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devtime-ei
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Local-first Engineering Intelligence for software repositories
|
|
5
5
|
Author-email: Aviad Shakargi <aviad94@gmail.com>
|
|
6
6
|
Maintainer-email: Aviad Shakargi <aviad94@gmail.com>
|
|
@@ -8,7 +8,7 @@ License: Apache-2.0
|
|
|
8
8
|
Project-URL: Homepage, https://github.com/Shakargy/devtime
|
|
9
9
|
Project-URL: Repository, https://github.com/Shakargy/devtime
|
|
10
10
|
Project-URL: Issues, https://github.com/Shakargy/devtime/issues
|
|
11
|
-
Project-URL: Release Notes, https://github.com/Shakargy/devtime/releases/tag/v0.
|
|
11
|
+
Project-URL: Release Notes, https://github.com/Shakargy/devtime/releases/tag/v0.2.0
|
|
12
12
|
Project-URL: Demo, https://youtu.be/1Hiu3Y9J_SI
|
|
13
13
|
Keywords: devtools,cli,static-analysis,repository-analysis,engineering-intelligence,local-first
|
|
14
14
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -204,6 +204,7 @@ Anything outside these six is out of scope for V0. See [LIMITATIONS.md](LIMITATI
|
|
|
204
204
|
| `dtc context <concept>` | Create a governed Context Pack for agents or humans. |
|
|
205
205
|
| `dtc risk --diff` | Review a git diff for risky changes using local evidence (advisory). |
|
|
206
206
|
| `dtc decision add` | Add a local decision record that can reduce uncertainty. |
|
|
207
|
+
| `dtc verify [claim]` | Verify a repository claim against evidence: status, contradictions, freshness (experimental). |
|
|
207
208
|
|
|
208
209
|
(Also available: `dtc evidence`, `dtc debt`, `dtc status`, `dtc doctor --privacy`,
|
|
209
210
|
`dtc export`, `dtc reset`, `dtc mcp start`.)
|
|
@@ -246,9 +247,10 @@ Or in any MCP client that reads `.mcp.json`:
|
|
|
246
247
|
}
|
|
247
248
|
```
|
|
248
249
|
|
|
249
|
-
The agent gets
|
|
250
|
+
The agent gets four read-only tools: `list_concepts`, `explain_concept`,
|
|
250
251
|
`get_context_pack` (governed context with do-not-change-without-review paths, tests
|
|
251
|
-
to run, and agent guidance)
|
|
252
|
+
to run, and agent guidance), and `verify_claim` (claim status, contradictions, and
|
|
253
|
+
missing evidence, computed fresh and never persisted). Local stdio only - no network listener, no write tools,
|
|
252
254
|
no source code returned, only evidence file paths.
|
|
253
255
|
|
|
254
256
|
DevTime is listed in the official MCP Registry as `io.github.Shakargy/devtime`.
|
|
@@ -294,6 +296,30 @@ python -m venv .venv
|
|
|
294
296
|
pip install -e ".[dev]"
|
|
295
297
|
```
|
|
296
298
|
|
|
299
|
+
## Verify claims (experimental)
|
|
300
|
+
|
|
301
|
+
DevTime is growing into a verification layer: ask whether a statement about the
|
|
302
|
+
repository is actually supported.
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
dtc verify billing-webhook-signature
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
Billing Webhook Signature Verification
|
|
310
|
+
Claim: Incoming billing webhooks verify the payment provider's signature.
|
|
311
|
+
Status: CONTRADICTED
|
|
312
|
+
|
|
313
|
+
Contradictions:
|
|
314
|
+
- The billing webhook endpoint cannot verify signatures because it is a disabled stub.
|
|
315
|
+
claimed: apps/web/pages/api/stripe/webhook.ts is named and routed as a billing webhook endpoint.
|
|
316
|
+
observed: The handler's only behavior is a 404/501 response.
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Statuses are SUPPORTED, WEAK, CONTRADICTED, or UNKNOWN; contradictions always
|
|
320
|
+
show both sides; changed evidence marks a claim STALE. One built-in claim ships
|
|
321
|
+
in v0.2. See **[VERIFICATION.md](VERIFICATION.md)**.
|
|
322
|
+
|
|
297
323
|
## Example output
|
|
298
324
|
|
|
299
325
|
```
|
|
@@ -29,6 +29,7 @@ src/devtime/intelligence/evidence.py
|
|
|
29
29
|
src/devtime/intelligence/lineage.py
|
|
30
30
|
src/devtime/intelligence/risk.py
|
|
31
31
|
src/devtime/intelligence/scoring.py
|
|
32
|
+
src/devtime/intelligence/verification.py
|
|
32
33
|
src/devtime/mcp/__init__.py
|
|
33
34
|
src/devtime/mcp/schemas.py
|
|
34
35
|
src/devtime/mcp/server.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/docs/decisions/0001-use-jwt.md
RENAMED
|
File without changes
|
|
File without changes
|
{devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/src/admin/permissions.ts
RENAMED
|
File without changes
|
|
File without changes
|
{devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/src/auth/middleware.ts
RENAMED
|
File without changes
|
|
File without changes
|
{devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/src/billing/stripe-webhook.ts
RENAMED
|
File without changes
|
|
File without changes
|
{devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/src/export/export-csv.ts
RENAMED
|
File without changes
|
{devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/src/jobs/email-worker.ts
RENAMED
|
File without changes
|
|
File without changes
|
{devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/tests/auth-login.test.ts
RENAMED
|
File without changes
|
{devtime_ei-0.1.3 → devtime_ei-0.2.0}/src/devtime/resources/demo-saas/tests/stripe-signature.test.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|