sourcecode 2.2.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/archetype.py +605 -0
- sourcecode/architecture_summary.py +41 -0
- sourcecode/classifier.py +94 -1
- sourcecode/cli.py +135 -1
- sourcecode/context_cache.py +637 -0
- sourcecode/explain.py +24 -0
- sourcecode/graph_evidence.py +352 -0
- {sourcecode-2.2.0.dist-info → sourcecode-2.4.0.dist-info}/METADATA +3 -3
- {sourcecode-2.2.0.dist-info → sourcecode-2.4.0.dist-info}/RECORD +13 -10
- {sourcecode-2.2.0.dist-info → sourcecode-2.4.0.dist-info}/WHEEL +0 -0
- {sourcecode-2.2.0.dist-info → sourcecode-2.4.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.2.0.dist-info → sourcecode-2.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -31,6 +31,18 @@ _CORE_DETECTION_MODULES = {"scanner", "detectors", "classifier", "workspace"}
|
|
|
31
31
|
# headline to a qualified, consistent phrasing instead of overclaiming.
|
|
32
32
|
_MIN_REST_ENDPOINTS_FOR_LABEL = 5
|
|
33
33
|
|
|
34
|
+
# BUG (neo4j field test): endpoint COUNT alone cannot separate "an API server"
|
|
35
|
+
# from "a large engine/platform that also exposes a few HTTP endpoints". A graph
|
|
36
|
+
# database with ~14 JAX-RS endpoints across 5,600+ source files is not a "REST
|
|
37
|
+
# API" — yet it clears the count threshold above and gets mis-headlined, the exact
|
|
38
|
+
# thing a reader sees in the first line. Add a proportionality guard: on a large
|
|
39
|
+
# JVM codebase, only keep the "REST API" headline when the HTTP surface is
|
|
40
|
+
# architecturally significant relative to code size (at least one endpoint per
|
|
41
|
+
# _API_DOMINANCE_FILES_PER_ENDPOINT source files). Below that density the web
|
|
42
|
+
# layer is a minor component, not the architecture.
|
|
43
|
+
_LARGE_JVM_SOURCE_FILES = 400
|
|
44
|
+
_API_DOMINANCE_FILES_PER_ENDPOINT = 100
|
|
45
|
+
|
|
34
46
|
_OPTIONAL_LABEL_MAP: dict[str, str] = {
|
|
35
47
|
"DependencyAnalyzer": "dependencias",
|
|
36
48
|
"GraphAnalyzer": "grafo de módulos",
|
|
@@ -207,6 +219,21 @@ class ArchitectureSummarizer:
|
|
|
207
219
|
f"(HTTP framework present; only {total} endpoint{plural} "
|
|
208
220
|
f"detected — see `endpoints`)."
|
|
209
221
|
)
|
|
222
|
+
# Proportionality guard: a large JVM codebase whose HTTP surface is
|
|
223
|
+
# a rounding error is an engine/platform exposing an API, not an
|
|
224
|
+
# "API" project. Endpoint count already cleared the threshold above,
|
|
225
|
+
# so lead with what the system IS instead of overclaiming "REST API".
|
|
226
|
+
n_src = self._jvm_source_file_count(sm)
|
|
227
|
+
if (
|
|
228
|
+
n_src >= _LARGE_JVM_SOURCE_FILES
|
|
229
|
+
and total * _API_DOMINANCE_FILES_PER_ENDPOINT < n_src
|
|
230
|
+
):
|
|
231
|
+
plural = "s" if total != 1 else ""
|
|
232
|
+
return (
|
|
233
|
+
f"{stack_label} multi-module codebase{fw_str} — large "
|
|
234
|
+
f"system ({n_src} source files) with a minor HTTP surface "
|
|
235
|
+
f"({total} endpoint{plural}); not API-first. See `endpoints`."
|
|
236
|
+
)
|
|
210
237
|
return f"{stack_label} {runtime.lower()}{fw_str}."
|
|
211
238
|
return f"{stack_label} project{fw_str}."
|
|
212
239
|
|
|
@@ -228,6 +255,20 @@ class ArchitectureSummarizer:
|
|
|
228
255
|
self._endpoint_support_cache = (total, high)
|
|
229
256
|
return self._endpoint_support_cache
|
|
230
257
|
|
|
258
|
+
def _jvm_source_file_count(self, sm: SourceMap) -> int:
|
|
259
|
+
"""Count JVM source files in the scanned tree (excludes tooling paths).
|
|
260
|
+
Used by the proportionality guard to tell a large engine/platform apart
|
|
261
|
+
from an API-first project. A truncated tree only undercounts, which makes
|
|
262
|
+
the guard more conservative (less likely to degrade a real API)."""
|
|
263
|
+
exts = (".java", ".kt", ".kts", ".scala")
|
|
264
|
+
try:
|
|
265
|
+
return sum(
|
|
266
|
+
1 for p in flatten_file_tree(sm.file_tree)
|
|
267
|
+
if p.endswith(exts) and not self._is_tooling_path(p)
|
|
268
|
+
)
|
|
269
|
+
except Exception:
|
|
270
|
+
return 0
|
|
271
|
+
|
|
231
272
|
def _qualify_web_pattern(self, arch: Any, arch_line: str, sm: SourceMap) -> str:
|
|
232
273
|
"""BUG #1 (Jenkins field test): the "mvc" pattern is a directory-name
|
|
233
274
|
heuristic (dirs matching controller/model/view keywords). On a Java/Kotlin
|
sourcecode/classifier.py
CHANGED
|
@@ -34,6 +34,30 @@ _API_FRAMEWORKS = {
|
|
|
34
34
|
_WEB_FRAMEWORKS = {"Next.js", "React", "Vue", "Svelte", "Vite", "Flutter", "Phoenix", "Angular"}
|
|
35
35
|
_CLI_FRAMEWORKS = {"Typer", "Cobra", "Clap"}
|
|
36
36
|
_API_STACKS = {"python", "go", "java", "php", "ruby", "dotnet", "kotlin", "scala"}
|
|
37
|
+
|
|
38
|
+
# Center-of-gravity gate for the "api" decision (neo4j field test).
|
|
39
|
+
# The historical rule was pure PRESENCE: any API framework on the classpath →
|
|
40
|
+
# project_type="api". That mislabels a large engine/platform that merely exposes a
|
|
41
|
+
# small HTTP surface. Two evidence paths defeat the existing adapter-localization
|
|
42
|
+
# guard because they are repo-wide (no locatable minority module):
|
|
43
|
+
# * a jakarta.ws.rs / javax.ws.rs-api coordinate pinned in a BOM/parent pom's
|
|
44
|
+
# <dependencyManagement> (a version pin, not usage), and
|
|
45
|
+
# * a source-annotation "Jakarta EE" marker synthesized from @Path resources
|
|
46
|
+
# (detected_via carries no file path).
|
|
47
|
+
# So on a repo large enough to HAVE a distinct center of gravity, require the API
|
|
48
|
+
# surface to occupy a real share of the code (or the dominant module) before we
|
|
49
|
+
# headline the whole system as an API. Small/single-module repos keep presence-based
|
|
50
|
+
# behavior (a real API framework there defines the project — e.g. the Eureka field
|
|
51
|
+
# test). JVM-only: this is where the false positive lives; other stacks are untouched.
|
|
52
|
+
_JVM_STACKS = {"java", "kotlin", "scala", "groovy"}
|
|
53
|
+
_JVM_CODE_EXTS = (".java", ".kt", ".kts", ".scala", ".groovy")
|
|
54
|
+
_API_DOMINANCE_MIN_FILES = 300
|
|
55
|
+
_API_PRIMARY_SHARE = 0.15
|
|
56
|
+
_API_SURFACE_ENTRY_KINDS = frozenset({
|
|
57
|
+
"jax_rs_controller", "jax_rs_provider",
|
|
58
|
+
"rest_controller", "mvc_controller", "controller", "web", "server",
|
|
59
|
+
})
|
|
60
|
+
|
|
37
61
|
ConfidenceLevel = Literal["high", "medium", "low"]
|
|
38
62
|
|
|
39
63
|
|
|
@@ -143,7 +167,18 @@ class TypeClassifier:
|
|
|
143
167
|
return "web_mvc"
|
|
144
168
|
|
|
145
169
|
if framework_names & _API_FRAMEWORKS:
|
|
146
|
-
|
|
170
|
+
if not (stack_names & _JVM_STACKS) or self._api_is_center_of_gravity(
|
|
171
|
+
file_tree, stacks, entry_points
|
|
172
|
+
):
|
|
173
|
+
return "api"
|
|
174
|
+
# A JVM API framework is present but is NOT the repo's center of gravity.
|
|
175
|
+
# It survived adapter-localization only because its evidence is repo-wide
|
|
176
|
+
# (a BOM/parent-pom version pin, or a source-annotation marker with no
|
|
177
|
+
# locatable minority module). Do not headline the whole system as an API —
|
|
178
|
+
# the framework stays in stacks[].frameworks as a secondary capability and
|
|
179
|
+
# the primary type falls back to the structural center of gravity.
|
|
180
|
+
if self._is_multi_module(file_tree):
|
|
181
|
+
return "library"
|
|
147
182
|
|
|
148
183
|
# All app-defining frameworks were localized to optional adapter submodules
|
|
149
184
|
# (multi-module library with per-framework integrations) — report library,
|
|
@@ -263,6 +298,64 @@ class TypeClassifier:
|
|
|
263
298
|
localized.add(fw_name)
|
|
264
299
|
return localized
|
|
265
300
|
|
|
301
|
+
def _api_is_center_of_gravity(
|
|
302
|
+
self,
|
|
303
|
+
file_tree: dict[str, Any],
|
|
304
|
+
stacks: Sequence[StackDetection],
|
|
305
|
+
entry_points: Sequence[EntryPoint],
|
|
306
|
+
) -> bool:
|
|
307
|
+
"""True when an API/web framework reflects the repo's center of gravity, not
|
|
308
|
+
merely its presence. Small or single-module JVM repos: a present API framework
|
|
309
|
+
defines the project (keeps the historical behavior). Large multi-module repos:
|
|
310
|
+
the HTTP surface must live in the dominant module or occupy at least
|
|
311
|
+
``_API_PRIMARY_SHARE`` of the JVM source — otherwise it is a minor capability
|
|
312
|
+
of an engine/platform/library, not the primary type."""
|
|
313
|
+
counts: dict[str, int] = {}
|
|
314
|
+
for p in flatten_file_tree(file_tree):
|
|
315
|
+
if p.endswith(_JVM_CODE_EXTS):
|
|
316
|
+
mod = self._module_of(p)
|
|
317
|
+
counts[mod] = counts.get(mod, 0) + 1
|
|
318
|
+
counts.pop("", None)
|
|
319
|
+
total = sum(counts.values())
|
|
320
|
+
# Too small, or a single module, to reason about a center of gravity.
|
|
321
|
+
if total < _API_DOMINANCE_MIN_FILES or len(counts) < 2:
|
|
322
|
+
return True
|
|
323
|
+
dominant = max(counts, key=lambda m: counts[m])
|
|
324
|
+
api_modules = self._api_surface_modules(stacks, entry_points)
|
|
325
|
+
if not api_modules:
|
|
326
|
+
# API framework known only repo-wide (BOM version pin / annotation marker)
|
|
327
|
+
# with no source-located HTTP surface in a large multi-module system.
|
|
328
|
+
return False
|
|
329
|
+
if dominant in api_modules:
|
|
330
|
+
return True
|
|
331
|
+
api_files = sum(counts.get(m, 0) for m in api_modules)
|
|
332
|
+
return (api_files / total) >= _API_PRIMARY_SHARE
|
|
333
|
+
|
|
334
|
+
def _api_surface_modules(
|
|
335
|
+
self,
|
|
336
|
+
stacks: Sequence[StackDetection],
|
|
337
|
+
entry_points: Sequence[EntryPoint],
|
|
338
|
+
) -> set[str]:
|
|
339
|
+
"""Modules that actually host an HTTP/REST surface: derived from API entry
|
|
340
|
+
points (controllers / JAX-RS resources) and from API-framework file evidence.
|
|
341
|
+
Repo-wide manifest evidence (no path) contributes no module, by design."""
|
|
342
|
+
modules: set[str] = set()
|
|
343
|
+
for ep in entry_points:
|
|
344
|
+
if getattr(ep, "kind", None) in _API_SURFACE_ENTRY_KINDS and ep.path:
|
|
345
|
+
modules.add(self._module_of(ep.path))
|
|
346
|
+
for stack in stacks:
|
|
347
|
+
for fw in stack.frameworks:
|
|
348
|
+
if fw.name not in _API_FRAMEWORKS:
|
|
349
|
+
continue
|
|
350
|
+
for ev in fw.detected_via:
|
|
351
|
+
if ev.startswith("manifest:"):
|
|
352
|
+
continue
|
|
353
|
+
m = self._EVIDENCE_PATH_RE.search(ev)
|
|
354
|
+
if m:
|
|
355
|
+
modules.add(self._module_of(m.group(1).strip()))
|
|
356
|
+
modules.discard("")
|
|
357
|
+
return modules
|
|
358
|
+
|
|
266
359
|
def _is_fullstack(self, stacks: Sequence[StackDetection]) -> bool:
|
|
267
360
|
has_web = False
|
|
268
361
|
has_api = False
|
sourcecode/cli.py
CHANGED
|
@@ -246,6 +246,8 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
|
|
|
246
246
|
"rename-class",
|
|
247
247
|
# Large file semantic chunking (BLOCKER-B)
|
|
248
248
|
"chunk-file",
|
|
249
|
+
# Experimental evidence-based archetype (parallel to legacy)
|
|
250
|
+
"archetype",
|
|
249
251
|
}
|
|
250
252
|
)
|
|
251
253
|
|
|
@@ -5339,6 +5341,7 @@ def explain_cmd(
|
|
|
5339
5341
|
from sourcecode.context_graph import ContextGraph
|
|
5340
5342
|
from sourcecode.spring_model import SpringSemanticModel
|
|
5341
5343
|
from sourcecode.explain import explain_class
|
|
5344
|
+
from sourcecode import context_cache as _ctxcache
|
|
5342
5345
|
|
|
5343
5346
|
if not class_name.strip():
|
|
5344
5347
|
_emit_error_json(
|
|
@@ -5381,14 +5384,27 @@ def explain_cmd(
|
|
|
5381
5384
|
)
|
|
5382
5385
|
raise typer.Exit(code=1)
|
|
5383
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.
|
|
5384
5392
|
_prog = Progress()
|
|
5385
5393
|
_prog.start(f"explaining {class_name} ({len(file_list)} files)")
|
|
5394
|
+
_cc_look = None
|
|
5386
5395
|
try:
|
|
5387
|
-
|
|
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
|
|
5388
5402
|
model = SpringSemanticModel.build(cir)
|
|
5389
5403
|
explanation = explain_class(class_name, cir, model)
|
|
5390
5404
|
finally:
|
|
5391
5405
|
_prog.finish()
|
|
5406
|
+
if _cc_look is not None:
|
|
5407
|
+
typer.echo(_cc_look.render(), err=True)
|
|
5392
5408
|
|
|
5393
5409
|
if format == "json":
|
|
5394
5410
|
output = _json.dumps(explanation.to_dict(), indent=2, ensure_ascii=False)
|
|
@@ -6422,6 +6438,77 @@ def config_cmd() -> None:
|
|
|
6422
6438
|
|
|
6423
6439
|
# ── cold-start (RIS bootstrap for external MCP and agents) ───────────────────
|
|
6424
6440
|
|
|
6441
|
+
@app.command("archetype")
|
|
6442
|
+
def archetype_cmd(
|
|
6443
|
+
path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
|
|
6444
|
+
output_path: Optional[Path] = typer.Option(
|
|
6445
|
+
None, "--output", "-o", help="Write output to a file instead of stdout."
|
|
6446
|
+
),
|
|
6447
|
+
) -> None:
|
|
6448
|
+
"""[EXPERIMENTAL] Evidence-based architectural archetype (parallel to legacy).
|
|
6449
|
+
|
|
6450
|
+
Emits a 4-dimension archetype analysis — software_archetype, architectural_style,
|
|
6451
|
+
deployment_shape, primary_interface — each with scored candidates, the evidence
|
|
6452
|
+
(weighted by code mass), a confidence level, and a human explanation.
|
|
6453
|
+
|
|
6454
|
+
This runs ALONGSIDE the legacy architecture pattern and does NOT change any
|
|
6455
|
+
report wording. It exists to validate the new model against the old one across
|
|
6456
|
+
many repositories before any cutover.
|
|
6457
|
+
"""
|
|
6458
|
+
import json as _json
|
|
6459
|
+
from sourcecode.adaptive_scanner import AdaptiveScanner
|
|
6460
|
+
from sourcecode.repo_classifier import RepoClassifier
|
|
6461
|
+
from sourcecode.detectors import ProjectDetector, build_default_detectors
|
|
6462
|
+
from sourcecode.schema import SourceMap
|
|
6463
|
+
from sourcecode.tree_utils import flatten_file_tree
|
|
6464
|
+
from sourcecode.archetype import ArchetypeClassifier
|
|
6465
|
+
|
|
6466
|
+
target = Path(path).resolve()
|
|
6467
|
+
topology = RepoClassifier().classify(target)
|
|
6468
|
+
scanner = AdaptiveScanner(target, topology=topology, base_depth=12)
|
|
6469
|
+
file_tree = scanner.scan_tree()
|
|
6470
|
+
manifests = scanner.find_manifests()
|
|
6471
|
+
|
|
6472
|
+
detector = ProjectDetector(build_default_detectors())
|
|
6473
|
+
stacks, entry_points, _ = detector.detect(target, file_tree, manifests)
|
|
6474
|
+
stacks, project_type = detector.classify_results(file_tree, stacks, entry_points)
|
|
6475
|
+
|
|
6476
|
+
sm = SourceMap(
|
|
6477
|
+
file_tree=file_tree, stacks=stacks,
|
|
6478
|
+
project_type=project_type, entry_points=entry_points,
|
|
6479
|
+
)
|
|
6480
|
+
sm.file_paths = [p.replace("\\", "/") for p in flatten_file_tree(file_tree)]
|
|
6481
|
+
_java = next((s for s in stacks if s.stack == "java"), None)
|
|
6482
|
+
if _java is not None:
|
|
6483
|
+
sm.packaging = getattr(_java, "packaging", None)
|
|
6484
|
+
|
|
6485
|
+
# Module graph as a first-class Evidence Provider (ADR-0003 step 2). Built
|
|
6486
|
+
# here so archetype can consume topological evidence. Bounded + best-effort:
|
|
6487
|
+
# on very large trees or any failure it degrades to None (name-mass-only),
|
|
6488
|
+
# which is exactly the pre-provider behaviour — no regression.
|
|
6489
|
+
module_graph = None
|
|
6490
|
+
try:
|
|
6491
|
+
from sourcecode.graph_analyzer import GraphAnalyzer
|
|
6492
|
+
module_graph = GraphAnalyzer(max_nodes=1200, max_edges=4000).analyze(
|
|
6493
|
+
target, file_tree, detail="full", entry_points=entry_points,
|
|
6494
|
+
)
|
|
6495
|
+
if module_graph is not None:
|
|
6496
|
+
sm.module_graph = module_graph
|
|
6497
|
+
sm.module_graph_summary = module_graph.summary
|
|
6498
|
+
except Exception:
|
|
6499
|
+
module_graph = None
|
|
6500
|
+
|
|
6501
|
+
analysis = ArchetypeClassifier().analyze(sm, root=target, module_graph=module_graph)
|
|
6502
|
+
result = analysis.to_dict()
|
|
6503
|
+
result["legacy_project_type"] = project_type # side-by-side with legacy for validation
|
|
6504
|
+
out = _json.dumps(result, indent=2, ensure_ascii=False)
|
|
6505
|
+
if output_path:
|
|
6506
|
+
_safe_write_file(output_path, out)
|
|
6507
|
+
typer.echo(f"Archetype analysis written to {output_path}")
|
|
6508
|
+
else:
|
|
6509
|
+
typer.echo(out)
|
|
6510
|
+
|
|
6511
|
+
|
|
6425
6512
|
@app.command("cold-start")
|
|
6426
6513
|
def cold_start_cmd(
|
|
6427
6514
|
path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
|
|
@@ -7126,6 +7213,53 @@ def cache_freshness_cmd(
|
|
|
7126
7213
|
typer.echo(f"RIS updated: {result.get('ris_last_updated_at') or 'never'}")
|
|
7127
7214
|
|
|
7128
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
|
+
|
|
7129
7263
|
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
7130
7264
|
|
|
7131
7265
|
def _stderr_is_interactive() -> bool:
|