devtime-ei 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devtime/__init__.py +9 -0
- devtime/ai/__init__.py +0 -0
- devtime/ai/local.py +11 -0
- devtime/ai/prompts.py +24 -0
- devtime/ai/providers.py +41 -0
- devtime/assets/devtimeignore.starter +23 -0
- devtime/cli.py +374 -0
- devtime/config.py +67 -0
- devtime/db/__init__.py +0 -0
- devtime/db/connection.py +16 -0
- devtime/db/migrations.py +114 -0
- devtime/db/repository.py +351 -0
- devtime/db/schema.sql +145 -0
- devtime/fixtures/__init__.py +0 -0
- devtime/fixtures/assertions.py +51 -0
- devtime/fixtures/loader.py +52 -0
- devtime/fixtures/runner.py +73 -0
- devtime/intelligence/__init__.py +0 -0
- devtime/intelligence/claims.py +235 -0
- devtime/intelligence/concepts.py +483 -0
- devtime/intelligence/context_pack.py +276 -0
- devtime/intelligence/evidence.py +127 -0
- devtime/intelligence/lineage.py +21 -0
- devtime/intelligence/risk.py +267 -0
- devtime/intelligence/scoring.py +99 -0
- devtime/mcp/__init__.py +0 -0
- devtime/mcp/schemas.py +39 -0
- devtime/mcp/server.py +35 -0
- devtime/mcp/tools.py +90 -0
- devtime/output/__init__.py +0 -0
- devtime/output/json_export.py +50 -0
- devtime/output/markdown.py +50 -0
- devtime/output/terminal.py +208 -0
- devtime/paths.py +40 -0
- devtime/privacy.py +96 -0
- devtime/scanner/__init__.py +0 -0
- devtime/scanner/extractors/__init__.py +0 -0
- devtime/scanner/extractors/base.py +83 -0
- devtime/scanner/extractors/config_files.py +41 -0
- devtime/scanner/extractors/docs.py +35 -0
- devtime/scanner/extractors/nextjs.py +82 -0
- devtime/scanner/extractors/python.py +81 -0
- devtime/scanner/extractors/tests.py +61 -0
- devtime/scanner/extractors/typescript.py +99 -0
- devtime/scanner/file_walker.py +96 -0
- devtime/scanner/ignore.py +96 -0
- devtime/scanner/language.py +36 -0
- devtime/scanner/signals.py +252 -0
- devtime_ei-0.1.0.dist-info/METADATA +289 -0
- devtime_ei-0.1.0.dist-info/RECORD +54 -0
- devtime_ei-0.1.0.dist-info/WHEEL +5 -0
- devtime_ei-0.1.0.dist-info/entry_points.txt +2 -0
- devtime_ei-0.1.0.dist-info/licenses/LICENSE +201 -0
- devtime_ei-0.1.0.dist-info/top_level.txt +1 -0
devtime/db/repository.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Persistence and read-back for repository memory (Builder Edition, Chapter 6).
|
|
2
|
+
|
|
3
|
+
Writes concepts, evidence, claims, uncertainties, and decisions during a scan,
|
|
4
|
+
and reconstructs ConceptIntelligence objects for explain/context/risk/scoring so
|
|
5
|
+
those commands read from durable memory rather than re-deriving truth on the fly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sqlite3
|
|
12
|
+
import uuid
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
|
|
15
|
+
from devtime.intelligence.claims import (
|
|
16
|
+
Claim,
|
|
17
|
+
ConceptIntelligence,
|
|
18
|
+
Uncertainty,
|
|
19
|
+
)
|
|
20
|
+
from devtime.intelligence.concepts import ConceptCandidate
|
|
21
|
+
from devtime.intelligence.evidence import EvidenceItem
|
|
22
|
+
from devtime.scanner.extractors.base import Signal
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _now() -> str:
|
|
26
|
+
return datetime.now(timezone.utc).isoformat()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _id(prefix: str) -> str:
|
|
30
|
+
return f"{prefix}-{uuid.uuid4().hex[:10]}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# --------------------------------------------------------------------------- #
|
|
34
|
+
# Write
|
|
35
|
+
# --------------------------------------------------------------------------- #
|
|
36
|
+
|
|
37
|
+
def add_repository_if_missing(conn: sqlite3.Connection, root) -> str:
|
|
38
|
+
"""Return the existing repository id, creating a row if none exists."""
|
|
39
|
+
row = conn.execute("SELECT id FROM repositories LIMIT 1").fetchone()
|
|
40
|
+
if row:
|
|
41
|
+
return row["id"]
|
|
42
|
+
repo_id = _id("repo")
|
|
43
|
+
root_path = str(root.resolve()) if hasattr(root, "resolve") else str(root)
|
|
44
|
+
conn.execute(
|
|
45
|
+
"INSERT INTO repositories(id, root_path, created_at, updated_at) VALUES (?,?,?,?)",
|
|
46
|
+
(repo_id, root_path, _now(), _now()),
|
|
47
|
+
)
|
|
48
|
+
conn.commit()
|
|
49
|
+
return repo_id
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def clear_derived_memory(conn: sqlite3.Connection) -> None:
|
|
53
|
+
"""Remove machine-derived memory before a re-scan. Human decisions are kept."""
|
|
54
|
+
for table in ("evidence", "claims", "uncertainties", "concepts", "risk_findings"):
|
|
55
|
+
conn.execute(f"DELETE FROM {table}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def save_intelligence(
|
|
59
|
+
conn: sqlite3.Connection,
|
|
60
|
+
repository_id: str,
|
|
61
|
+
items: list[ConceptIntelligence],
|
|
62
|
+
) -> None:
|
|
63
|
+
for ci in items:
|
|
64
|
+
concept_id = ci.concept.slug
|
|
65
|
+
conn.execute(
|
|
66
|
+
"INSERT OR REPLACE INTO concepts"
|
|
67
|
+
"(id, repository_id, name, slug, kind, summary, confidence, status, "
|
|
68
|
+
" created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
|
69
|
+
(
|
|
70
|
+
concept_id,
|
|
71
|
+
repository_id,
|
|
72
|
+
ci.concept.name,
|
|
73
|
+
ci.concept.slug,
|
|
74
|
+
ci.concept.kind,
|
|
75
|
+
f"{ci.concept.name} detected from repository evidence.",
|
|
76
|
+
ci.concept.confidence,
|
|
77
|
+
"supported",
|
|
78
|
+
_now(),
|
|
79
|
+
_now(),
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Evidence (stash original signal kind/confidence for faithful read-back).
|
|
84
|
+
ev_id_map: dict[int, str] = {}
|
|
85
|
+
for idx, e in enumerate(ci.evidence):
|
|
86
|
+
eid = _id("E")
|
|
87
|
+
ev_id_map[idx] = eid
|
|
88
|
+
conn.execute(
|
|
89
|
+
"INSERT INTO evidence"
|
|
90
|
+
"(id, concept_id, file_id, signal_id, kind, strength, summary, path, "
|
|
91
|
+
" start_line, end_line, metadata_json, created_at) "
|
|
92
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
93
|
+
(
|
|
94
|
+
eid,
|
|
95
|
+
concept_id,
|
|
96
|
+
None,
|
|
97
|
+
None,
|
|
98
|
+
e.kind,
|
|
99
|
+
e.strength,
|
|
100
|
+
e.summary,
|
|
101
|
+
e.path,
|
|
102
|
+
e.start_line,
|
|
103
|
+
e.end_line,
|
|
104
|
+
json.dumps(
|
|
105
|
+
{
|
|
106
|
+
"signal_kind": e.signal.kind,
|
|
107
|
+
"signal_confidence": e.signal.confidence,
|
|
108
|
+
"signal_name": e.signal.name,
|
|
109
|
+
"supports": e.supports_claim_types,
|
|
110
|
+
# Persist signal metadata (imports/e2e/purpose) so the
|
|
111
|
+
# Context Pack import reason survives a save/load round-trip.
|
|
112
|
+
"signal_metadata": e.signal.metadata,
|
|
113
|
+
}
|
|
114
|
+
),
|
|
115
|
+
_now(),
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
for c in ci.claims:
|
|
120
|
+
conn.execute(
|
|
121
|
+
"INSERT INTO claims"
|
|
122
|
+
"(id, concept_id, type, text, confidence, state, evidence_ids_json, "
|
|
123
|
+
" uncertainty_ids_json, requires_human_confirmation, created_by, "
|
|
124
|
+
" created_at, updated_at, last_verified_at) "
|
|
125
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
126
|
+
(
|
|
127
|
+
_id("C"),
|
|
128
|
+
concept_id,
|
|
129
|
+
c.type,
|
|
130
|
+
c.text,
|
|
131
|
+
c.confidence,
|
|
132
|
+
c.state,
|
|
133
|
+
json.dumps([e.path for e in c.evidence if e.path]),
|
|
134
|
+
"[]",
|
|
135
|
+
1 if c.requires_human_confirmation else 0,
|
|
136
|
+
c.created_by,
|
|
137
|
+
_now(),
|
|
138
|
+
_now(),
|
|
139
|
+
None,
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
for u in ci.uncertainties:
|
|
144
|
+
conn.execute(
|
|
145
|
+
"INSERT INTO uncertainties"
|
|
146
|
+
"(id, concept_id, type, text, action, severity, evidence_gap_json, created_at) "
|
|
147
|
+
"VALUES (?,?,?,?,?,?,?,?)",
|
|
148
|
+
(_id("U"), concept_id, u.type, u.text, u.action, u.severity, "{}", _now()),
|
|
149
|
+
)
|
|
150
|
+
conn.commit()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def add_decision(
|
|
154
|
+
conn: sqlite3.Connection,
|
|
155
|
+
title: str,
|
|
156
|
+
body: str,
|
|
157
|
+
concept_slug: str | None = None,
|
|
158
|
+
) -> str:
|
|
159
|
+
did = _id("D")
|
|
160
|
+
conn.execute(
|
|
161
|
+
"INSERT INTO decisions"
|
|
162
|
+
"(id, concept_id, title, body, source, status, evidence_ids_json, "
|
|
163
|
+
" created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)",
|
|
164
|
+
(did, concept_slug, title, body, "human", "active", "[]", _now(), _now()),
|
|
165
|
+
)
|
|
166
|
+
conn.commit()
|
|
167
|
+
return did
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# --------------------------------------------------------------------------- #
|
|
171
|
+
# Read / reconstruct
|
|
172
|
+
# --------------------------------------------------------------------------- #
|
|
173
|
+
|
|
174
|
+
# Behaviors a decision may claim that DevTime cannot take on faith - they must be
|
|
175
|
+
# corroborated by scanned implementation evidence (Trust Repair v0.0.6).
|
|
176
|
+
_DECISION_BEHAVIOR_TOKENS = {
|
|
177
|
+
"retry": ("retry", "retries", "retrying"),
|
|
178
|
+
"idempotency": ("idempoten",),
|
|
179
|
+
"deduplication": ("dedupe", "deduplicat", "duplicate delivery", "duplicate-delivery"),
|
|
180
|
+
"backoff": ("backoff", "back-off"),
|
|
181
|
+
"rate limiting": ("rate limit", "rate-limit", "ratelimit"),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _decision_corroboration(body: str, impl_text: str) -> tuple[bool, list[str]]:
|
|
186
|
+
"""Return (corroborated, uncorroborated_behaviors).
|
|
187
|
+
|
|
188
|
+
A generic decision that claims no specific verifiable behavior is treated as
|
|
189
|
+
corroborated. A decision that claims a behavior (retry, idempotency, ...) is
|
|
190
|
+
corroborated only if the scanned implementation evidence shows that behavior.
|
|
191
|
+
V0 corroboration is signal/evidence-text based and intentionally conservative.
|
|
192
|
+
"""
|
|
193
|
+
body_l = body.lower()
|
|
194
|
+
claimed = [
|
|
195
|
+
label for label, toks in _DECISION_BEHAVIOR_TOKENS.items()
|
|
196
|
+
if any(t in body_l for t in toks)
|
|
197
|
+
]
|
|
198
|
+
if not claimed:
|
|
199
|
+
return True, []
|
|
200
|
+
uncorroborated = [
|
|
201
|
+
label for label in claimed
|
|
202
|
+
if not any(t in impl_text for t in _DECISION_BEHAVIOR_TOKENS[label])
|
|
203
|
+
]
|
|
204
|
+
return (len(uncorroborated) == 0), uncorroborated
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _row_to_evidence(row: sqlite3.Row) -> EvidenceItem:
|
|
208
|
+
meta = json.loads(row["metadata_json"] or "{}")
|
|
209
|
+
signal = Signal(
|
|
210
|
+
kind=meta.get("signal_kind", row["kind"]),
|
|
211
|
+
name=meta.get("signal_name"),
|
|
212
|
+
file_rel_path=row["path"] or "",
|
|
213
|
+
confidence=meta.get("signal_confidence", 0.5),
|
|
214
|
+
metadata=meta.get("signal_metadata", {}) or {},
|
|
215
|
+
)
|
|
216
|
+
return EvidenceItem(
|
|
217
|
+
concept_slug=row["concept_id"],
|
|
218
|
+
kind=row["kind"],
|
|
219
|
+
strength=row["strength"],
|
|
220
|
+
summary=row["summary"],
|
|
221
|
+
path=row["path"],
|
|
222
|
+
start_line=row["start_line"],
|
|
223
|
+
end_line=row["end_line"],
|
|
224
|
+
signal=signal,
|
|
225
|
+
supports_claim_types=meta.get("supports", []),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def load_concept(conn: sqlite3.Connection, slug_or_name: str) -> ConceptIntelligence | None:
|
|
230
|
+
row = conn.execute(
|
|
231
|
+
"SELECT * FROM concepts WHERE slug = ? OR lower(name) = lower(?)",
|
|
232
|
+
(slug_or_name, slug_or_name),
|
|
233
|
+
).fetchone()
|
|
234
|
+
if row is None:
|
|
235
|
+
return None
|
|
236
|
+
return _load_for_concept_row(conn, row)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _load_for_concept_row(conn: sqlite3.Connection, row: sqlite3.Row) -> ConceptIntelligence:
|
|
240
|
+
concept_id = row["id"]
|
|
241
|
+
evidence = [
|
|
242
|
+
_row_to_evidence(r)
|
|
243
|
+
for r in conn.execute(
|
|
244
|
+
"SELECT * FROM evidence WHERE concept_id = ?", (concept_id,)
|
|
245
|
+
).fetchall()
|
|
246
|
+
]
|
|
247
|
+
candidate = ConceptCandidate(
|
|
248
|
+
slug=row["slug"],
|
|
249
|
+
name=row["name"],
|
|
250
|
+
kind=row["kind"],
|
|
251
|
+
confidence=row["confidence"],
|
|
252
|
+
signals=[e.signal for e in evidence],
|
|
253
|
+
)
|
|
254
|
+
# Attach human decisions - but only CORROBORATED decisions count as evidence
|
|
255
|
+
# (Trust Repair v0.0.6). A decision that describes behavior the scanned code does
|
|
256
|
+
# not show must not clear uncertainty or improve the score.
|
|
257
|
+
impl_text = " ".join(
|
|
258
|
+
" ".join(
|
|
259
|
+
str(x) for x in (e.summary, e.signal.name, e.signal.kind, e.path) if x
|
|
260
|
+
).lower()
|
|
261
|
+
for e in evidence # only scanned evidence so far (decisions not yet added)
|
|
262
|
+
)
|
|
263
|
+
decision_rows = conn.execute(
|
|
264
|
+
"SELECT * FROM decisions WHERE concept_id = ? AND status = 'active'",
|
|
265
|
+
(concept_id,),
|
|
266
|
+
).fetchall()
|
|
267
|
+
any_decision_exists = len(decision_rows) > 0
|
|
268
|
+
uncorroborated_notes: list[tuple[str, list[str]]] = []
|
|
269
|
+
for d in decision_rows:
|
|
270
|
+
corroborated, missing = _decision_corroboration(d["body"] or "", impl_text)
|
|
271
|
+
if corroborated:
|
|
272
|
+
evidence.append(
|
|
273
|
+
EvidenceItem(
|
|
274
|
+
concept_slug=row["slug"],
|
|
275
|
+
kind="decision",
|
|
276
|
+
strength="strong",
|
|
277
|
+
summary=f"Decision: {d['title']} (corroborated by implementation evidence)",
|
|
278
|
+
path=None,
|
|
279
|
+
start_line=None,
|
|
280
|
+
end_line=None,
|
|
281
|
+
signal=Signal(kind="decision", name=d["title"], file_rel_path="(decision)"),
|
|
282
|
+
supports_claim_types=["decision"],
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
else:
|
|
286
|
+
uncorroborated_notes.append((d["title"], missing))
|
|
287
|
+
|
|
288
|
+
claims = [
|
|
289
|
+
Claim(
|
|
290
|
+
type=r["type"],
|
|
291
|
+
text=r["text"],
|
|
292
|
+
confidence=r["confidence"],
|
|
293
|
+
state=r["state"],
|
|
294
|
+
evidence=[e for e in evidence if e.path in set(json.loads(r["evidence_ids_json"] or "[]"))],
|
|
295
|
+
requires_human_confirmation=bool(r["requires_human_confirmation"]),
|
|
296
|
+
created_by=r["created_by"],
|
|
297
|
+
)
|
|
298
|
+
for r in conn.execute(
|
|
299
|
+
"SELECT * FROM claims WHERE concept_id = ?", (concept_id,)
|
|
300
|
+
).fetchall()
|
|
301
|
+
]
|
|
302
|
+
has_corroborated_decision = any(e.kind == "decision" for e in evidence)
|
|
303
|
+
uncertainties = [
|
|
304
|
+
Uncertainty(
|
|
305
|
+
type=r["type"], text=r["text"], action=r["action"], severity=r["severity"]
|
|
306
|
+
)
|
|
307
|
+
for r in conn.execute(
|
|
308
|
+
"SELECT * FROM uncertainties WHERE concept_id = ?", (concept_id,)
|
|
309
|
+
).fetchall()
|
|
310
|
+
# A decision (corroborated or not) means a decision *was* found, so the
|
|
311
|
+
# "no decision was found" uncertainty no longer applies verbatim.
|
|
312
|
+
if not (any_decision_exists and r["type"] == "missing_decision")
|
|
313
|
+
]
|
|
314
|
+
# Uncorroborated decisions preserve uncertainty with an explicit corroboration note.
|
|
315
|
+
for title, missing in uncorroborated_notes:
|
|
316
|
+
behaviors = ", ".join(missing) if missing else "the described behavior"
|
|
317
|
+
uncertainties.append(
|
|
318
|
+
Uncertainty(
|
|
319
|
+
type="decision_not_corroborated",
|
|
320
|
+
text=(
|
|
321
|
+
f"Decision '{title}' exists, but {behaviors} is not corroborated "
|
|
322
|
+
f"by scanned implementation evidence."
|
|
323
|
+
),
|
|
324
|
+
action="Confirm the decision matches the implementation, or update one of them.",
|
|
325
|
+
severity="medium",
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
# Only a corroborated decision may remove the uncertainty-typed claim.
|
|
329
|
+
if has_corroborated_decision:
|
|
330
|
+
claims = [
|
|
331
|
+
c for c in claims
|
|
332
|
+
if not (c.type == "uncertainty" and "decision" in c.text.lower())
|
|
333
|
+
]
|
|
334
|
+
return ConceptIntelligence(
|
|
335
|
+
concept=candidate, evidence=evidence, claims=claims, uncertainties=uncertainties
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def load_all_concepts(conn: sqlite3.Connection) -> list[ConceptIntelligence]:
|
|
340
|
+
rows = conn.execute(
|
|
341
|
+
"SELECT * FROM concepts ORDER BY confidence DESC"
|
|
342
|
+
).fetchall()
|
|
343
|
+
return [_load_for_concept_row(conn, r) for r in rows]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def last_scan_time(conn: sqlite3.Connection) -> str | None:
|
|
347
|
+
row = conn.execute(
|
|
348
|
+
"SELECT finished_at FROM scans WHERE status='completed' "
|
|
349
|
+
"ORDER BY finished_at DESC LIMIT 1"
|
|
350
|
+
).fetchone()
|
|
351
|
+
return row["finished_at"] if row and row["finished_at"] else None
|
devtime/db/schema.sql
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
-- DevTime V0 SQLite schema (Builder Edition, Chapter 6 + Appendix C).
|
|
2
|
+
-- The database is the local memory layer.
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
5
|
+
version INTEGER PRIMARY KEY,
|
|
6
|
+
applied_at TEXT NOT NULL
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
CREATE TABLE IF NOT EXISTS repositories (
|
|
10
|
+
id TEXT PRIMARY KEY,
|
|
11
|
+
root_path TEXT NOT NULL,
|
|
12
|
+
created_at TEXT NOT NULL,
|
|
13
|
+
updated_at TEXT NOT NULL
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE TABLE IF NOT EXISTS scans (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
repository_id TEXT NOT NULL,
|
|
19
|
+
started_at TEXT NOT NULL,
|
|
20
|
+
finished_at TEXT,
|
|
21
|
+
status TEXT NOT NULL,
|
|
22
|
+
devtime_version TEXT NOT NULL,
|
|
23
|
+
file_count INTEGER DEFAULT 0,
|
|
24
|
+
signal_count INTEGER DEFAULT 0,
|
|
25
|
+
FOREIGN KEY(repository_id) REFERENCES repositories(id)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
repository_id TEXT NOT NULL,
|
|
31
|
+
path TEXT NOT NULL,
|
|
32
|
+
extension TEXT,
|
|
33
|
+
language TEXT,
|
|
34
|
+
sha256 TEXT,
|
|
35
|
+
size_bytes INTEGER,
|
|
36
|
+
is_test INTEGER DEFAULT 0,
|
|
37
|
+
is_doc INTEGER DEFAULT 0,
|
|
38
|
+
ignored INTEGER DEFAULT 0,
|
|
39
|
+
last_seen_scan_id TEXT,
|
|
40
|
+
UNIQUE(repository_id, path)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS signals (
|
|
44
|
+
id TEXT PRIMARY KEY,
|
|
45
|
+
scan_id TEXT NOT NULL,
|
|
46
|
+
file_id TEXT NOT NULL,
|
|
47
|
+
kind TEXT NOT NULL,
|
|
48
|
+
name TEXT,
|
|
49
|
+
value TEXT,
|
|
50
|
+
start_line INTEGER,
|
|
51
|
+
end_line INTEGER,
|
|
52
|
+
confidence REAL DEFAULT 0.5,
|
|
53
|
+
metadata_json TEXT DEFAULT '{}'
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS concepts (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
repository_id TEXT NOT NULL,
|
|
59
|
+
name TEXT NOT NULL,
|
|
60
|
+
slug TEXT NOT NULL,
|
|
61
|
+
kind TEXT NOT NULL,
|
|
62
|
+
summary TEXT,
|
|
63
|
+
confidence REAL NOT NULL DEFAULT 0.0,
|
|
64
|
+
status TEXT NOT NULL DEFAULT 'proposed',
|
|
65
|
+
created_at TEXT NOT NULL,
|
|
66
|
+
updated_at TEXT NOT NULL,
|
|
67
|
+
UNIQUE(repository_id, slug)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS evidence (
|
|
71
|
+
id TEXT PRIMARY KEY,
|
|
72
|
+
concept_id TEXT NOT NULL,
|
|
73
|
+
file_id TEXT,
|
|
74
|
+
signal_id TEXT,
|
|
75
|
+
kind TEXT NOT NULL,
|
|
76
|
+
strength TEXT NOT NULL,
|
|
77
|
+
summary TEXT NOT NULL,
|
|
78
|
+
path TEXT,
|
|
79
|
+
start_line INTEGER,
|
|
80
|
+
end_line INTEGER,
|
|
81
|
+
metadata_json TEXT DEFAULT '{}',
|
|
82
|
+
created_at TEXT NOT NULL,
|
|
83
|
+
FOREIGN KEY(concept_id) REFERENCES concepts(id)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE TABLE IF NOT EXISTS claims (
|
|
87
|
+
id TEXT PRIMARY KEY,
|
|
88
|
+
concept_id TEXT NOT NULL,
|
|
89
|
+
type TEXT NOT NULL,
|
|
90
|
+
text TEXT NOT NULL,
|
|
91
|
+
confidence REAL NOT NULL,
|
|
92
|
+
state TEXT NOT NULL DEFAULT 'supported',
|
|
93
|
+
evidence_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
94
|
+
uncertainty_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
95
|
+
requires_human_confirmation INTEGER DEFAULT 0,
|
|
96
|
+
created_by TEXT NOT NULL DEFAULT 'machine',
|
|
97
|
+
created_at TEXT NOT NULL,
|
|
98
|
+
updated_at TEXT NOT NULL,
|
|
99
|
+
last_verified_at TEXT
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE TABLE IF NOT EXISTS decisions (
|
|
103
|
+
id TEXT PRIMARY KEY,
|
|
104
|
+
concept_id TEXT,
|
|
105
|
+
title TEXT NOT NULL,
|
|
106
|
+
body TEXT NOT NULL,
|
|
107
|
+
source TEXT NOT NULL DEFAULT 'human',
|
|
108
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
109
|
+
evidence_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
110
|
+
created_at TEXT NOT NULL,
|
|
111
|
+
updated_at TEXT NOT NULL
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
CREATE TABLE IF NOT EXISTS uncertainties (
|
|
115
|
+
id TEXT PRIMARY KEY,
|
|
116
|
+
concept_id TEXT NOT NULL,
|
|
117
|
+
type TEXT NOT NULL,
|
|
118
|
+
text TEXT NOT NULL,
|
|
119
|
+
action TEXT,
|
|
120
|
+
severity TEXT DEFAULT 'medium',
|
|
121
|
+
evidence_gap_json TEXT DEFAULT '{}',
|
|
122
|
+
created_at TEXT NOT NULL
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
CREATE TABLE IF NOT EXISTS risk_findings (
|
|
126
|
+
id TEXT PRIMARY KEY,
|
|
127
|
+
concept_id TEXT NOT NULL,
|
|
128
|
+
severity TEXT NOT NULL,
|
|
129
|
+
type TEXT NOT NULL,
|
|
130
|
+
text TEXT NOT NULL,
|
|
131
|
+
evidence_ids_json TEXT DEFAULT '[]',
|
|
132
|
+
changed_files_json TEXT DEFAULT '[]',
|
|
133
|
+
suggested_action TEXT,
|
|
134
|
+
human_review_required INTEGER DEFAULT 0,
|
|
135
|
+
created_at TEXT NOT NULL
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
CREATE TABLE IF NOT EXISTS context_packs (
|
|
139
|
+
id TEXT PRIMARY KEY,
|
|
140
|
+
concept_id TEXT NOT NULL,
|
|
141
|
+
mode TEXT NOT NULL,
|
|
142
|
+
body_markdown TEXT NOT NULL,
|
|
143
|
+
metadata_json TEXT DEFAULT '{}',
|
|
144
|
+
created_at TEXT NOT NULL
|
|
145
|
+
);
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Fixture assertions (Builder Edition, Chapter 17).
|
|
2
|
+
|
|
3
|
+
Allowed claims must appear, forbidden claims must not, required uncertainty must
|
|
4
|
+
be present, and expected concepts must be detected.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from devtime.intelligence.claims import ConceptIntelligence
|
|
10
|
+
|
|
11
|
+
# Loose substring matching so fixtures stay readable while detection wording evolves.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _all_claim_texts(intelligence: list[ConceptIntelligence]) -> list[str]:
|
|
15
|
+
texts: list[str] = []
|
|
16
|
+
for ci in intelligence:
|
|
17
|
+
texts += [c.text for c in ci.claims]
|
|
18
|
+
return texts
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _all_uncertainty_texts(intelligence: list[ConceptIntelligence]) -> list[str]:
|
|
22
|
+
texts: list[str] = []
|
|
23
|
+
for ci in intelligence:
|
|
24
|
+
texts += [u.text for u in ci.uncertainties]
|
|
25
|
+
texts += [c.text for c in ci.claims if c.type == "uncertainty"]
|
|
26
|
+
return texts
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _matches(needle: str, haystack: list[str]) -> bool:
|
|
30
|
+
key = needle.lower().strip()
|
|
31
|
+
return any(key in h.lower() for h in haystack)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def assert_expected_concepts(intelligence, expected: list[str]) -> list[str]:
|
|
35
|
+
names = {ci.concept.name.lower() for ci in intelligence}
|
|
36
|
+
return [c for c in expected if c.lower() not in names]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def assert_allowed_claims(intelligence, allowed: list[str]) -> list[str]:
|
|
40
|
+
texts = _all_claim_texts(intelligence)
|
|
41
|
+
return [c for c in allowed if not _matches(c, texts)]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def assert_forbidden_claims_absent(intelligence, forbidden: list[str]) -> list[str]:
|
|
45
|
+
texts = _all_claim_texts(intelligence)
|
|
46
|
+
return [c for c in forbidden if _matches(c, texts)]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def assert_required_uncertainty(intelligence, required: list[str]) -> list[str]:
|
|
50
|
+
texts = _all_uncertainty_texts(intelligence)
|
|
51
|
+
return [u for u in required if not _matches(u, texts)]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Fixture loading (Builder Edition, Chapter 17 / Appendix D)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class FixtureExpectation:
|
|
13
|
+
concepts: list[str] = field(default_factory=list)
|
|
14
|
+
allowed_claims: list[str] = field(default_factory=list)
|
|
15
|
+
forbidden_claims: list[str] = field(default_factory=list)
|
|
16
|
+
required_uncertainty: list[str] = field(default_factory=list)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Fixture:
|
|
21
|
+
id: str
|
|
22
|
+
type: str
|
|
23
|
+
repo_path: Path
|
|
24
|
+
expected: FixtureExpectation
|
|
25
|
+
contains_secrets: bool = False
|
|
26
|
+
language: str | None = None
|
|
27
|
+
framework: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_fixture(directory: Path) -> Fixture:
|
|
31
|
+
spec_path = directory / "fixture.yaml"
|
|
32
|
+
data = yaml.safe_load(spec_path.read_text(encoding="utf-8")) or {}
|
|
33
|
+
expected = data.get("expected", {}) or {}
|
|
34
|
+
privacy = data.get("privacy", {}) or {}
|
|
35
|
+
return Fixture(
|
|
36
|
+
id=data["id"],
|
|
37
|
+
type=data.get("type", "claim"),
|
|
38
|
+
repo_path=directory / "repo",
|
|
39
|
+
language=data.get("language"),
|
|
40
|
+
framework=data.get("framework"),
|
|
41
|
+
contains_secrets=bool(privacy.get("contains_secrets", False)),
|
|
42
|
+
expected=FixtureExpectation(
|
|
43
|
+
concepts=expected.get("concepts", []) or [],
|
|
44
|
+
allowed_claims=expected.get("allowed_claims", []) or [],
|
|
45
|
+
forbidden_claims=expected.get("forbidden_claims", []) or [],
|
|
46
|
+
required_uncertainty=expected.get("required_uncertainty", []) or [],
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def discover_fixtures(root: Path) -> list[Path]:
|
|
52
|
+
return sorted(p.parent for p in root.rglob("fixture.yaml"))
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Fixture runner (Builder Edition, Chapter 17).
|
|
2
|
+
|
|
3
|
+
Scans a fixture repo in an isolated temp copy (so fixtures are never mutated),
|
|
4
|
+
then asserts allowed claims, forbidden claims, required uncertainty, and concepts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import shutil
|
|
10
|
+
import tempfile
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from devtime.fixtures import assertions
|
|
15
|
+
from devtime.fixtures.loader import Fixture, load_fixture
|
|
16
|
+
from devtime.scanner.signals import run_scan
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class FixtureResult:
|
|
21
|
+
fixture_id: str
|
|
22
|
+
passed: bool
|
|
23
|
+
missing_concepts: list[str] = field(default_factory=list)
|
|
24
|
+
missing_allowed: list[str] = field(default_factory=list)
|
|
25
|
+
present_forbidden: list[str] = field(default_factory=list)
|
|
26
|
+
missing_uncertainty: list[str] = field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
def failures(self) -> list[str]:
|
|
29
|
+
out: list[str] = []
|
|
30
|
+
for label, items in (
|
|
31
|
+
("missing concept", self.missing_concepts),
|
|
32
|
+
("missing allowed claim", self.missing_allowed),
|
|
33
|
+
("forbidden claim present", self.present_forbidden),
|
|
34
|
+
("missing required uncertainty", self.missing_uncertainty),
|
|
35
|
+
):
|
|
36
|
+
out += [f"{label}: {i}" for i in items]
|
|
37
|
+
return out
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def run_devtime_scan(repo: Path):
|
|
41
|
+
"""Scan a repo in an isolated temp copy and return its intelligence."""
|
|
42
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
43
|
+
dest = Path(tmp) / "repo"
|
|
44
|
+
shutil.copytree(repo, dest)
|
|
45
|
+
result = run_scan(root=dest)
|
|
46
|
+
return result.intelligence
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run_fixture(directory: Path) -> FixtureResult:
|
|
50
|
+
fixture: Fixture = load_fixture(directory)
|
|
51
|
+
intelligence = run_devtime_scan(fixture.repo_path)
|
|
52
|
+
exp = fixture.expected
|
|
53
|
+
|
|
54
|
+
missing_concepts = assertions.assert_expected_concepts(intelligence, exp.concepts)
|
|
55
|
+
missing_allowed = assertions.assert_allowed_claims(intelligence, exp.allowed_claims)
|
|
56
|
+
present_forbidden = assertions.assert_forbidden_claims_absent(
|
|
57
|
+
intelligence, exp.forbidden_claims
|
|
58
|
+
)
|
|
59
|
+
missing_uncertainty = assertions.assert_required_uncertainty(
|
|
60
|
+
intelligence, exp.required_uncertainty
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
passed = not (
|
|
64
|
+
missing_concepts or missing_allowed or present_forbidden or missing_uncertainty
|
|
65
|
+
)
|
|
66
|
+
return FixtureResult(
|
|
67
|
+
fixture_id=fixture.id,
|
|
68
|
+
passed=passed,
|
|
69
|
+
missing_concepts=missing_concepts,
|
|
70
|
+
missing_allowed=missing_allowed,
|
|
71
|
+
present_forbidden=present_forbidden,
|
|
72
|
+
missing_uncertainty=missing_uncertainty,
|
|
73
|
+
)
|
|
File without changes
|