sourcecode 2.3.0__py3-none-any.whl → 2.4.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.
- sourcecode/__init__.py +1 -1
- sourcecode/cli.py +62 -1
- sourcecode/context_cache.py +637 -0
- sourcecode/explain.py +24 -0
- {sourcecode-2.3.0.dist-info → sourcecode-2.4.0.dist-info}/METADATA +1 -1
- {sourcecode-2.3.0.dist-info → sourcecode-2.4.0.dist-info}/RECORD +9 -8
- {sourcecode-2.3.0.dist-info → sourcecode-2.4.0.dist-info}/WHEEL +0 -0
- {sourcecode-2.3.0.dist-info → sourcecode-2.4.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.3.0.dist-info → sourcecode-2.4.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -5341,6 +5341,7 @@ def explain_cmd(
|
|
|
5341
5341
|
from sourcecode.context_graph import ContextGraph
|
|
5342
5342
|
from sourcecode.spring_model import SpringSemanticModel
|
|
5343
5343
|
from sourcecode.explain import explain_class
|
|
5344
|
+
from sourcecode import context_cache as _ctxcache
|
|
5344
5345
|
|
|
5345
5346
|
if not class_name.strip():
|
|
5346
5347
|
_emit_error_json(
|
|
@@ -5383,14 +5384,27 @@ def explain_cmd(
|
|
|
5383
5384
|
)
|
|
5384
5385
|
raise typer.Exit(code=1)
|
|
5385
5386
|
|
|
5387
|
+
# AI Context Cache — operates at the *knowledge* level: it caches the
|
|
5388
|
+
# reusable Canonical IR (the expensive Java parse), keyed only by knowledge
|
|
5389
|
+
# state, not by command. explain then derives its answer cheaply from that
|
|
5390
|
+
# shared CIR. The same cached CIR is reused for free by any other command.
|
|
5391
|
+
# Provider-agnostic and best-effort: any fault degrades to a fresh build.
|
|
5386
5392
|
_prog = Progress()
|
|
5387
5393
|
_prog.start(f"explaining {class_name} ({len(file_list)} files)")
|
|
5394
|
+
_cc_look = None
|
|
5388
5395
|
try:
|
|
5389
|
-
|
|
5396
|
+
try:
|
|
5397
|
+
cir, _cc_look = _ctxcache.get_or_build_cir(
|
|
5398
|
+
_resolve_repo_root(target), target, file_list
|
|
5399
|
+
)
|
|
5400
|
+
except Exception:
|
|
5401
|
+
cir = ContextGraph.build(file_list, target).cir # fallback: never break explain
|
|
5390
5402
|
model = SpringSemanticModel.build(cir)
|
|
5391
5403
|
explanation = explain_class(class_name, cir, model)
|
|
5392
5404
|
finally:
|
|
5393
5405
|
_prog.finish()
|
|
5406
|
+
if _cc_look is not None:
|
|
5407
|
+
typer.echo(_cc_look.render(), err=True)
|
|
5394
5408
|
|
|
5395
5409
|
if format == "json":
|
|
5396
5410
|
output = _json.dumps(explanation.to_dict(), indent=2, ensure_ascii=False)
|
|
@@ -7199,6 +7213,53 @@ def cache_freshness_cmd(
|
|
|
7199
7213
|
typer.echo(f"RIS updated: {result.get('ris_last_updated_at') or 'never'}")
|
|
7200
7214
|
|
|
7201
7215
|
|
|
7216
|
+
@cache_app.command("context-stats")
|
|
7217
|
+
def cache_context_stats_cmd(
|
|
7218
|
+
path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
|
|
7219
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
7220
|
+
) -> None:
|
|
7221
|
+
"""Show AI Context Cache statistics (structured-knowledge reuse metrics).
|
|
7222
|
+
|
|
7223
|
+
This is the provider-agnostic context cache — distinct from the snapshot
|
|
7224
|
+
cache reported by `ask cache status`. It measures reuse of ASK's structured
|
|
7225
|
+
context (entities/evidence/observations/summaries), never prompts or LLM output.
|
|
7226
|
+
"""
|
|
7227
|
+
from sourcecode import context_cache as _ctxcache
|
|
7228
|
+
target = _resolve_repo_root(Path(path))
|
|
7229
|
+
stats = _ctxcache.stats_for_repo(target)
|
|
7230
|
+
if json_output:
|
|
7231
|
+
import json as _j
|
|
7232
|
+
typer.echo(_j.dumps(stats, indent=2, ensure_ascii=False))
|
|
7233
|
+
else:
|
|
7234
|
+
typer.echo(f"Cache dir: {stats['cache_dir']}")
|
|
7235
|
+
typer.echo(f"Enabled: {stats['enabled']}")
|
|
7236
|
+
typer.echo(f"Contexts: {stats['contexts']}")
|
|
7237
|
+
typer.echo(f"Hits: {stats['hits']}")
|
|
7238
|
+
typer.echo(f"Misses: {stats['misses']}")
|
|
7239
|
+
typer.echo(f"Invalidations: {stats['invalidations']}")
|
|
7240
|
+
typer.echo(f"Stores: {stats['stores']}")
|
|
7241
|
+
typer.echo(f"Hit ratio: {stats['hit_ratio']}")
|
|
7242
|
+
typer.echo(f"Avg lookup: {stats['avg_lookup_ms']} ms")
|
|
7243
|
+
typer.echo(f"Avg build: {stats['avg_build_ms']} ms")
|
|
7244
|
+
typer.echo(f"Bytes stored: {stats['bytes_stored']}")
|
|
7245
|
+
typer.echo(f"KL version: {stats['kl_schema_version']} producer={stats['producer_version']}")
|
|
7246
|
+
|
|
7247
|
+
|
|
7248
|
+
@cache_app.command("context-clear")
|
|
7249
|
+
def cache_context_clear_cmd(
|
|
7250
|
+
path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
|
|
7251
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
7252
|
+
) -> None:
|
|
7253
|
+
"""Delete the AI Context Cache for a repository (structured context + stats)."""
|
|
7254
|
+
from sourcecode import context_cache as _ctxcache
|
|
7255
|
+
target = _resolve_repo_root(Path(path))
|
|
7256
|
+
if not yes and sys.stdin.isatty():
|
|
7257
|
+
import click as _click
|
|
7258
|
+
_click.confirm(f"Delete AI context cache for {target}?", abort=True, err=True)
|
|
7259
|
+
removed = _ctxcache.clear_for_repo(target)
|
|
7260
|
+
typer.echo(f"Removed {removed} context file(s).", err=True)
|
|
7261
|
+
|
|
7262
|
+
|
|
7202
7263
|
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
7203
7264
|
|
|
7204
7265
|
def _stderr_is_interactive() -> bool:
|
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Context Cache — structured-knowledge cache for ASK.
|
|
3
|
+
|
|
4
|
+
This is **not** a prompt cache. It never stores prompts, generated text, or LLM
|
|
5
|
+
responses. It stores the *structured context* that ASK's Knowledge Layer
|
|
6
|
+
produces — entities, evidence, observations, summaries and graph metrics — so
|
|
7
|
+
that expensive commands (``explain``, ``impact``, ``onboard``, ``prepare-context``,
|
|
8
|
+
``archetype`` …) can reuse the same derived knowledge across runs.
|
|
9
|
+
|
|
10
|
+
The component is completely independent of any AI provider. The cached objects
|
|
11
|
+
are consumed identically by GPT, Claude, Gemini, a local model, or any future
|
|
12
|
+
tool. The LLM never participates in producing or reading this cache.
|
|
13
|
+
|
|
14
|
+
Design
|
|
15
|
+
------
|
|
16
|
+
Context Request → Cache Key → Lookup → Hit / Miss → Store → Return Context
|
|
17
|
+
|
|
18
|
+
Cache key
|
|
19
|
+
---------
|
|
20
|
+
The key depends **only on the state of the knowledge**, never on any prompt text::
|
|
21
|
+
|
|
22
|
+
repo_id + worktree_signature + KL_SCHEMA_VERSION + PRODUCER_VERSION
|
|
23
|
+
+ command + target + relevant_options
|
|
24
|
+
|
|
25
|
+
``worktree_signature`` is the committed HEAD SHA, folded with a hash of the
|
|
26
|
+
working-tree diff when the tree is dirty — so an uncommitted edit deterministically
|
|
27
|
+
invalidates the entry instead of serving stale context. If *any* component
|
|
28
|
+
changes, the key changes and the old context is no longer served.
|
|
29
|
+
|
|
30
|
+
Invalidation
|
|
31
|
+
------------
|
|
32
|
+
Deterministic and state-based, never TTL-based for correctness:
|
|
33
|
+
* new commit → different ``worktree_signature`` → miss
|
|
34
|
+
* working-tree edit → different ``worktree_signature`` → miss
|
|
35
|
+
* Knowledge Layer bump → different ``KL_SCHEMA_VERSION`` → miss
|
|
36
|
+
* producer bump → different ``PRODUCER_VERSION`` → miss
|
|
37
|
+
* relevant option flip → different ``relevant_options`` → miss
|
|
38
|
+
Stored entries also carry their own ``kl_version`` / ``signature`` and are
|
|
39
|
+
re-validated on read as a defensive second check. TTL and a max-entry cap exist
|
|
40
|
+
only as *cleanup*, never as a consistency guarantee.
|
|
41
|
+
|
|
42
|
+
Persistence
|
|
43
|
+
-----------
|
|
44
|
+
<base>/context-cache/<repo_id>/
|
|
45
|
+
ctx-<key16>.json.gz ← one gzip'd CachedContext envelope per key
|
|
46
|
+
stats.json ← advisory counters (hits/misses/…)
|
|
47
|
+
|
|
48
|
+
Env vars
|
|
49
|
+
--------
|
|
50
|
+
SOURCECODE_CONTEXT_CACHE_DIR Override base dir (default: ~/.sourcecode)
|
|
51
|
+
SOURCECODE_CONTEXT_CACHE_DISABLE "1" to disable (always miss, never store)
|
|
52
|
+
SOURCECODE_CONTEXT_CACHE_TTL_DAYS Cleanup age (default 30; 0 = never)
|
|
53
|
+
SOURCECODE_CONTEXT_CACHE_MAX_ENTRIES Max entries per repo (default 256; 0 = unlimited)
|
|
54
|
+
"""
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
import gzip
|
|
58
|
+
import hashlib
|
|
59
|
+
import json
|
|
60
|
+
import os
|
|
61
|
+
import subprocess
|
|
62
|
+
import time
|
|
63
|
+
import uuid
|
|
64
|
+
from dataclasses import dataclass, field
|
|
65
|
+
from datetime import datetime, timezone
|
|
66
|
+
from pathlib import Path
|
|
67
|
+
from typing import Any, Optional
|
|
68
|
+
|
|
69
|
+
from sourcecode import __version__ as _PKG_VERSION
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Versioning — the stable half of the cache key
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
#: Bump when the *shape or meaning* of the Knowledge Layer / context objects
|
|
76
|
+
#: changes in a way that makes older cached context invalid. Independent of the
|
|
77
|
+
#: package version — a patch release must NOT bump this unless the context
|
|
78
|
+
#: format actually changed.
|
|
79
|
+
KL_SCHEMA_VERSION: str = "1"
|
|
80
|
+
|
|
81
|
+
#: Envelope schema version. Bump to invalidate *every* stored context file.
|
|
82
|
+
ENVELOPE_VERSION: str = "1"
|
|
83
|
+
|
|
84
|
+
#: Producer identity — the exact code that built the context. Package version
|
|
85
|
+
#: bumps naturally participate in the key so a new release never reads context
|
|
86
|
+
#: built by an older one.
|
|
87
|
+
PRODUCER_VERSION: str = _PKG_VERSION
|
|
88
|
+
|
|
89
|
+
_DEFAULT_TTL_DAYS: int = 30
|
|
90
|
+
_DEFAULT_MAX_ENTRIES: int = 256
|
|
91
|
+
|
|
92
|
+
_UNIT_SEP = "\x1f" # key-material field separator (never appears in inputs)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Cached object
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class CachedContext:
|
|
101
|
+
"""One unit of structured, inspectable context produced by ASK.
|
|
102
|
+
|
|
103
|
+
Deliberately *not* an opaque blob: every field is a plain JSON-serialisable
|
|
104
|
+
structure so the cache can be inspected, diffed and reasoned about. No
|
|
105
|
+
prompts, no generated text, no LLM output ever lands here.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
109
|
+
entities: list[Any] = field(default_factory=list)
|
|
110
|
+
evidence: list[Any] = field(default_factory=list)
|
|
111
|
+
observations: list[Any] = field(default_factory=list)
|
|
112
|
+
summaries: dict[str, Any] = field(default_factory=dict)
|
|
113
|
+
graph_metrics: dict[str, Any] = field(default_factory=dict)
|
|
114
|
+
#: The reusable, command-agnostic knowledge artifact itself (e.g. the raw IR
|
|
115
|
+
#: dict from which a full CIR is rebuilt cheaply). This is the expensive thing
|
|
116
|
+
#: worth caching; the fields above are inspectable metadata over it.
|
|
117
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
118
|
+
timestamps: dict[str, Any] = field(default_factory=dict)
|
|
119
|
+
producer_version: str = PRODUCER_VERSION
|
|
120
|
+
|
|
121
|
+
def to_dict(self) -> dict[str, Any]:
|
|
122
|
+
return {
|
|
123
|
+
"metadata": self.metadata,
|
|
124
|
+
"entities": self.entities,
|
|
125
|
+
"evidence": self.evidence,
|
|
126
|
+
"observations": self.observations,
|
|
127
|
+
"summaries": self.summaries,
|
|
128
|
+
"graph_metrics": self.graph_metrics,
|
|
129
|
+
"payload": self.payload,
|
|
130
|
+
"timestamps": self.timestamps,
|
|
131
|
+
"producer_version": self.producer_version,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_dict(cls, d: dict[str, Any]) -> "CachedContext":
|
|
136
|
+
return cls(
|
|
137
|
+
metadata=d.get("metadata", {}) or {},
|
|
138
|
+
entities=d.get("entities", []) or [],
|
|
139
|
+
evidence=d.get("evidence", []) or [],
|
|
140
|
+
observations=d.get("observations", []) or [],
|
|
141
|
+
summaries=d.get("summaries", {}) or {},
|
|
142
|
+
graph_metrics=d.get("graph_metrics", {}) or {},
|
|
143
|
+
payload=d.get("payload", {}) or {},
|
|
144
|
+
timestamps=d.get("timestamps", {}) or {},
|
|
145
|
+
producer_version=d.get("producer_version", "") or "",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# Location / identity helpers
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def _now_iso() -> str:
|
|
154
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _repo_id(repo_root: Path) -> str:
|
|
158
|
+
"""Stable 16-char id from the canonical repo path (mirrors cache.repo_id)."""
|
|
159
|
+
return hashlib.sha256(str(repo_root.resolve()).encode()).hexdigest()[:16]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _base_dir() -> Path:
|
|
163
|
+
env = os.environ.get("SOURCECODE_CONTEXT_CACHE_DIR", "")
|
|
164
|
+
return Path(env) if env else Path.home() / ".sourcecode"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _cache_dir(repo_root: Path) -> Path:
|
|
168
|
+
return _base_dir() / "context-cache" / _repo_id(repo_root)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _git_head(repo_root: Path) -> str:
|
|
172
|
+
try:
|
|
173
|
+
r = subprocess.run(
|
|
174
|
+
["git", "-C", str(repo_root), "rev-parse", "HEAD"],
|
|
175
|
+
capture_output=True, text=True, timeout=3,
|
|
176
|
+
)
|
|
177
|
+
if r.returncode == 0:
|
|
178
|
+
return r.stdout.strip()
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
return ""
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _worktree_signature(repo_root: Path) -> str:
|
|
185
|
+
"""Deterministic fingerprint of the *exact* tree state.
|
|
186
|
+
|
|
187
|
+
committed HEAD, plus — only when the tree is dirty — a hash of the porcelain
|
|
188
|
+
status and the diff against HEAD. Two identical tree states yield the same
|
|
189
|
+
signature (correct reuse); any tracked edit changes it (correct invalidation).
|
|
190
|
+
Returns ``""`` when the path is not a git repo (caller disables caching).
|
|
191
|
+
"""
|
|
192
|
+
head = _git_head(repo_root)
|
|
193
|
+
if not head:
|
|
194
|
+
return ""
|
|
195
|
+
try:
|
|
196
|
+
status = subprocess.run(
|
|
197
|
+
["git", "-C", str(repo_root), "status", "--porcelain"],
|
|
198
|
+
capture_output=True, text=True, timeout=5,
|
|
199
|
+
)
|
|
200
|
+
porcelain = status.stdout if status.returncode == 0 else ""
|
|
201
|
+
except Exception:
|
|
202
|
+
porcelain = ""
|
|
203
|
+
if not porcelain.strip():
|
|
204
|
+
return head # clean tree — HEAD fully describes it
|
|
205
|
+
try:
|
|
206
|
+
diff = subprocess.run(
|
|
207
|
+
["git", "-C", str(repo_root), "diff", "HEAD"],
|
|
208
|
+
capture_output=True, text=True, timeout=10,
|
|
209
|
+
)
|
|
210
|
+
diff_txt = diff.stdout if diff.returncode == 0 else ""
|
|
211
|
+
except Exception:
|
|
212
|
+
diff_txt = ""
|
|
213
|
+
dirty = hashlib.sha256((porcelain + "\x00" + diff_txt).encode("utf-8", "replace")).hexdigest()[:16]
|
|
214
|
+
return f"{head}+dirty:{dirty}"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _options_fingerprint(options: Optional[dict[str, Any]]) -> str:
|
|
218
|
+
"""Stable hash of the knowledge-relevant options (order-independent)."""
|
|
219
|
+
if not options:
|
|
220
|
+
return "-"
|
|
221
|
+
try:
|
|
222
|
+
canon = json.dumps(options, sort_keys=True, ensure_ascii=False, default=str)
|
|
223
|
+
except Exception:
|
|
224
|
+
canon = repr(sorted((str(k), str(v)) for k, v in options.items()))
|
|
225
|
+
return hashlib.sha256(canon.encode("utf-8")).hexdigest()[:12]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
# Cache
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
class ContextCache:
|
|
233
|
+
"""Provider-agnostic structured-context cache for one repository.
|
|
234
|
+
|
|
235
|
+
Construct via :meth:`for_repo`. All operations are best-effort: a broken or
|
|
236
|
+
unwritable cache degrades to a permanent miss, never to an error that would
|
|
237
|
+
break the command. Knows nothing about any AI model.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
def __init__(
|
|
241
|
+
self,
|
|
242
|
+
cache_dir: Path,
|
|
243
|
+
signature: str,
|
|
244
|
+
*,
|
|
245
|
+
enabled: bool = True,
|
|
246
|
+
) -> None:
|
|
247
|
+
self._dir = cache_dir
|
|
248
|
+
self._signature = signature
|
|
249
|
+
# No signature (non-git tree) means we cannot invalidate deterministically,
|
|
250
|
+
# so caching is disabled rather than risk serving stale context.
|
|
251
|
+
self.enabled = bool(enabled and signature)
|
|
252
|
+
|
|
253
|
+
@classmethod
|
|
254
|
+
def for_repo(cls, repo_root: Path, *, enabled: Optional[bool] = None) -> "ContextCache":
|
|
255
|
+
if enabled is None:
|
|
256
|
+
enabled = os.environ.get("SOURCECODE_CONTEXT_CACHE_DISABLE", "") not in ("1", "true", "yes")
|
|
257
|
+
root = repo_root.resolve()
|
|
258
|
+
return cls(_cache_dir(root), _worktree_signature(root), enabled=enabled)
|
|
259
|
+
|
|
260
|
+
# ── key ────────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
def knowledge_key(
|
|
263
|
+
self,
|
|
264
|
+
scope: str,
|
|
265
|
+
*,
|
|
266
|
+
options: Optional[dict[str, Any]] = None,
|
|
267
|
+
) -> str:
|
|
268
|
+
"""Key for a **reusable, command-agnostic** knowledge artifact.
|
|
269
|
+
|
|
270
|
+
Depends only on the knowledge state and the artifact *scope* (e.g.
|
|
271
|
+
``"java-cir"``) — deliberately NOT on any command or target, so every
|
|
272
|
+
command that needs the same knowledge shares one cache entry. This is
|
|
273
|
+
the level the cache operates at: cache the knowledge once, let commands
|
|
274
|
+
derive their answers from it for free.
|
|
275
|
+
"""
|
|
276
|
+
return self.make_key(scope, options=options)
|
|
277
|
+
|
|
278
|
+
def make_key(
|
|
279
|
+
self,
|
|
280
|
+
scope: str,
|
|
281
|
+
*,
|
|
282
|
+
target: Optional[str] = None,
|
|
283
|
+
options: Optional[dict[str, Any]] = None,
|
|
284
|
+
) -> str:
|
|
285
|
+
"""Deterministic cache key. Depends only on knowledge state, never on prompt text.
|
|
286
|
+
|
|
287
|
+
``scope`` names what is being cached. ``target`` is an optional finer
|
|
288
|
+
discriminator; for command-agnostic knowledge (the normal case) leave it
|
|
289
|
+
unset and prefer :meth:`knowledge_key`.
|
|
290
|
+
"""
|
|
291
|
+
material = _UNIT_SEP.join([
|
|
292
|
+
self._signature,
|
|
293
|
+
KL_SCHEMA_VERSION,
|
|
294
|
+
PRODUCER_VERSION,
|
|
295
|
+
ENVELOPE_VERSION,
|
|
296
|
+
scope,
|
|
297
|
+
target or "",
|
|
298
|
+
_options_fingerprint(options),
|
|
299
|
+
])
|
|
300
|
+
return hashlib.sha256(material.encode("utf-8")).hexdigest()[:16]
|
|
301
|
+
|
|
302
|
+
def _path(self, key: str) -> Path:
|
|
303
|
+
return self._dir / f"ctx-{key}.json.gz"
|
|
304
|
+
|
|
305
|
+
# ── read ─────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
def get(self, key: str) -> Optional[CachedContext]:
|
|
308
|
+
"""Return the cached context for *key*, or ``None`` on miss.
|
|
309
|
+
|
|
310
|
+
Records hit/miss and lookup latency. A stored entry whose embedded
|
|
311
|
+
signature/version no longer matches the current knowledge state is
|
|
312
|
+
treated as an invalidation (removed, counted, reported as a miss).
|
|
313
|
+
"""
|
|
314
|
+
if not self.enabled:
|
|
315
|
+
return None
|
|
316
|
+
start = time.perf_counter()
|
|
317
|
+
path = self._path(key)
|
|
318
|
+
try:
|
|
319
|
+
if not path.exists():
|
|
320
|
+
self._record(miss=True, lookup_ms=(time.perf_counter() - start) * 1000)
|
|
321
|
+
return None
|
|
322
|
+
envelope = json.loads(gzip.decompress(path.read_bytes()).decode("utf-8"))
|
|
323
|
+
except Exception:
|
|
324
|
+
# Corrupted file — evict and count as a miss.
|
|
325
|
+
_safe_unlink(path)
|
|
326
|
+
self._record(miss=True, lookup_ms=(time.perf_counter() - start) * 1000)
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
lookup_ms = (time.perf_counter() - start) * 1000
|
|
330
|
+
if not isinstance(envelope, dict) or envelope.get("ev") != ENVELOPE_VERSION:
|
|
331
|
+
_safe_unlink(path)
|
|
332
|
+
self._record(invalidation=True, lookup_ms=lookup_ms)
|
|
333
|
+
return None
|
|
334
|
+
# Defensive re-validation against live knowledge state.
|
|
335
|
+
if envelope.get("sig") != self._signature or envelope.get("kl") != KL_SCHEMA_VERSION:
|
|
336
|
+
_safe_unlink(path)
|
|
337
|
+
self._record(invalidation=True, lookup_ms=lookup_ms)
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
data = envelope.get("context")
|
|
341
|
+
if not isinstance(data, dict):
|
|
342
|
+
_safe_unlink(path)
|
|
343
|
+
self._record(invalidation=True, lookup_ms=lookup_ms)
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
self._record(hit=True, lookup_ms=lookup_ms)
|
|
347
|
+
return CachedContext.from_dict(data)
|
|
348
|
+
|
|
349
|
+
# ── write ─────────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
def put(
|
|
352
|
+
self,
|
|
353
|
+
key: str,
|
|
354
|
+
context: CachedContext,
|
|
355
|
+
*,
|
|
356
|
+
build_time_ms: Optional[float] = None,
|
|
357
|
+
) -> None:
|
|
358
|
+
"""Store *context* under *key*. Best-effort; failures are swallowed."""
|
|
359
|
+
if not self.enabled:
|
|
360
|
+
return
|
|
361
|
+
ts = dict(context.timestamps)
|
|
362
|
+
ts.setdefault("created_at", _now_iso())
|
|
363
|
+
if build_time_ms is not None:
|
|
364
|
+
ts["build_time_ms"] = round(build_time_ms, 3)
|
|
365
|
+
context.timestamps = ts
|
|
366
|
+
|
|
367
|
+
envelope = {
|
|
368
|
+
"ev": ENVELOPE_VERSION,
|
|
369
|
+
"kl": KL_SCHEMA_VERSION,
|
|
370
|
+
"sig": self._signature,
|
|
371
|
+
"key": key,
|
|
372
|
+
"ts": _now_iso(),
|
|
373
|
+
"context": context.to_dict(),
|
|
374
|
+
}
|
|
375
|
+
try:
|
|
376
|
+
self._dir.mkdir(parents=True, exist_ok=True)
|
|
377
|
+
payload = gzip.compress(json.dumps(envelope, ensure_ascii=False).encode("utf-8"), 6)
|
|
378
|
+
_atomic_write(self._path(key), payload)
|
|
379
|
+
except Exception:
|
|
380
|
+
return
|
|
381
|
+
self._record(store=True, build_ms=build_time_ms)
|
|
382
|
+
self._gc()
|
|
383
|
+
|
|
384
|
+
# ── stats ─────────────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
def stats(self) -> dict[str, Any]:
|
|
387
|
+
"""Return advisory metrics for this repo's context cache."""
|
|
388
|
+
s = _read_stats(self._dir)
|
|
389
|
+
files = list(self._dir.glob("ctx-*.json.gz")) if self._dir.exists() else []
|
|
390
|
+
bytes_stored = sum(f.stat().st_size for f in files if f.exists())
|
|
391
|
+
hits = s.get("hits", 0)
|
|
392
|
+
misses = s.get("misses", 0)
|
|
393
|
+
invalidations = s.get("invalidations", 0)
|
|
394
|
+
lookups = hits + misses + invalidations
|
|
395
|
+
lookup_total = s.get("lookup_time_total_ms", 0.0)
|
|
396
|
+
build_count = s.get("build_count", 0)
|
|
397
|
+
build_total = s.get("build_time_total_ms", 0.0)
|
|
398
|
+
return {
|
|
399
|
+
"cache_dir": str(self._dir),
|
|
400
|
+
"enabled": self.enabled,
|
|
401
|
+
"contexts": len(files),
|
|
402
|
+
"hits": hits,
|
|
403
|
+
"misses": misses,
|
|
404
|
+
"invalidations": invalidations,
|
|
405
|
+
"stores": s.get("stores", 0),
|
|
406
|
+
"hit_ratio": round(hits / lookups, 4) if lookups else 0.0,
|
|
407
|
+
"avg_lookup_ms": round(lookup_total / lookups, 4) if lookups else 0.0,
|
|
408
|
+
"avg_build_ms": round(build_total / build_count, 4) if build_count else 0.0,
|
|
409
|
+
"bytes_stored": bytes_stored,
|
|
410
|
+
"kl_schema_version": KL_SCHEMA_VERSION,
|
|
411
|
+
"producer_version": PRODUCER_VERSION,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
def clear(self) -> int:
|
|
415
|
+
"""Delete all context files and reset stats. Returns files removed."""
|
|
416
|
+
removed = 0
|
|
417
|
+
if self._dir.exists():
|
|
418
|
+
for f in self._dir.glob("ctx-*.json.gz"):
|
|
419
|
+
_safe_unlink(f)
|
|
420
|
+
removed += 1
|
|
421
|
+
_safe_unlink(self._dir / "stats.json")
|
|
422
|
+
return removed
|
|
423
|
+
|
|
424
|
+
# ── internals ─────────────────────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
def _record(
|
|
427
|
+
self,
|
|
428
|
+
*,
|
|
429
|
+
hit: bool = False,
|
|
430
|
+
miss: bool = False,
|
|
431
|
+
invalidation: bool = False,
|
|
432
|
+
store: bool = False,
|
|
433
|
+
lookup_ms: Optional[float] = None,
|
|
434
|
+
build_ms: Optional[float] = None,
|
|
435
|
+
) -> None:
|
|
436
|
+
try:
|
|
437
|
+
s = _read_stats(self._dir)
|
|
438
|
+
if hit:
|
|
439
|
+
s["hits"] = s.get("hits", 0) + 1
|
|
440
|
+
if miss:
|
|
441
|
+
s["misses"] = s.get("misses", 0) + 1
|
|
442
|
+
if invalidation:
|
|
443
|
+
s["invalidations"] = s.get("invalidations", 0) + 1
|
|
444
|
+
if store:
|
|
445
|
+
s["stores"] = s.get("stores", 0) + 1
|
|
446
|
+
if lookup_ms is not None:
|
|
447
|
+
s["lookup_time_total_ms"] = s.get("lookup_time_total_ms", 0.0) + lookup_ms
|
|
448
|
+
if build_ms is not None:
|
|
449
|
+
s["build_count"] = s.get("build_count", 0) + 1
|
|
450
|
+
s["build_time_total_ms"] = s.get("build_time_total_ms", 0.0) + build_ms
|
|
451
|
+
_write_stats(self._dir, s)
|
|
452
|
+
except Exception:
|
|
453
|
+
pass # stats are advisory — never fatal
|
|
454
|
+
|
|
455
|
+
def _gc(self) -> None:
|
|
456
|
+
"""Cleanup pass: drop entries past TTL, then cap total entry count.
|
|
457
|
+
|
|
458
|
+
Cleanup only — correctness never depends on it (stale-key entries are
|
|
459
|
+
already unreachable because the key embeds the knowledge signature).
|
|
460
|
+
"""
|
|
461
|
+
try:
|
|
462
|
+
ttl_days = int(os.environ.get("SOURCECODE_CONTEXT_CACHE_TTL_DAYS", _DEFAULT_TTL_DAYS))
|
|
463
|
+
max_entries = int(os.environ.get("SOURCECODE_CONTEXT_CACHE_MAX_ENTRIES", _DEFAULT_MAX_ENTRIES))
|
|
464
|
+
files = list(self._dir.glob("ctx-*.json.gz"))
|
|
465
|
+
|
|
466
|
+
if ttl_days > 0:
|
|
467
|
+
cutoff = time.time() - ttl_days * 86400
|
|
468
|
+
for f in list(files):
|
|
469
|
+
try:
|
|
470
|
+
if f.stat().st_mtime < cutoff:
|
|
471
|
+
_safe_unlink(f)
|
|
472
|
+
files.remove(f)
|
|
473
|
+
except Exception:
|
|
474
|
+
pass
|
|
475
|
+
|
|
476
|
+
if max_entries > 0 and len(files) > max_entries:
|
|
477
|
+
files.sort(key=lambda p: p.stat().st_mtime, reverse=True) # newest first
|
|
478
|
+
for f in files[max_entries:]:
|
|
479
|
+
_safe_unlink(f)
|
|
480
|
+
except Exception:
|
|
481
|
+
pass
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ---------------------------------------------------------------------------
|
|
485
|
+
# Stats file (advisory, plain JSON)
|
|
486
|
+
# ---------------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
def _stats_path(cache_dir: Path) -> Path:
|
|
489
|
+
return cache_dir / "stats.json"
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _read_stats(cache_dir: Path) -> dict[str, Any]:
|
|
493
|
+
p = _stats_path(cache_dir)
|
|
494
|
+
if not p.exists():
|
|
495
|
+
return {}
|
|
496
|
+
try:
|
|
497
|
+
d = json.loads(p.read_text(encoding="utf-8"))
|
|
498
|
+
return d if isinstance(d, dict) else {}
|
|
499
|
+
except Exception:
|
|
500
|
+
_safe_unlink(p) # corrupted — reset
|
|
501
|
+
return {}
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _write_stats(cache_dir: Path, stats: dict[str, Any]) -> None:
|
|
505
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
506
|
+
_atomic_write(_stats_path(cache_dir), json.dumps(stats, ensure_ascii=False).encode("utf-8"))
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# ---------------------------------------------------------------------------
|
|
510
|
+
# I/O utilities
|
|
511
|
+
# ---------------------------------------------------------------------------
|
|
512
|
+
|
|
513
|
+
def _atomic_write(dest: Path, data: bytes) -> None:
|
|
514
|
+
"""Write *data* to *dest* atomically via a unique temp file + os.replace."""
|
|
515
|
+
tmp = dest.parent / f"{dest.name}.{os.getpid()}.{uuid.uuid4().hex[:8]}.tmp"
|
|
516
|
+
try:
|
|
517
|
+
tmp.write_bytes(data)
|
|
518
|
+
os.replace(tmp, dest)
|
|
519
|
+
finally:
|
|
520
|
+
_safe_unlink(tmp)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _safe_unlink(path: Path) -> None:
|
|
524
|
+
try:
|
|
525
|
+
path.unlink(missing_ok=True)
|
|
526
|
+
except Exception:
|
|
527
|
+
pass
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
# ---------------------------------------------------------------------------
|
|
531
|
+
# Knowledge-level API — the reusable-context surface commands build on
|
|
532
|
+
# ---------------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
#: Scope name for the Java/Spring Canonical IR — the expensive parse artifact
|
|
535
|
+
#: shared by explain / impact / onboard / review-pr / modernize / archetype …
|
|
536
|
+
SCOPE_JAVA_CIR: str = "java-cir"
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
@dataclass
|
|
540
|
+
class KnowledgeLookup:
|
|
541
|
+
"""Outcome of a knowledge fetch, for command-side observability."""
|
|
542
|
+
hit: bool
|
|
543
|
+
enabled: bool
|
|
544
|
+
lookup_ms: float = 0.0
|
|
545
|
+
build_ms: float = 0.0
|
|
546
|
+
|
|
547
|
+
def render(self) -> str:
|
|
548
|
+
if not self.enabled:
|
|
549
|
+
return "Context cache: disabled"
|
|
550
|
+
if self.hit:
|
|
551
|
+
return f"Context cache: HIT (lookup {self.lookup_ms:.1f}ms)"
|
|
552
|
+
return f"Context cache: MISS (build {self.build_ms:.0f}ms)"
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def get_or_build_cir(
|
|
556
|
+
repo_root: Path,
|
|
557
|
+
root: Path,
|
|
558
|
+
file_list: list[str],
|
|
559
|
+
*,
|
|
560
|
+
since: Optional[str] = None,
|
|
561
|
+
) -> tuple[Any, KnowledgeLookup]:
|
|
562
|
+
"""Return ``(CanonicalRepositoryIR, KnowledgeLookup)``, caching the CIR.
|
|
563
|
+
|
|
564
|
+
This is the knowledge-level entry point. It caches the **reusable** raw IR
|
|
565
|
+
(the expensive Java parse) keyed only by knowledge state — not by command —
|
|
566
|
+
so every command that needs a CIR shares one cache entry. On a hit the full
|
|
567
|
+
CIR is rebuilt cheaply from the cached raw IR via ``ir_dict_to_canonical``,
|
|
568
|
+
skipping the parse entirely. Best-effort: any cache fault falls back to a
|
|
569
|
+
fresh build.
|
|
570
|
+
"""
|
|
571
|
+
from sourcecode.canonical_ir import (
|
|
572
|
+
build_canonical_ir,
|
|
573
|
+
ir_dict_to_canonical,
|
|
574
|
+
validate_canonical_ir,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
cache = ContextCache.for_repo(repo_root)
|
|
578
|
+
if not cache.enabled:
|
|
579
|
+
cir = build_canonical_ir(file_list, root, since=since)
|
|
580
|
+
return cir, KnowledgeLookup(hit=False, enabled=False)
|
|
581
|
+
|
|
582
|
+
key = cache.knowledge_key(SCOPE_JAVA_CIR, options={"since": since} if since else None)
|
|
583
|
+
|
|
584
|
+
t0 = time.perf_counter()
|
|
585
|
+
cached = cache.get(key)
|
|
586
|
+
lookup_ms = (time.perf_counter() - t0) * 1000
|
|
587
|
+
if cached is not None:
|
|
588
|
+
raw = cached.payload.get("raw_ir")
|
|
589
|
+
if isinstance(raw, dict):
|
|
590
|
+
try:
|
|
591
|
+
cir = ir_dict_to_canonical(raw, file_paths=file_list)
|
|
592
|
+
if not validate_canonical_ir(cir):
|
|
593
|
+
return cir, KnowledgeLookup(hit=True, enabled=True, lookup_ms=lookup_ms)
|
|
594
|
+
except Exception:
|
|
595
|
+
pass # corrupt/incompatible payload — fall through to rebuild
|
|
596
|
+
|
|
597
|
+
t1 = time.perf_counter()
|
|
598
|
+
cir = build_canonical_ir(file_list, root, since=since)
|
|
599
|
+
build_ms = (time.perf_counter() - t1) * 1000
|
|
600
|
+
try:
|
|
601
|
+
ctx = CachedContext(
|
|
602
|
+
metadata={
|
|
603
|
+
"scope": SCOPE_JAVA_CIR,
|
|
604
|
+
"cir_hash": cir.cir_hash,
|
|
605
|
+
"schema_version": cir.schema_version,
|
|
606
|
+
"file_count": len(cir.files),
|
|
607
|
+
"symbol_count": len(cir.symbols),
|
|
608
|
+
},
|
|
609
|
+
entities=list(cir.symbols[:200]), # inspectable sample, not exhaustive
|
|
610
|
+
graph_metrics={
|
|
611
|
+
"symbol_count": len(cir.symbols),
|
|
612
|
+
"edge_count": len(cir.call_graph),
|
|
613
|
+
"endpoint_count": len(cir.endpoints),
|
|
614
|
+
},
|
|
615
|
+
payload={"raw_ir": cir._raw_ir},
|
|
616
|
+
)
|
|
617
|
+
cache.put(key, ctx, build_time_ms=build_ms)
|
|
618
|
+
except Exception:
|
|
619
|
+
pass
|
|
620
|
+
return cir, KnowledgeLookup(hit=False, enabled=True, build_ms=build_ms)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
# ---------------------------------------------------------------------------
|
|
624
|
+
# Module-level observability helper
|
|
625
|
+
# ---------------------------------------------------------------------------
|
|
626
|
+
|
|
627
|
+
def stats_for_repo(repo_root: Path) -> dict[str, Any]:
|
|
628
|
+
"""Convenience: stats for a repo without a live signature (read-only view)."""
|
|
629
|
+
root = repo_root.resolve()
|
|
630
|
+
cache = ContextCache(_cache_dir(root), _worktree_signature(root), enabled=True)
|
|
631
|
+
return cache.stats()
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def clear_for_repo(repo_root: Path) -> int:
|
|
635
|
+
root = repo_root.resolve()
|
|
636
|
+
cache = ContextCache(_cache_dir(root), "read-only", enabled=True)
|
|
637
|
+
return cache.clear()
|
sourcecode/explain.py
CHANGED
|
@@ -133,6 +133,30 @@ class ClassExplanation:
|
|
|
133
133
|
"found": self.found,
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
@classmethod
|
|
137
|
+
def from_dict(cls, d: dict) -> "ClassExplanation":
|
|
138
|
+
"""Reconstruct from :meth:`to_dict` output (lossless round-trip).
|
|
139
|
+
|
|
140
|
+
The informational ``incoming_callers_note`` key is derived, not stored
|
|
141
|
+
on the instance, so it is dropped on the way back in.
|
|
142
|
+
"""
|
|
143
|
+
return cls(
|
|
144
|
+
class_name=d.get("class_name", ""),
|
|
145
|
+
class_fqn=d.get("class_fqn", ""),
|
|
146
|
+
stereotype=d.get("stereotype", ""),
|
|
147
|
+
purpose=d.get("purpose", ""),
|
|
148
|
+
public_methods=list(d.get("public_methods", [])),
|
|
149
|
+
incoming_callers=list(d.get("incoming_callers", [])),
|
|
150
|
+
outgoing_deps=list(d.get("outgoing_deps", [])),
|
|
151
|
+
events_published=list(d.get("events_published", [])),
|
|
152
|
+
events_consumed=list(d.get("events_consumed", [])),
|
|
153
|
+
transactions=list(d.get("transactions", [])),
|
|
154
|
+
security_constraints=list(d.get("security_constraints", [])),
|
|
155
|
+
rest_endpoints=list(d.get("rest_endpoints", [])),
|
|
156
|
+
warnings=list(d.get("warnings", [])),
|
|
157
|
+
found=bool(d.get("found", True)),
|
|
158
|
+
)
|
|
159
|
+
|
|
136
160
|
|
|
137
161
|
# ---------------------------------------------------------------------------
|
|
138
162
|
# FQN resolution
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=1iN-frOfJr05mYoED0IYnoxSY-4xnA-sHP9MRe_9Qn4,308
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/archetype.py,sha256=ezeyCR4E0VeZUUBPwmsWUUPAhIlqwwkc8qDieUx2JWA,31087
|
|
4
4
|
sourcecode/architecture_analyzer.py,sha256=6_wC4TYuBYxaqckZS0I1vBSMRPeH2vzlDB48gXwOOd4,46627
|
|
@@ -10,9 +10,10 @@ sourcecode/caller_metrics.py,sha256=HG8RCOkpzzsEGdnMRob6byrwjoj5__t0TEsBj4yR7ks,
|
|
|
10
10
|
sourcecode/canonical_ir.py,sha256=U69m8Vkr0p407xF1q5y1KguHXWEAubYw8NFHR9hDNJk,29178
|
|
11
11
|
sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
|
|
12
12
|
sourcecode/classifier.py,sha256=RhLMDR031FAiUcT3bsf2EL3KrFwl7KsWh_wXXR_Bumo,18729
|
|
13
|
-
sourcecode/cli.py,sha256=
|
|
13
|
+
sourcecode/cli.py,sha256=xYrKelvgspWuus8LM4DcRroXGydbU47CAr-KV-8oysU,299977
|
|
14
14
|
sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
|
|
15
15
|
sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
|
|
16
|
+
sourcecode/context_cache.py,sha256=maws79rLKDwyZ7c9Q2LwlIxqZHRFlaKzErtO3mglWoA,25036
|
|
16
17
|
sourcecode/context_graph.py,sha256=8MVDu06bPhvRDTgUqWEvRME2fOw34bFLf4OUILZd13I,43527
|
|
17
18
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
18
19
|
sourcecode/context_summarizer.py,sha256=cI2TZMvEhl0BEma12VtPaX6z03ZVBAetVzuK5GaCOvg,6852
|
|
@@ -28,7 +29,7 @@ sourcecode/entrypoint_classifier.py,sha256=jhTYlyqDJH2AtdEcLVaRU3lYRTJuF8DkxVzl4
|
|
|
28
29
|
sourcecode/env_analyzer.py,sha256=aNTyYgQk5noJDfJU6FmasmESOHfiomyJw5EvZqjy6qc,22213
|
|
29
30
|
sourcecode/error_schema.py,sha256=uwosfNaSujtYm11_732Hu92z5ITV040fQDaIyefSvR4,1683
|
|
30
31
|
sourcecode/evidence_provider.py,sha256=GSSL44JEaouO5AHks2sB3d1YvC9xIKIld1yBYxZpXxo,4277
|
|
31
|
-
sourcecode/explain.py,sha256=
|
|
32
|
+
sourcecode/explain.py,sha256=HnHWVTNNf9fzeR3FP-A-eXKeKZvBcUEuODgIO3rJQkQ,22312
|
|
32
33
|
sourcecode/file_chunker.py,sha256=3vkM3mDQ5eE_yTPvUgjyjpGFBIjkW6_mrBmIbrylnA8,16444
|
|
33
34
|
sourcecode/file_classifier.py,sha256=A0fEABqtfVu1MfoaxnPAvGpZgneGgVXlJDhT74NYXxE,15314
|
|
34
35
|
sourcecode/format_contract.py,sha256=1cTNqwP8geA2hbQoBHUPgX3_vSh3l8guJT_jmgEnFF8,3466
|
|
@@ -117,8 +118,8 @@ sourcecode/telemetry/consent.py,sha256=LIAO9ohJZF8OuZwM4u1VWtALlYfTCCKq4wV3Vwc7i
|
|
|
117
118
|
sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
|
|
118
119
|
sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
|
|
119
120
|
sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
|
|
120
|
-
sourcecode-2.
|
|
121
|
-
sourcecode-2.
|
|
122
|
-
sourcecode-2.
|
|
123
|
-
sourcecode-2.
|
|
124
|
-
sourcecode-2.
|
|
121
|
+
sourcecode-2.4.0.dist-info/METADATA,sha256=fairNRCqgRAaSicMGWcWQcPral9_3NeVBIDTrv2rHaI,10837
|
|
122
|
+
sourcecode-2.4.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
123
|
+
sourcecode-2.4.0.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
|
|
124
|
+
sourcecode-2.4.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
125
|
+
sourcecode-2.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|