devtime-ei 0.3.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/PKG-INFO +15 -23
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/README.md +13 -21
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/pyproject.toml +2 -2
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/__init__.py +1 -1
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/cli.py +133 -11
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/intelligence/verification.py +415 -36
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/mcp/transport.py +30 -9
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime_ei.egg-info/PKG-INFO +15 -23
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/LICENSE +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/setup.cfg +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/ai/__init__.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/ai/local.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/ai/prompts.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/ai/providers.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/assets/devtimeignore.starter +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/config.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/db/__init__.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/db/connection.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/db/migrations.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/db/repository.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/db/schema.sql +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/demo.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/fixtures/__init__.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/fixtures/assertions.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/fixtures/loader.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/fixtures/runner.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/intelligence/__init__.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/intelligence/claims.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/intelligence/concepts.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/intelligence/context_pack.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/intelligence/evidence.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/intelligence/lineage.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/intelligence/risk.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/intelligence/scoring.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/mcp/__init__.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/mcp/schemas.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/mcp/server.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/mcp/tools.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/output/__init__.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/output/json_export.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/output/markdown.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/output/terminal.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/paths.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/privacy.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/.devtimeignore +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/README.md +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/docs/decisions/0001-use-jwt.md +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/package.json +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/src/admin/permissions.ts +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/src/auth/login.ts +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/src/auth/middleware.ts +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/src/auth/tokens.ts +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/src/billing/stripe-webhook.ts +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/src/billing/subscription-service.ts +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/src/export/export-csv.ts +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/src/jobs/email-worker.ts +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/src/jobs/queues.ts +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/tests/auth-login.test.ts +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/tests/stripe-signature.test.ts +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/scanner/__init__.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/scanner/extractors/__init__.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/scanner/extractors/base.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/scanner/extractors/config_files.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/scanner/extractors/docs.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/scanner/extractors/nextjs.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/scanner/extractors/python.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/scanner/extractors/tests.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/scanner/extractors/typescript.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/scanner/file_walker.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/scanner/ignore.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/scanner/language.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/scanner/signals.py +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime_ei.egg-info/SOURCES.txt +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime_ei.egg-info/dependency_links.txt +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime_ei.egg-info/entry_points.txt +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime_ei.egg-info/requires.txt +0 -0
- {devtime_ei-0.3.0 → devtime_ei-0.5.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.5.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.5.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,7 +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
|
|
207
|
+
| `dtc verify [claim]` | Verify repository claims against evidence: status, contradictions, freshness (experimental). |
|
|
208
208
|
|
|
209
209
|
(Also available: `dtc evidence`, `dtc debt`, `dtc status`, `dtc doctor --privacy`,
|
|
210
210
|
`dtc export`, `dtc reset`, `dtc mcp start`.)
|
|
@@ -299,26 +299,18 @@ pip install -e ".[dev]"
|
|
|
299
299
|
## Verify claims (experimental)
|
|
300
300
|
|
|
301
301
|
DevTime is growing into a verification layer: ask whether a statement about the
|
|
302
|
-
repository is actually supported.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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. Two built-in claims ship
|
|
321
|
-
(billing webhook signatures, JWT authentication). See **[VERIFICATION.md](VERIFICATION.md)**.
|
|
302
|
+
repository is actually supported. Watch a claim live through its whole life -
|
|
303
|
+
supported, contradicted by a stub, restored, and stale after the evidence
|
|
304
|
+
changes:
|
|
305
|
+
|
|
306
|
+

|
|
307
|
+
|
|
308
|
+
Statuses are SUPPORTED, WEAK, CONTRADICTED, UNKNOWN, or NOT_APPLICABLE;
|
|
309
|
+
contradictions always show both sides; changed evidence marks a claim STALE.
|
|
310
|
+
Four built-in claims ship: route test coverage, admin authorization, billing
|
|
311
|
+
webhook signatures, and JWT authentication. `dtc verify` leads with what it can
|
|
312
|
+
actually verify here, and when nothing applies it says what would make a claim
|
|
313
|
+
verifiable instead of dead-ending. See **[VERIFICATION.md](VERIFICATION.md)**.
|
|
322
314
|
|
|
323
315
|
## Example output
|
|
324
316
|
|
|
@@ -165,7 +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
|
|
168
|
+
| `dtc verify [claim]` | Verify repository claims against evidence: status, contradictions, freshness (experimental). |
|
|
169
169
|
|
|
170
170
|
(Also available: `dtc evidence`, `dtc debt`, `dtc status`, `dtc doctor --privacy`,
|
|
171
171
|
`dtc export`, `dtc reset`, `dtc mcp start`.)
|
|
@@ -260,26 +260,18 @@ pip install -e ".[dev]"
|
|
|
260
260
|
## Verify claims (experimental)
|
|
261
261
|
|
|
262
262
|
DevTime is growing into a verification layer: ask whether a statement about the
|
|
263
|
-
repository is actually supported.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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. Two built-in claims ship
|
|
282
|
-
(billing webhook signatures, JWT authentication). See **[VERIFICATION.md](VERIFICATION.md)**.
|
|
263
|
+
repository is actually supported. Watch a claim live through its whole life -
|
|
264
|
+
supported, contradicted by a stub, restored, and stale after the evidence
|
|
265
|
+
changes:
|
|
266
|
+
|
|
267
|
+

|
|
268
|
+
|
|
269
|
+
Statuses are SUPPORTED, WEAK, CONTRADICTED, UNKNOWN, or NOT_APPLICABLE;
|
|
270
|
+
contradictions always show both sides; changed evidence marks a claim STALE.
|
|
271
|
+
Four built-in claims ship: route test coverage, admin authorization, billing
|
|
272
|
+
webhook signatures, and JWT authentication. `dtc verify` leads with what it can
|
|
273
|
+
actually verify here, and when nothing applies it says what would make a claim
|
|
274
|
+
verifiable instead of dead-ending. See **[VERIFICATION.md](VERIFICATION.md)**.
|
|
283
275
|
|
|
284
276
|
## Example output
|
|
285
277
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "devtime-ei"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.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.5.0"
|
|
54
54
|
Demo = "https://youtu.be/1Hiu3Y9J_SI"
|
|
55
55
|
|
|
56
56
|
[project.scripts]
|
|
@@ -97,51 +97,71 @@ def verify(
|
|
|
97
97
|
conn = connection.connect()
|
|
98
98
|
try:
|
|
99
99
|
if list_claims:
|
|
100
|
+
# Relevance is computed live so the list answers the useful question:
|
|
101
|
+
# which of these claims apply to THIS repository?
|
|
102
|
+
current = {r.claim_slug: r for r in ver.verify_all(conn)}
|
|
100
103
|
rows = []
|
|
101
104
|
for slug, definition in ver.BUILTIN_CLAIMS.items():
|
|
102
105
|
latest = ver.load_latest_verification(conn, slug)
|
|
103
106
|
freshness, changed = ver.freshness_for(conn, slug)
|
|
107
|
+
live = current.get(slug)
|
|
104
108
|
rows.append(
|
|
105
109
|
{
|
|
106
110
|
"claim_id": slug,
|
|
107
111
|
"name": definition.name,
|
|
108
112
|
"statement": definition.statement,
|
|
113
|
+
"applies_here": bool(
|
|
114
|
+
live and live.status in ver.APPLICABLE_STATUSES
|
|
115
|
+
),
|
|
116
|
+
"current_status": live.status if live else None,
|
|
109
117
|
"last_status": latest[0]["status"] if latest else None,
|
|
110
118
|
"freshness": freshness,
|
|
111
119
|
"changed_evidence": changed,
|
|
112
120
|
}
|
|
113
121
|
)
|
|
122
|
+
rows.sort(key=lambda r: (not r["applies_here"], r["claim_id"]))
|
|
114
123
|
if as_json:
|
|
115
|
-
console.print_json(_json.dumps({"schema_version": "
|
|
124
|
+
console.print_json(_json.dumps({"schema_version": "2", "claims": rows}))
|
|
116
125
|
else:
|
|
117
126
|
console.print("[bold]Built-in claims[/bold]\n")
|
|
118
127
|
for r in rows:
|
|
128
|
+
if not r["applies_here"]:
|
|
129
|
+
console.print(f" [dim]{r['claim_id']} (not applicable here)[/dim]")
|
|
130
|
+
continue
|
|
119
131
|
status_txt = r["last_status"] or "never verified"
|
|
120
132
|
console.print(f" {r['claim_id']}")
|
|
121
133
|
console.print(f" {r['statement']}")
|
|
122
|
-
console.print(
|
|
134
|
+
console.print(
|
|
135
|
+
f" current: {r['current_status']} "
|
|
136
|
+
f"last verified: {status_txt} freshness: {r['freshness']}"
|
|
137
|
+
)
|
|
123
138
|
for p in r["changed_evidence"]:
|
|
124
139
|
console.print(f" changed since verification: {p}", markup=False)
|
|
125
140
|
console.print("")
|
|
126
141
|
return
|
|
127
142
|
|
|
128
|
-
|
|
129
|
-
results = []
|
|
130
|
-
for slug in slugs:
|
|
143
|
+
if claim:
|
|
131
144
|
try:
|
|
132
|
-
|
|
145
|
+
results = [ver.verify_claim(conn, claim)]
|
|
133
146
|
except KeyError:
|
|
134
|
-
console.print(f"[red]Unknown claim:[/red] {
|
|
147
|
+
console.print(f"[red]Unknown claim:[/red] {claim}")
|
|
135
148
|
console.print("Run [bold]dtc verify --list[/bold] to see built-in claims.")
|
|
136
149
|
raise typer.Exit(code=1)
|
|
137
|
-
|
|
138
|
-
results.
|
|
150
|
+
else:
|
|
151
|
+
results = ver.verify_all(conn)
|
|
152
|
+
|
|
153
|
+
# Only real verifications are recorded. A claim that does not apply to
|
|
154
|
+
# this repository was not verified, so storing it would pollute
|
|
155
|
+
# freshness and diff impact with claims that have no evidence.
|
|
156
|
+
for result in results:
|
|
157
|
+
if result.status in ver.APPLICABLE_STATUSES:
|
|
158
|
+
ver.save_verification(conn, result)
|
|
139
159
|
|
|
140
160
|
if as_json:
|
|
141
161
|
console.print_json(
|
|
142
162
|
_json.dumps(
|
|
143
163
|
{
|
|
144
|
-
"schema_version": "
|
|
164
|
+
"schema_version": "2",
|
|
145
165
|
"command": "verify",
|
|
146
166
|
"results": [r.to_dict() for r in results],
|
|
147
167
|
}
|
|
@@ -149,11 +169,94 @@ def verify(
|
|
|
149
169
|
)
|
|
150
170
|
return
|
|
151
171
|
|
|
152
|
-
|
|
172
|
+
_print_report(results, single=bool(claim))
|
|
173
|
+
finally:
|
|
174
|
+
conn.close()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _print_report(results: list, single: bool) -> None:
|
|
178
|
+
"""Report card: what DevTime can and cannot verify about this repository."""
|
|
179
|
+
from devtime.intelligence import verification as ver
|
|
180
|
+
|
|
181
|
+
applicable = [r for r in results if r.status in ver.APPLICABLE_STATUSES]
|
|
182
|
+
not_applicable = [r for r in results if r.status == ver.NOT_APPLICABLE]
|
|
183
|
+
|
|
184
|
+
for result in applicable:
|
|
185
|
+
_print_verification(result)
|
|
186
|
+
|
|
187
|
+
if not_applicable and not single:
|
|
188
|
+
console.print("[dim]Not applicable to this repository:[/dim]")
|
|
189
|
+
for r in not_applicable:
|
|
190
|
+
reason = r.why[0] if r.why else "No relevant surface was found."
|
|
191
|
+
console.print(f" - {r.claim_slug}: {reason}", markup=False)
|
|
192
|
+
console.print("")
|
|
193
|
+
|
|
194
|
+
if single and not applicable:
|
|
195
|
+
# An explicitly requested claim that does not apply still explains itself.
|
|
196
|
+
for result in not_applicable:
|
|
153
197
|
_print_verification(result)
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if not applicable:
|
|
201
|
+
_print_nothing_verifiable()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _print_nothing_verifiable() -> None:
|
|
205
|
+
"""Never a dead end: say what was scanned and what would unlock a claim."""
|
|
206
|
+
from devtime.db import connection
|
|
207
|
+
|
|
208
|
+
conn = connection.connect()
|
|
209
|
+
try:
|
|
210
|
+
scan = conn.execute(
|
|
211
|
+
"SELECT id, file_count, signal_count FROM scans WHERE status = 'completed' "
|
|
212
|
+
"ORDER BY started_at DESC LIMIT 1"
|
|
213
|
+
).fetchone()
|
|
214
|
+
kinds = []
|
|
215
|
+
if scan:
|
|
216
|
+
kinds = conn.execute(
|
|
217
|
+
"SELECT kind, COUNT(*) c FROM signals WHERE scan_id = ? "
|
|
218
|
+
"GROUP BY kind ORDER BY c DESC LIMIT 6",
|
|
219
|
+
(scan["id"],),
|
|
220
|
+
).fetchall()
|
|
154
221
|
finally:
|
|
155
222
|
conn.close()
|
|
156
223
|
|
|
224
|
+
console.print("[bold]No built-in claim applies to this repository yet.[/bold]")
|
|
225
|
+
console.print("")
|
|
226
|
+
if scan:
|
|
227
|
+
console.print(
|
|
228
|
+
f"DevTime scanned {scan['file_count']} files and found "
|
|
229
|
+
f"{scan['signal_count']} signals.",
|
|
230
|
+
markup=False,
|
|
231
|
+
)
|
|
232
|
+
if kinds:
|
|
233
|
+
summary = ", ".join(f"{k['kind']}={k['c']}" for k in kinds)
|
|
234
|
+
console.print(f"Evidence collected: {summary}", markup=False)
|
|
235
|
+
else:
|
|
236
|
+
console.print(
|
|
237
|
+
"No evidence was extracted, which usually means this repository's "
|
|
238
|
+
"language or framework is outside current scanner coverage.",
|
|
239
|
+
markup=False,
|
|
240
|
+
)
|
|
241
|
+
console.print("")
|
|
242
|
+
console.print("Built-in claims become verifiable when a repository has:")
|
|
243
|
+
console.print(" - HTTP routes and tests (route-test-coverage)")
|
|
244
|
+
console.print(" - admin, staff, or back-office routes (admin-authorization)")
|
|
245
|
+
console.print(" - JWT usage or a JWT dependency (jwt-authentication)")
|
|
246
|
+
console.print(" - billing webhooks or a payment provider (billing-webhook-signature)")
|
|
247
|
+
console.print("")
|
|
248
|
+
console.print(
|
|
249
|
+
"This is a coverage limit, not a verdict on your repository. "
|
|
250
|
+
"Scanner support is strongest on TypeScript, Next.js, Express, and "
|
|
251
|
+
"FastAPI-style code; see LIMITATIONS.md.",
|
|
252
|
+
markup=False,
|
|
253
|
+
)
|
|
254
|
+
console.print(
|
|
255
|
+
"If DevTime missed something your repository clearly has, that is worth "
|
|
256
|
+
"an issue: https://github.com/Shakargy/devtime/issues",
|
|
257
|
+
markup=False,
|
|
258
|
+
)
|
|
259
|
+
|
|
157
260
|
|
|
158
261
|
def _print_verification(result) -> None:
|
|
159
262
|
color = {
|
|
@@ -161,6 +264,7 @@ def _print_verification(result) -> None:
|
|
|
161
264
|
"WEAK": "yellow",
|
|
162
265
|
"CONTRADICTED": "red",
|
|
163
266
|
"UNKNOWN": "cyan",
|
|
267
|
+
"NOT_APPLICABLE": "dim",
|
|
164
268
|
}.get(result.status, "white")
|
|
165
269
|
console.print(f"[bold]{result.claim_name}[/bold]")
|
|
166
270
|
console.print(f"Claim: {result.statement}")
|
|
@@ -347,11 +451,29 @@ def risk(
|
|
|
347
451
|
conn = connection.connect()
|
|
348
452
|
try:
|
|
349
453
|
intelligence = repository.load_all_concepts(conn)
|
|
454
|
+
from devtime.intelligence.verification import claims_affected_by_paths
|
|
455
|
+
|
|
456
|
+
claim_impact = claims_affected_by_paths(conn, info.changed_files)
|
|
350
457
|
finally:
|
|
351
458
|
conn.close()
|
|
352
459
|
|
|
353
460
|
review = review_diff(info, intelligence)
|
|
354
461
|
console.print(render_risk_review(review), markup=False)
|
|
462
|
+
|
|
463
|
+
# v0.4.0: claim impact. A diff is not just risky in general - it can
|
|
464
|
+
# destabilize a previously verified claim. Only evidence files count;
|
|
465
|
+
# nothing is printed when no verified claim is affected.
|
|
466
|
+
if claim_impact:
|
|
467
|
+
console.print("\nClaim impact:", markup=False)
|
|
468
|
+
for item in claim_impact:
|
|
469
|
+
console.print(
|
|
470
|
+
f" - {item['claim_id']} (previous status: {item['previous_status']})",
|
|
471
|
+
markup=False,
|
|
472
|
+
)
|
|
473
|
+
for p in item["changed_evidence"]:
|
|
474
|
+
console.print(f" changed evidence: {p}", markup=False)
|
|
475
|
+
console.print(f" re-verify: {item['suggested_action']}", markup=False)
|
|
476
|
+
|
|
355
477
|
if review.state == STATE_REVIEW_FAILED:
|
|
356
478
|
raise typer.Exit(code=1)
|
|
357
479
|
|
|
@@ -6,19 +6,22 @@ persisted scan evidence and answers with a status, both-sided contradictions,
|
|
|
6
6
|
missing evidence, coverage limitations, and freshness - never with confidence
|
|
7
7
|
the evidence cannot back.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Scope, deliberately narrow:
|
|
10
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
11
|
- Deterministic and rule-driven. No AI, no network, no code execution.
|
|
12
|
+
- Freshness from file fingerprints: FRESH, STALE, NEEDS_VERIFICATION.
|
|
15
13
|
|
|
16
14
|
Statuses (documented meaning, per repository evidence policy - not formal proof):
|
|
17
|
-
SUPPORTED
|
|
18
|
-
WEAK
|
|
19
|
-
CONTRADICTED
|
|
20
|
-
UNKNOWN
|
|
21
|
-
|
|
15
|
+
SUPPORTED required behavior evidence exists in the current scan.
|
|
16
|
+
WEAK the claim's surface exists, but the proving evidence is missing.
|
|
17
|
+
CONTRADICTED credible evidence conflicts with the claim; both sides are shown.
|
|
18
|
+
UNKNOWN the surface exists but coverage cannot responsibly decide.
|
|
19
|
+
NOT_APPLICABLE the repository has no surface this claim is about (v0.5.0).
|
|
20
|
+
|
|
21
|
+
NOT_APPLICABLE matters as much as the others. A repository with no billing code
|
|
22
|
+
is not "unknown" for a billing claim - the claim simply does not apply, and
|
|
23
|
+
saying so plainly is more honest than an ominous UNKNOWN. UNKNOWN is reserved
|
|
24
|
+
for the harder case: the surface exists, but the evidence cannot decide.
|
|
22
25
|
|
|
23
26
|
Freshness is separate from truth:
|
|
24
27
|
FRESH supporting evidence files are unchanged since verification.
|
|
@@ -41,6 +44,10 @@ SUPPORTED = "SUPPORTED"
|
|
|
41
44
|
WEAK = "WEAK"
|
|
42
45
|
CONTRADICTED = "CONTRADICTED"
|
|
43
46
|
UNKNOWN = "UNKNOWN"
|
|
47
|
+
NOT_APPLICABLE = "NOT_APPLICABLE"
|
|
48
|
+
|
|
49
|
+
# Statuses that mean "this claim has something to say about this repository".
|
|
50
|
+
APPLICABLE_STATUSES = (SUPPORTED, WEAK, CONTRADICTED, UNKNOWN)
|
|
44
51
|
|
|
45
52
|
# Freshness
|
|
46
53
|
FRESH = "FRESH"
|
|
@@ -117,8 +124,11 @@ class VerificationResult:
|
|
|
117
124
|
engine_version: str
|
|
118
125
|
|
|
119
126
|
def to_dict(self) -> dict:
|
|
127
|
+
# schema_version 2 (v0.5.0): adds the NOT_APPLICABLE status value. All
|
|
128
|
+
# version 1 fields are unchanged, so consumers that ignore unknown
|
|
129
|
+
# status values keep working.
|
|
120
130
|
return {
|
|
121
|
-
"schema_version": "
|
|
131
|
+
"schema_version": "2",
|
|
122
132
|
"claim_id": self.claim_slug,
|
|
123
133
|
"claim_name": self.claim_name,
|
|
124
134
|
"statement": self.statement,
|
|
@@ -155,6 +165,18 @@ BUILTIN_CLAIMS: dict[str, ClaimDefinition] = {
|
|
|
155
165
|
statement="Authentication uses JWT access tokens.",
|
|
156
166
|
category="authentication",
|
|
157
167
|
),
|
|
168
|
+
"route-test-coverage": ClaimDefinition(
|
|
169
|
+
slug="route-test-coverage",
|
|
170
|
+
name="Route Test Coverage",
|
|
171
|
+
statement="HTTP routes are exercised by tests.",
|
|
172
|
+
category="testing",
|
|
173
|
+
),
|
|
174
|
+
"admin-authorization": ClaimDefinition(
|
|
175
|
+
slug="admin-authorization",
|
|
176
|
+
name="Admin Authorization",
|
|
177
|
+
statement="Administrative routes require an authorization check.",
|
|
178
|
+
category="security",
|
|
179
|
+
),
|
|
158
180
|
}
|
|
159
181
|
|
|
160
182
|
# JWT library names for dependency evidence.
|
|
@@ -210,6 +232,23 @@ def _is_billingish(hay: str) -> bool:
|
|
|
210
232
|
# The engine
|
|
211
233
|
# --------------------------------------------------------------------------- #
|
|
212
234
|
|
|
235
|
+
def _no_scan_result(definition: ClaimDefinition) -> VerificationResult:
|
|
236
|
+
return VerificationResult(
|
|
237
|
+
claim_slug=definition.slug,
|
|
238
|
+
claim_name=definition.name,
|
|
239
|
+
statement=definition.statement,
|
|
240
|
+
status=UNKNOWN,
|
|
241
|
+
why=["No completed scan exists. Run dtc scan first."],
|
|
242
|
+
supporting=[],
|
|
243
|
+
contradictions=[],
|
|
244
|
+
missing=["A completed repository scan."],
|
|
245
|
+
limitations=_LIMITATIONS,
|
|
246
|
+
scan_id=None,
|
|
247
|
+
verified_at=_now(),
|
|
248
|
+
engine_version=__version__,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
213
252
|
def verify_claim(conn: sqlite3.Connection, slug: str) -> VerificationResult:
|
|
214
253
|
"""Verify one built-in claim against the latest completed scan."""
|
|
215
254
|
definition = BUILTIN_CLAIMS.get(slug)
|
|
@@ -218,24 +257,31 @@ def verify_claim(conn: sqlite3.Connection, slug: str) -> VerificationResult:
|
|
|
218
257
|
|
|
219
258
|
scan_id = _latest_scan_id(conn)
|
|
220
259
|
if scan_id is None:
|
|
221
|
-
return
|
|
222
|
-
claim_slug=definition.slug,
|
|
223
|
-
claim_name=definition.name,
|
|
224
|
-
statement=definition.statement,
|
|
225
|
-
status=UNKNOWN,
|
|
226
|
-
why=["No completed scan exists. Run dtc scan first."],
|
|
227
|
-
supporting=[],
|
|
228
|
-
contradictions=[],
|
|
229
|
-
missing=["A completed repository scan."],
|
|
230
|
-
limitations=_LIMITATIONS,
|
|
231
|
-
scan_id=None,
|
|
232
|
-
verified_at=_now(),
|
|
233
|
-
engine_version=__version__,
|
|
234
|
-
)
|
|
260
|
+
return _no_scan_result(definition)
|
|
235
261
|
|
|
236
262
|
rows = _load_signals(conn, scan_id)
|
|
237
|
-
|
|
238
|
-
|
|
263
|
+
return _EVALUATORS[slug](definition, rows, scan_id)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def verify_all(conn: sqlite3.Connection) -> list[VerificationResult]:
|
|
267
|
+
"""Verify every built-in claim, loading scan evidence exactly once.
|
|
268
|
+
|
|
269
|
+
Applicable results (something to say about this repository) sort first, so
|
|
270
|
+
the first thing a user reads is what DevTime actually found.
|
|
271
|
+
"""
|
|
272
|
+
scan_id = _latest_scan_id(conn)
|
|
273
|
+
if scan_id is None:
|
|
274
|
+
return [_no_scan_result(d) for d in BUILTIN_CLAIMS.values()]
|
|
275
|
+
|
|
276
|
+
rows = _load_signals(conn, scan_id)
|
|
277
|
+
results = [
|
|
278
|
+
_EVALUATORS[slug](definition, rows, scan_id)
|
|
279
|
+
for slug, definition in BUILTIN_CLAIMS.items()
|
|
280
|
+
]
|
|
281
|
+
# Contradictions first: they are the findings a user most needs to see.
|
|
282
|
+
order = {CONTRADICTED: 0, SUPPORTED: 1, WEAK: 2, UNKNOWN: 3, NOT_APPLICABLE: 4}
|
|
283
|
+
results.sort(key=lambda r: (order.get(r.status, 9), r.claim_slug))
|
|
284
|
+
return results
|
|
239
285
|
|
|
240
286
|
|
|
241
287
|
def _verify_billing_webhook_signature(
|
|
@@ -335,12 +381,12 @@ def _verify_billing_webhook_signature(
|
|
|
335
381
|
if not signature_tests:
|
|
336
382
|
missing.append("A test that exercises webhook signature verification.")
|
|
337
383
|
else:
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
"
|
|
384
|
+
return _not_applicable(
|
|
385
|
+
definition,
|
|
386
|
+
scan_id,
|
|
387
|
+
"No billing webhook surface was found in the scanned files.",
|
|
388
|
+
"A billing webhook route, handler, or payment provider dependency.",
|
|
342
389
|
)
|
|
343
|
-
missing.append("Any billing webhook route, handler, or provider dependency.")
|
|
344
390
|
|
|
345
391
|
return VerificationResult(
|
|
346
392
|
claim_slug=definition.slug,
|
|
@@ -458,12 +504,12 @@ def _verify_jwt_authentication(
|
|
|
458
504
|
)
|
|
459
505
|
missing.append("JWT access-token usage (login/bearer/authorization context).")
|
|
460
506
|
else:
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
"
|
|
507
|
+
return _not_applicable(
|
|
508
|
+
definition,
|
|
509
|
+
scan_id,
|
|
510
|
+
"No JWT evidence was found in the scanned files.",
|
|
511
|
+
"JWT usage, a JWT library dependency, or documentation referencing JWT.",
|
|
465
512
|
)
|
|
466
|
-
missing.append("Any JWT usage, dependency, or documentation.")
|
|
467
513
|
|
|
468
514
|
return VerificationResult(
|
|
469
515
|
claim_slug=definition.slug,
|
|
@@ -481,9 +527,310 @@ def _verify_jwt_authentication(
|
|
|
481
527
|
)
|
|
482
528
|
|
|
483
529
|
|
|
530
|
+
# --------------------------------------------------------------------------- #
|
|
531
|
+
# Route test coverage (v0.5.0)
|
|
532
|
+
# --------------------------------------------------------------------------- #
|
|
533
|
+
|
|
534
|
+
# Route path segments that carry no identity and must not be used for matching.
|
|
535
|
+
_GENERIC_SEGMENTS = {"api", "v1", "v2", "v3", "app", "index", "route", "routes", "src"}
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _module_token(path: str) -> str:
|
|
539
|
+
"""The distinctive file stem of an implementation file, lowercased."""
|
|
540
|
+
stem = path.rsplit("/", 1)[-1]
|
|
541
|
+
for suffix in (".ts", ".tsx", ".js", ".jsx", ".py", ".mjs", ".cjs"):
|
|
542
|
+
if stem.endswith(suffix):
|
|
543
|
+
stem = stem[: -len(suffix)]
|
|
544
|
+
break
|
|
545
|
+
return stem.lower()
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _route_tokens(route_path: str) -> list[str]:
|
|
549
|
+
"""Distinctive, non-generic segments of a route path."""
|
|
550
|
+
out = []
|
|
551
|
+
for seg in route_path.lower().replace("\\", "/").split("/"):
|
|
552
|
+
seg = seg.strip()
|
|
553
|
+
if not seg or seg.startswith(("[", ":", "{", "<")) or seg in _GENERIC_SEGMENTS:
|
|
554
|
+
continue
|
|
555
|
+
if len(seg) < 3:
|
|
556
|
+
continue
|
|
557
|
+
out.append(seg)
|
|
558
|
+
return out
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _verify_route_test_coverage(
|
|
562
|
+
definition: ClaimDefinition, rows: list[sqlite3.Row], scan_id: str
|
|
563
|
+
) -> VerificationResult:
|
|
564
|
+
"""Verify that HTTP routes are exercised by tests.
|
|
565
|
+
|
|
566
|
+
Matching is deliberately conservative and explainable. A route counts as
|
|
567
|
+
covered when a test file either imports the route's implementation module,
|
|
568
|
+
or names a distinctive segment of the route path. Test files are aggregated
|
|
569
|
+
first so the comparison stays linear in test FILES, not test cases (large
|
|
570
|
+
repos have thousands of test cases across a few dozen files).
|
|
571
|
+
|
|
572
|
+
Absence of tests is missing evidence, never a contradiction.
|
|
573
|
+
"""
|
|
574
|
+
# Aggregate tests per file: imports + a single blob of test names.
|
|
575
|
+
test_imports: dict[str, set[str]] = {}
|
|
576
|
+
test_blobs: dict[str, list[str]] = {}
|
|
577
|
+
for row in rows:
|
|
578
|
+
if row["kind"] != "test":
|
|
579
|
+
continue
|
|
580
|
+
try:
|
|
581
|
+
meta = json.loads(row["metadata_json"] or "{}")
|
|
582
|
+
except json.JSONDecodeError:
|
|
583
|
+
meta = {}
|
|
584
|
+
if meta.get("e2e"):
|
|
585
|
+
# E2E specs match by accident (Reality Validation finding); they are
|
|
586
|
+
# weak evidence for concepts and unreliable for route attribution.
|
|
587
|
+
continue
|
|
588
|
+
path = row["path"]
|
|
589
|
+
imports = test_imports.setdefault(path, set())
|
|
590
|
+
for imp in meta.get("imports") or []:
|
|
591
|
+
imports.add(str(imp).lower())
|
|
592
|
+
test_blobs.setdefault(path, []).append(str(row["name"] or "").lower())
|
|
593
|
+
|
|
594
|
+
# One joined blob per test file keeps matching linear in test FILES and turns
|
|
595
|
+
# each check into a single substring scan.
|
|
596
|
+
test_name_blob = {p: " ".join(names) for p, names in test_blobs.items()}
|
|
597
|
+
test_import_blob = {p: " ".join(sorted(i)) for p, i in test_imports.items()}
|
|
598
|
+
|
|
599
|
+
# Deduplicate routes: several methods on one path are one surface to cover.
|
|
600
|
+
routes: dict[tuple[str, str], sqlite3.Row] = {}
|
|
601
|
+
for row in rows:
|
|
602
|
+
if row["kind"] != "route":
|
|
603
|
+
continue
|
|
604
|
+
try:
|
|
605
|
+
meta = json.loads(row["metadata_json"] or "{}")
|
|
606
|
+
except json.JSONDecodeError:
|
|
607
|
+
meta = {}
|
|
608
|
+
route_path = str(meta.get("path") or row["name"] or "").strip()
|
|
609
|
+
routes.setdefault((row["path"], route_path.lower()), row)
|
|
610
|
+
|
|
611
|
+
if not routes:
|
|
612
|
+
return _not_applicable(
|
|
613
|
+
definition,
|
|
614
|
+
scan_id,
|
|
615
|
+
"No HTTP routes were found in the scanned files.",
|
|
616
|
+
"Any HTTP route (Express, Next.js, or FastAPI style).",
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
covered: list[tuple[str, str, str]] = [] # (impl path, route path, reason)
|
|
620
|
+
uncovered: list[tuple[str, str]] = []
|
|
621
|
+
for (impl_path, route_path), row in sorted(routes.items()):
|
|
622
|
+
token = _module_token(impl_path)
|
|
623
|
+
reason = ""
|
|
624
|
+
# 1. A test that imports the implementation module.
|
|
625
|
+
if token and len(token) >= 3:
|
|
626
|
+
for test_path, blob in test_import_blob.items():
|
|
627
|
+
if token in blob:
|
|
628
|
+
reason = f"{test_path} imports {token}"
|
|
629
|
+
break
|
|
630
|
+
# 2. A test whose names mention a distinctive segment of the route path.
|
|
631
|
+
if not reason:
|
|
632
|
+
segments = _route_tokens(route_path)
|
|
633
|
+
for test_path, blob in test_name_blob.items():
|
|
634
|
+
if segments and any(seg in blob for seg in segments):
|
|
635
|
+
reason = f"{test_path} names {segments[0]}"
|
|
636
|
+
break
|
|
637
|
+
if reason:
|
|
638
|
+
covered.append((impl_path, route_path, reason))
|
|
639
|
+
else:
|
|
640
|
+
uncovered.append((impl_path, route_path))
|
|
641
|
+
|
|
642
|
+
total = len(routes)
|
|
643
|
+
n_covered = len(covered)
|
|
644
|
+
# Evidence is bounded (responses and stored fingerprints must stay bounded),
|
|
645
|
+
# and truncation is disclosed below rather than hidden.
|
|
646
|
+
_EVIDENCE_CAP = 25
|
|
647
|
+
sha_by_path = {row["path"]: row["sha256"] for row in rows}
|
|
648
|
+
supporting = [
|
|
649
|
+
EvidenceRef(
|
|
650
|
+
path=impl,
|
|
651
|
+
observation=f"Route {route or impl} is referenced by a test ({reason}).",
|
|
652
|
+
kind="route",
|
|
653
|
+
strength="moderate",
|
|
654
|
+
sha256=sha_by_path.get(impl),
|
|
655
|
+
)
|
|
656
|
+
for impl, route, reason in covered[:_EVIDENCE_CAP]
|
|
657
|
+
]
|
|
658
|
+
|
|
659
|
+
why = [f"{n_covered} of {total} routes have a referencing test."]
|
|
660
|
+
missing: list[str] = []
|
|
661
|
+
if n_covered == total:
|
|
662
|
+
status = SUPPORTED
|
|
663
|
+
why.append("Every detected route has at least one test referencing it.")
|
|
664
|
+
else:
|
|
665
|
+
status = WEAK
|
|
666
|
+
why.append(
|
|
667
|
+
"Routes without a referencing test are not proven to be exercised."
|
|
668
|
+
)
|
|
669
|
+
shown = [r or p for p, r in uncovered[:8]]
|
|
670
|
+
missing.append(
|
|
671
|
+
f"Tests referencing {total - n_covered} route(s): " + ", ".join(shown)
|
|
672
|
+
+ (" ..." if len(uncovered) > 8 else "")
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
limitations = _LIMITATIONS + [
|
|
676
|
+
"Coverage is attributed by test imports and route names, not by executing "
|
|
677
|
+
"tests; a route exercised only indirectly may be reported as uncovered.",
|
|
678
|
+
"End-to-end specs are excluded from attribution because they match by "
|
|
679
|
+
"accident.",
|
|
680
|
+
]
|
|
681
|
+
if len(covered) > _EVIDENCE_CAP:
|
|
682
|
+
limitations.append(
|
|
683
|
+
f"Evidence is capped at {_EVIDENCE_CAP} routes; freshness tracks only "
|
|
684
|
+
f"those recorded files, not all {len(covered)} covered routes."
|
|
685
|
+
)
|
|
686
|
+
return VerificationResult(
|
|
687
|
+
claim_slug=definition.slug,
|
|
688
|
+
claim_name=definition.name,
|
|
689
|
+
statement=definition.statement,
|
|
690
|
+
status=status,
|
|
691
|
+
why=why,
|
|
692
|
+
supporting=supporting,
|
|
693
|
+
contradictions=[],
|
|
694
|
+
missing=missing,
|
|
695
|
+
limitations=limitations,
|
|
696
|
+
scan_id=scan_id,
|
|
697
|
+
verified_at=_now(),
|
|
698
|
+
engine_version=__version__,
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
# --------------------------------------------------------------------------- #
|
|
703
|
+
# Admin authorization (v0.5.0)
|
|
704
|
+
# --------------------------------------------------------------------------- #
|
|
705
|
+
|
|
706
|
+
_ADMIN_TOKENS = ("admin", "superuser", "staff", "backoffice", "back-office")
|
|
707
|
+
_AUTHZ_TOKENS = (
|
|
708
|
+
"requireadmin", "require_admin", "isadmin", "is_admin", "adminonly",
|
|
709
|
+
"admin_only", "hasrole", "has_role", "authorize", "authorization",
|
|
710
|
+
"permission", "rbac", "requireauth", "require_auth", "isauthenticated",
|
|
711
|
+
"current_user", "get_current_user", "authmiddleware", "auth_middleware",
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _verify_admin_authorization(
|
|
716
|
+
definition: ClaimDefinition, rows: list[sqlite3.Row], scan_id: str
|
|
717
|
+
) -> VerificationResult:
|
|
718
|
+
"""Verify that administrative routes require an authorization check.
|
|
719
|
+
|
|
720
|
+
Honesty rule for this claim: a missing authorization signal is WEAK, never
|
|
721
|
+
CONTRADICTED. Authorization can be applied globally, by a decorator, or by a
|
|
722
|
+
wrapper the scanner cannot see. Telling someone their admin endpoint is
|
|
723
|
+
unprotected when it is not would destroy the trust this tool is built on.
|
|
724
|
+
"""
|
|
725
|
+
admin_routes: list[sqlite3.Row] = []
|
|
726
|
+
authz_files: set[str] = set()
|
|
727
|
+
authz_rows: list[sqlite3.Row] = []
|
|
728
|
+
|
|
729
|
+
for row in rows:
|
|
730
|
+
hay = _hay(row)
|
|
731
|
+
kind = row["kind"]
|
|
732
|
+
if kind == "route" and any(t in hay for t in _ADMIN_TOKENS):
|
|
733
|
+
admin_routes.append(row)
|
|
734
|
+
if kind in ("middleware", "auth_dependency") or any(
|
|
735
|
+
t in hay for t in _AUTHZ_TOKENS
|
|
736
|
+
):
|
|
737
|
+
if kind in ("middleware", "auth_dependency", "route", "test"):
|
|
738
|
+
authz_files.add(row["path"])
|
|
739
|
+
if kind in ("middleware", "auth_dependency"):
|
|
740
|
+
authz_rows.append(row)
|
|
741
|
+
|
|
742
|
+
if not admin_routes:
|
|
743
|
+
return _not_applicable(
|
|
744
|
+
definition,
|
|
745
|
+
scan_id,
|
|
746
|
+
"No administrative routes were found in the scanned files.",
|
|
747
|
+
"An admin, staff, or back-office route.",
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
protected: list[sqlite3.Row] = []
|
|
751
|
+
unprotected: list[sqlite3.Row] = []
|
|
752
|
+
for row in admin_routes:
|
|
753
|
+
hay = _hay(row)
|
|
754
|
+
# Authorization evidence in the route's own file, or in the route itself.
|
|
755
|
+
if row["path"] in authz_files or any(t in hay for t in _AUTHZ_TOKENS):
|
|
756
|
+
protected.append(row)
|
|
757
|
+
else:
|
|
758
|
+
unprotected.append(row)
|
|
759
|
+
|
|
760
|
+
supporting = [
|
|
761
|
+
_ref(r, "Admin route shows an authorization check in its file.", "moderate")
|
|
762
|
+
for r in protected[:5]
|
|
763
|
+
] + [
|
|
764
|
+
_ref(r, "Authorization middleware or dependency.", "moderate")
|
|
765
|
+
for r in authz_rows[:2]
|
|
766
|
+
]
|
|
767
|
+
|
|
768
|
+
total = len(admin_routes)
|
|
769
|
+
why = [f"{len(protected)} of {total} administrative route(s) show an "
|
|
770
|
+
"authorization check."]
|
|
771
|
+
missing: list[str] = []
|
|
772
|
+
|
|
773
|
+
if not unprotected:
|
|
774
|
+
status = SUPPORTED
|
|
775
|
+
why.append("Every detected admin route has authorization evidence.")
|
|
776
|
+
else:
|
|
777
|
+
status = WEAK
|
|
778
|
+
why.append(
|
|
779
|
+
"No authorization evidence was found for the remaining admin route(s). "
|
|
780
|
+
"This is missing evidence, not proof that they are unprotected."
|
|
781
|
+
)
|
|
782
|
+
missing.append(
|
|
783
|
+
"Authorization evidence for: "
|
|
784
|
+
+ ", ".join(sorted({r["path"] for r in unprotected})[:6])
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
limitations = _LIMITATIONS + [
|
|
788
|
+
"Authorization applied globally (a server-wide middleware, a router "
|
|
789
|
+
"mount, or a framework decorator the scanner does not parse) is not "
|
|
790
|
+
"detected. A WEAK result means DevTime found no evidence, never that a "
|
|
791
|
+
"route is confirmed unprotected.",
|
|
792
|
+
]
|
|
793
|
+
return VerificationResult(
|
|
794
|
+
claim_slug=definition.slug,
|
|
795
|
+
claim_name=definition.name,
|
|
796
|
+
statement=definition.statement,
|
|
797
|
+
status=status,
|
|
798
|
+
why=why,
|
|
799
|
+
supporting=supporting,
|
|
800
|
+
contradictions=[],
|
|
801
|
+
missing=missing,
|
|
802
|
+
limitations=limitations,
|
|
803
|
+
scan_id=scan_id,
|
|
804
|
+
verified_at=_now(),
|
|
805
|
+
engine_version=__version__,
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def _not_applicable(
|
|
810
|
+
definition: ClaimDefinition, scan_id: str, reason: str, would_need: str
|
|
811
|
+
) -> VerificationResult:
|
|
812
|
+
"""This claim has no surface in this repository. Say so plainly."""
|
|
813
|
+
return VerificationResult(
|
|
814
|
+
claim_slug=definition.slug,
|
|
815
|
+
claim_name=definition.name,
|
|
816
|
+
statement=definition.statement,
|
|
817
|
+
status=NOT_APPLICABLE,
|
|
818
|
+
why=[reason, "This claim does not apply to this repository."],
|
|
819
|
+
supporting=[],
|
|
820
|
+
contradictions=[],
|
|
821
|
+
missing=[f"Would become verifiable with: {would_need}"],
|
|
822
|
+
limitations=_LIMITATIONS,
|
|
823
|
+
scan_id=scan_id,
|
|
824
|
+
verified_at=_now(),
|
|
825
|
+
engine_version=__version__,
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
|
|
484
829
|
_EVALUATORS = {
|
|
485
830
|
"billing-webhook-signature": _verify_billing_webhook_signature,
|
|
486
831
|
"jwt-authentication": _verify_jwt_authentication,
|
|
832
|
+
"route-test-coverage": _verify_route_test_coverage,
|
|
833
|
+
"admin-authorization": _verify_admin_authorization,
|
|
487
834
|
}
|
|
488
835
|
|
|
489
836
|
|
|
@@ -580,6 +927,38 @@ def load_latest_verification(
|
|
|
580
927
|
)
|
|
581
928
|
|
|
582
929
|
|
|
930
|
+
def claims_affected_by_paths(
|
|
931
|
+
conn: sqlite3.Connection, changed_paths: list[str]
|
|
932
|
+
) -> list[dict]:
|
|
933
|
+
"""Claim impact for a set of changed paths (v0.4.0, diff integration).
|
|
934
|
+
|
|
935
|
+
For every claim with a stored verification, report it when a changed path
|
|
936
|
+
is one of its recorded evidence files. Only evidence files count: a diff
|
|
937
|
+
touching unrelated files never flags a claim. Advisory output - the caller
|
|
938
|
+
decides what to do with it.
|
|
939
|
+
"""
|
|
940
|
+
changed = set(changed_paths)
|
|
941
|
+
out: list[dict] = []
|
|
942
|
+
for slug in BUILTIN_CLAIMS:
|
|
943
|
+
latest = load_latest_verification(conn, slug)
|
|
944
|
+
if latest is None:
|
|
945
|
+
continue
|
|
946
|
+
result, fingerprints, created_at = latest
|
|
947
|
+
evidence_paths = {fp["path"] for fp in fingerprints}
|
|
948
|
+
hits = sorted(evidence_paths & changed)
|
|
949
|
+
if hits:
|
|
950
|
+
out.append(
|
|
951
|
+
{
|
|
952
|
+
"claim_id": slug,
|
|
953
|
+
"previous_status": result["status"],
|
|
954
|
+
"verified_at": created_at,
|
|
955
|
+
"changed_evidence": hits,
|
|
956
|
+
"suggested_action": f"dtc verify {slug}",
|
|
957
|
+
}
|
|
958
|
+
)
|
|
959
|
+
return out
|
|
960
|
+
|
|
961
|
+
|
|
583
962
|
def freshness_for(conn: sqlite3.Connection, slug: str) -> tuple[str, list[str]]:
|
|
584
963
|
"""Compare stored evidence fingerprints against current file hashes.
|
|
585
964
|
|
|
@@ -52,14 +52,32 @@ class McpDependencyMissing(RuntimeError):
|
|
|
52
52
|
INSTALL_HINT = 'MCP support needs the optional dependency: pip install "devtime-ei[mcp]"'
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def
|
|
56
|
-
"""
|
|
57
|
-
|
|
55
|
+
def _server_class():
|
|
56
|
+
"""Return the SDK's server class across MCP SDK generations.
|
|
57
|
+
|
|
58
|
+
The SDK renamed its high-level server in 2.0: `mcp.server.fastmcp.FastMCP`
|
|
59
|
+
became `mcp.server.MCPServer`. Both expose the surface DevTime uses (a
|
|
60
|
+
`tool()` decorator, async `list_tools`/`call_tool`, and a stdio `run`), so
|
|
61
|
+
both are supported rather than pinning users to one generation.
|
|
62
|
+
"""
|
|
63
|
+
try: # MCP SDK 2.x
|
|
64
|
+
from mcp.server import MCPServer
|
|
65
|
+
|
|
66
|
+
return MCPServer
|
|
67
|
+
except ImportError:
|
|
68
|
+
pass
|
|
69
|
+
try: # MCP SDK 1.x
|
|
58
70
|
from mcp.server.fastmcp import FastMCP
|
|
71
|
+
|
|
72
|
+
return FastMCP
|
|
59
73
|
except ImportError as exc: # pragma: no cover - exercised via CLI test
|
|
60
74
|
raise McpDependencyMissing(McpDependencyMissing.INSTALL_HINT) from exc
|
|
61
75
|
|
|
62
|
-
|
|
76
|
+
|
|
77
|
+
def build_server():
|
|
78
|
+
"""Build the MCP server with the read-only tool surface registered."""
|
|
79
|
+
server_class = _server_class()
|
|
80
|
+
server = server_class(name=SERVER_NAME, instructions=SERVER_INSTRUCTIONS)
|
|
63
81
|
|
|
64
82
|
@server.tool()
|
|
65
83
|
def list_concepts(limit: int = 50) -> list[dict] | dict:
|
|
@@ -100,11 +118,14 @@ def build_server():
|
|
|
100
118
|
def verify_claim(claim_id: str = "") -> dict:
|
|
101
119
|
"""Verify a repository claim against scanned evidence (read-only compute).
|
|
102
120
|
|
|
103
|
-
Returns status (SUPPORTED / WEAK / CONTRADICTED / UNKNOWN
|
|
104
|
-
supporting evidence with file paths, both-sided
|
|
105
|
-
evidence, and coverage limitations.
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
Returns status (SUPPORTED / WEAK / CONTRADICTED / UNKNOWN /
|
|
122
|
+
NOT_APPLICABLE), why, supporting evidence with file paths, both-sided
|
|
123
|
+
contradictions, missing evidence, and coverage limitations.
|
|
124
|
+
NOT_APPLICABLE means the repository has no surface this claim is about,
|
|
125
|
+
which is different from UNKNOWN (surface exists, evidence cannot
|
|
126
|
+
decide). Call with no claim_id to list the built-in claims. Results are
|
|
127
|
+
computed fresh and NOT persisted (this server stays read-only); use
|
|
128
|
+
`dtc verify` in a terminal to record one.
|
|
108
129
|
"""
|
|
109
130
|
if not paths.is_initialized():
|
|
110
131
|
return _NOT_INITIALIZED
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devtime-ei
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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.5.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,7 +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
|
|
207
|
+
| `dtc verify [claim]` | Verify repository claims against evidence: status, contradictions, freshness (experimental). |
|
|
208
208
|
|
|
209
209
|
(Also available: `dtc evidence`, `dtc debt`, `dtc status`, `dtc doctor --privacy`,
|
|
210
210
|
`dtc export`, `dtc reset`, `dtc mcp start`.)
|
|
@@ -299,26 +299,18 @@ pip install -e ".[dev]"
|
|
|
299
299
|
## Verify claims (experimental)
|
|
300
300
|
|
|
301
301
|
DevTime is growing into a verification layer: ask whether a statement about the
|
|
302
|
-
repository is actually supported.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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. Two built-in claims ship
|
|
321
|
-
(billing webhook signatures, JWT authentication). See **[VERIFICATION.md](VERIFICATION.md)**.
|
|
302
|
+
repository is actually supported. Watch a claim live through its whole life -
|
|
303
|
+
supported, contradicted by a stub, restored, and stale after the evidence
|
|
304
|
+
changes:
|
|
305
|
+
|
|
306
|
+

|
|
307
|
+
|
|
308
|
+
Statuses are SUPPORTED, WEAK, CONTRADICTED, UNKNOWN, or NOT_APPLICABLE;
|
|
309
|
+
contradictions always show both sides; changed evidence marks a claim STALE.
|
|
310
|
+
Four built-in claims ship: route test coverage, admin authorization, billing
|
|
311
|
+
webhook signatures, and JWT authentication. `dtc verify` leads with what it can
|
|
312
|
+
actually verify here, and when nothing applies it says what would make a claim
|
|
313
|
+
verifiable instead of dead-ending. See **[VERIFICATION.md](VERIFICATION.md)**.
|
|
322
314
|
|
|
323
315
|
## Example output
|
|
324
316
|
|
|
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
|
|
File without changes
|
{devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/docs/decisions/0001-use-jwt.md
RENAMED
|
File without changes
|
|
File without changes
|
{devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/src/admin/permissions.ts
RENAMED
|
File without changes
|
|
File without changes
|
{devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/src/auth/middleware.ts
RENAMED
|
File without changes
|
|
File without changes
|
{devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/src/billing/stripe-webhook.ts
RENAMED
|
File without changes
|
|
File without changes
|
{devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/src/export/export-csv.ts
RENAMED
|
File without changes
|
{devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/src/jobs/email-worker.ts
RENAMED
|
File without changes
|
|
File without changes
|
{devtime_ei-0.3.0 → devtime_ei-0.5.0}/src/devtime/resources/demo-saas/tests/auth-login.test.ts
RENAMED
|
File without changes
|
{devtime_ei-0.3.0 → devtime_ei-0.5.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
|
|
File without changes
|
|
File without changes
|