sourcecode 1.33.17__tar.gz → 1.33.19__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.
Files changed (95) hide show
  1. {sourcecode-1.33.17 → sourcecode-1.33.19}/PKG-INFO +2 -2
  2. {sourcecode-1.33.17 → sourcecode-1.33.19}/README.md +1 -1
  3. {sourcecode-1.33.17 → sourcecode-1.33.19}/pyproject.toml +1 -1
  4. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/cli.py +44 -6
  6. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/license.py +12 -3
  7. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/output_budget.py +80 -5
  8. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/ris.py +5 -4
  9. {sourcecode-1.33.17 → sourcecode-1.33.19}/.github/workflows/build-windows.yml +0 -0
  10. {sourcecode-1.33.17 → sourcecode-1.33.19}/.gitignore +0 -0
  11. {sourcecode-1.33.17 → sourcecode-1.33.19}/.ruff.toml +0 -0
  12. {sourcecode-1.33.17 → sourcecode-1.33.19}/CHANGELOG.md +0 -0
  13. {sourcecode-1.33.17 → sourcecode-1.33.19}/CONTRIBUTING.md +0 -0
  14. {sourcecode-1.33.17 → sourcecode-1.33.19}/LICENSE +0 -0
  15. {sourcecode-1.33.17 → sourcecode-1.33.19}/SECURITY.md +0 -0
  16. {sourcecode-1.33.17 → sourcecode-1.33.19}/raw +0 -0
  17. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/adaptive_scanner.py +0 -0
  18. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/architecture_analyzer.py +0 -0
  19. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/architecture_summary.py +0 -0
  20. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/ast_extractor.py +0 -0
  21. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/cache.py +0 -0
  22. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/canonical_ir.py +0 -0
  23. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/classifier.py +0 -0
  24. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/code_notes_analyzer.py +0 -0
  25. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/confidence_analyzer.py +0 -0
  26. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/context_scorer.py +0 -0
  27. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/context_summarizer.py +0 -0
  28. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/contract_model.py +0 -0
  29. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/contract_pipeline.py +0 -0
  30. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/coverage_parser.py +0 -0
  31. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/dependency_analyzer.py +0 -0
  32. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/__init__.py +0 -0
  33. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/base.py +0 -0
  34. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/csproj_parser.py +0 -0
  35. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/dart.py +0 -0
  36. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/dotnet.py +0 -0
  37. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/elixir.py +0 -0
  38. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/go.py +0 -0
  39. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/heuristic.py +0 -0
  40. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/hybrid.py +0 -0
  41. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/java.py +0 -0
  42. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/jvm_ext.py +0 -0
  43. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/nodejs.py +0 -0
  44. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/parsers.py +0 -0
  45. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/php.py +0 -0
  46. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/project.py +0 -0
  47. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/python.py +0 -0
  48. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/ruby.py +0 -0
  49. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/rust.py +0 -0
  50. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/systems.py +0 -0
  51. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/terraform.py +0 -0
  52. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/detectors/tooling.py +0 -0
  53. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/doc_analyzer.py +0 -0
  54. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/entrypoint_classifier.py +0 -0
  55. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/env_analyzer.py +0 -0
  56. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/error_schema.py +0 -0
  57. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/file_classifier.py +0 -0
  58. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/flow_analyzer.py +0 -0
  59. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/git_analyzer.py +0 -0
  60. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/graph_analyzer.py +0 -0
  61. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/mcp/__init__.py +0 -0
  62. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  63. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  64. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  65. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  66. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  67. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/mcp/orchestrator.py +0 -0
  68. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/mcp/registry.py +0 -0
  69. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/mcp/runner.py +0 -0
  70. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/mcp/server.py +0 -0
  71. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/mcp_nudge.py +0 -0
  72. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/metrics_analyzer.py +0 -0
  73. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/path_filters.py +0 -0
  74. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/pr_comment_renderer.py +0 -0
  75. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/prepare_context.py +0 -0
  76. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/progress.py +0 -0
  77. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/ranking_engine.py +0 -0
  78. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/redactor.py +0 -0
  79. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/relevance_scorer.py +0 -0
  80. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/repo_classifier.py +0 -0
  81. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/repository_ir.py +0 -0
  82. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/runtime_classifier.py +0 -0
  83. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/scanner.py +0 -0
  84. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/schema.py +0 -0
  85. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/semantic_analyzer.py +0 -0
  86. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/serializer.py +0 -0
  87. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/summarizer.py +0 -0
  88. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/telemetry/__init__.py +0 -0
  89. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/telemetry/config.py +0 -0
  90. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/telemetry/consent.py +0 -0
  91. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/telemetry/events.py +0 -0
  92. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/telemetry/filters.py +0 -0
  93. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/telemetry/transport.py +0 -0
  94. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/tree_utils.py +0 -0
  95. {sourcecode-1.33.17 → sourcecode-1.33.19}/src/sourcecode/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.33.17
3
+ Version: 1.33.19
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -39,7 +39,7 @@ Description-Content-Type: text/markdown
39
39
 
40
40
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
41
41
 
42
- ![Version](https://img.shields.io/badge/version-1.33.17-blue)
42
+ ![Version](https://img.shields.io/badge/version-1.33.19-blue)
43
43
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
44
44
 
45
45
  ---
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
4
4
 
5
- ![Version](https://img.shields.io/badge/version-1.33.17-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.33.19-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
7
7
 
8
8
  ---
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.33.17"
7
+ version = "1.33.19"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.33.17"
3
+ __version__ = "1.33.19"
@@ -887,6 +887,14 @@ def main(
887
887
  )
888
888
  raise typer.Exit(code=1)
889
889
 
890
+ # MEJORA-1: --compact is silently ignored when --agent is used.
891
+ # Always warn (not TTY-gated): user explicitly set both flags, one is being ignored.
892
+ if compact and agent:
893
+ typer.echo(
894
+ "[warning] --compact ignored when --agent is used. --agent takes precedence.",
895
+ err=True,
896
+ )
897
+
890
898
  # P0-2 FIX: --full without --compact or --agent has no effect in contract/raw mode.
891
899
  # Warn so the user knows the flag is not doing anything.
892
900
  if full and not compact and not agent:
@@ -1266,7 +1274,7 @@ def main(
1266
1274
  _view_key = ""
1267
1275
  _core_hash = ""
1268
1276
 
1269
- if _cache_hit_content is not None:
1277
+ if _cache_hit_content is not None and not changed_only:
1270
1278
  from sourcecode.serializer import write_output
1271
1279
  if format == "json":
1272
1280
  try:
@@ -1948,7 +1956,10 @@ def main(
1948
1956
  if _uc:
1949
1957
  # Include untracked (new files not yet staged) so new source files
1950
1958
  # are analyzed under --changed-only, not silently treated as "clean".
1951
- _allowed_changed_files = set(_uc.staged) | set(_uc.unstaged) | set(_uc.untracked)
1959
+ # Exclude directory entries (trailing "/") e.g. untracked tool
1960
+ # cache dirs like ".sourcecode-cache/" are dirs not source files.
1961
+ _uc_files = {p for p in _uc.untracked if not p.endswith("/")}
1962
+ _allowed_changed_files = set(_uc.staged) | set(_uc.unstaged) | _uc_files
1952
1963
  if not _allowed_changed_files:
1953
1964
  # Git is available and confirms no uncommitted changes.
1954
1965
  # Do NOT fall back to a full scan — that would silently produce
@@ -1967,9 +1978,11 @@ def main(
1967
1978
  changed_only = False
1968
1979
  if _git_confirmed_clean:
1969
1980
  _nc_payload = json.dumps({
1981
+ "changed_files_count": 0,
1970
1982
  "changed_files": [],
1971
1983
  "message": "no uncommitted changes detected",
1972
1984
  "analysis_scope": "empty",
1985
+ "note": "No uncommitted changes detected. No output produced — use without --changed-only for full context.",
1973
1986
  "_meta": {"changed_only": True},
1974
1987
  }, ensure_ascii=False)
1975
1988
  write_output(_nc_payload, output=output)
@@ -2075,8 +2088,9 @@ def main(
2075
2088
  if not no_redact:
2076
2089
  data = redact_dict(data)
2077
2090
  # P0-1: Apply output budget — safety net for large repos.
2091
+ # Skip budget when writing to a file (no size constraint); warn on stdout.
2078
2092
  from sourcecode.output_budget import trim_to_budget as _trim, BUDGET_AGENT
2079
- data = _trim(data, BUDGET_AGENT, label="agent")
2093
+ data = _trim(data, BUDGET_AGENT, label="agent", skip=(output is not None), warn_stderr=(output is None))
2080
2094
  # FIX-P0-2: agent mode must honour --format yaml (previously always emitted JSON).
2081
2095
  if format == "yaml":
2082
2096
  from io import StringIO
@@ -2119,8 +2133,9 @@ def main(
2119
2133
  if not no_redact:
2120
2134
  data = redact_dict(data)
2121
2135
  # P0-1: Apply output budget — safety net for large repos.
2136
+ # Skip budget when writing to a file (no size constraint); warn on stdout.
2122
2137
  from sourcecode.output_budget import trim_to_budget as _trim_c, BUDGET_COMPACT
2123
- data = _trim_c(data, BUDGET_COMPACT, label="compact")
2138
+ data = _trim_c(data, BUDGET_COMPACT, label="compact", skip=(output is not None), warn_stderr=(output is None))
2124
2139
  if format == "yaml":
2125
2140
  from io import StringIO
2126
2141
  from ruamel.yaml import YAML as _YAML
@@ -2498,8 +2513,11 @@ def prepare_context_cmd(
2498
2513
  # Pro gate: generate-tests and delta require an active Pro license.
2499
2514
  _PRO_TASKS: frozenset[str] = frozenset({"generate-tests", "delta"})
2500
2515
  if task in _PRO_TASKS:
2501
- from sourcecode.license import require_pro as _require_pro
2502
- _require_pro(task)
2516
+ from sourcecode.license import require_feature as _require_feature
2517
+ _extra: dict = {}
2518
+ if task == "delta":
2519
+ _extra["free_tier_alternative"] = "sourcecode prepare-context review-pr --since <ref>"
2520
+ _require_feature(task, extra_fields=_extra if _extra else None)
2503
2521
 
2504
2522
  # Validate --format: only "json" and "github-comment" are valid for prepare-context.
2505
2523
  # "yaml" is intentionally NOT supported here (use main command for yaml output).
@@ -3297,6 +3315,10 @@ def impact_cmd(
3297
3315
  - transactional_boundaries_touched — @Transactional classes in the call chain
3298
3316
  - risk_score / risk_level — quantified change risk
3299
3317
 
3318
+ \b
3319
+ NOTE: This feature requires a Pro license. Run 'sourcecode license' for details.
3320
+ Upgrade: sourcecode activate <license_key>
3321
+
3300
3322
  \b
3301
3323
  Examples:
3302
3324
  sourcecode impact UserService
@@ -4323,10 +4345,19 @@ def mcp_status() -> None:
4323
4345
  typer.echo(f" Fix: sourcecode mcp init --target {client.slug}")
4324
4346
  typer.echo("")
4325
4347
 
4348
+ # Build config state map for cross-check in Stage 3.
4349
+ _configured_clients: set[str] = set()
4350
+ for _c in clients:
4351
+ if _c.app_installed:
4352
+ _cfg = applier.read_config(_c.config_path)
4353
+ if applier.is_installed(_cfg):
4354
+ _configured_clients.add(_c.slug)
4355
+
4326
4356
  # Stage 3: Process liveness — is the client app currently running?
4327
4357
  # This is independent from config: a running app may still need restart to pick up config.
4328
4358
  typer.echo("Runtime (client app process running?)")
4329
4359
  any_installed = any(c.app_installed for c in clients)
4360
+ _action_required: list[str] = []
4330
4361
  if not any_installed:
4331
4362
  typer.echo(" (no client apps found — nothing to check)")
4332
4363
  else:
@@ -4335,11 +4366,18 @@ def mcp_status() -> None:
4335
4366
  continue
4336
4367
  if is_client_running(client):
4337
4368
  typer.echo(f" {client.name:<20} ✓ running")
4369
+ if client.slug not in _configured_clients:
4370
+ _action_required.append(client.name)
4338
4371
  else:
4339
4372
  typer.echo(f" {client.name:<20} ✗ not running")
4340
4373
  typer.echo(f" Fix: open {client.name}, then run sourcecode mcp status")
4341
4374
 
4342
4375
  typer.echo(sep)
4376
+ if _action_required:
4377
+ for _name in _action_required:
4378
+ typer.echo(f" ⚠ ACTION REQUIRED: {_name} is running but sourcecode is not configured.")
4379
+ typer.echo(" Run: sourcecode mcp init")
4380
+ typer.echo("")
4343
4381
  typer.echo(" Note: 'configured' and 'running' are checked independently.")
4344
4382
  typer.echo(" A running app still needs restart after first-time config.")
4345
4383
  typer.echo(" Path: repo_path must use forward slashes: C:/Users/... or /unix/path")
@@ -206,7 +206,10 @@ def can_use(feature_name: str) -> bool:
206
206
  return is_pro
207
207
 
208
208
 
209
- def require_feature(feature_name: str) -> None:
209
+ def require_feature(
210
+ feature_name: str,
211
+ extra_fields: Optional[dict] = None,
212
+ ) -> None:
210
213
  """Exit with a clean upgrade prompt when feature_name requires Pro.
211
214
 
212
215
  Re-validates stale cached license before gating (once per 24 h, online).
@@ -214,6 +217,10 @@ def require_feature(feature_name: str) -> None:
214
217
  Writes human-readable context to stderr (terminal UX) and a JSON error
215
218
  to stdout (backward-compatible machine-readable format).
216
219
 
220
+ Args:
221
+ extra_fields: Optional extra keys merged into the JSON error payload
222
+ (e.g. ``{"free_tier_alternative": "..."}``)
223
+
217
224
  Example:
218
225
  from sourcecode.license import require_feature
219
226
  require_feature("impact")
@@ -241,7 +248,7 @@ def require_feature(feature_name: str) -> None:
241
248
  sys.stderr.flush()
242
249
 
243
250
  # JSON on stdout — backward-compatible for CI / MCP consumers
244
- payload = {
251
+ payload: dict = {
245
252
  "error": "pro_required",
246
253
  "feature": feature_name,
247
254
  "message": (
@@ -250,9 +257,11 @@ def require_feature(feature_name: str) -> None:
250
257
  ),
251
258
  "upgrade_hint": "sourcecode activate <license_key>",
252
259
  }
260
+ if extra_fields:
261
+ payload.update(extra_fields)
253
262
  sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
254
263
  sys.stdout.flush()
255
- sys.exit(1)
264
+ sys.exit(2) # exit 2 = Pro feature required (0=ok, 1=runtime error, 2=license required)
256
265
 
257
266
 
258
267
  def require_pro(feature_name: str) -> None:
@@ -18,6 +18,7 @@ Always preserved: project_type, project_summary, architecture_summary, task,
18
18
  from __future__ import annotations
19
19
 
20
20
  import json
21
+ import sys
21
22
  from typing import Any
22
23
 
23
24
 
@@ -78,20 +79,36 @@ def _serialized_size(data: Any) -> int:
78
79
  return len(json.dumps(data, ensure_ascii=False).encode("utf-8"))
79
80
 
80
81
 
81
- def trim_to_budget(data: dict, budget_bytes: int, *, label: str = "") -> dict:
82
+ def trim_to_budget(
83
+ data: dict,
84
+ budget_bytes: int,
85
+ *,
86
+ label: str = "",
87
+ skip: bool = False,
88
+ warn_stderr: bool = False,
89
+ ) -> dict:
82
90
  """Progressively trim *data* to fit within *budget_bytes*.
83
91
 
84
92
  Preserves the highest-value sections. Adds ``_budget_note`` when trimming
85
93
  occurs so callers can surface it to users.
86
94
 
87
- Returns data unchanged if already within budget.
95
+ Args:
96
+ skip: When True (e.g. writing to a file), skip all trimming and return
97
+ data unchanged with no ``_budget_note``.
98
+ warn_stderr: When True, emit a WARNING line to stderr before returning
99
+ if trimming was applied. Used for stdout output so users
100
+ see the warning before the JSON payload.
101
+
102
+ Returns data unchanged if already within budget or ``skip=True``.
88
103
  """
89
- if _serialized_size(data) <= budget_bytes:
104
+ if skip or _serialized_size(data) <= budget_bytes:
90
105
  return data
91
106
 
92
107
  result: dict = dict(data)
93
108
  original_size = _serialized_size(result)
94
109
  trimmed_sections: list[str] = []
110
+ # Track original counts for total_omitted_items calculation.
111
+ _original_counts: dict[str, int] = {}
95
112
 
96
113
  for top_key, inner_key, max_items in _TRIM_SCHEDULE:
97
114
  if _serialized_size(result) <= budget_bytes:
@@ -107,9 +124,12 @@ def trim_to_budget(data: dict, budget_bytes: int, *, label: str = "") -> dict:
107
124
  if max_items == 0:
108
125
  if top_key in _ALWAYS_KEEP:
109
126
  continue
127
+ if isinstance(section_val, list):
128
+ _original_counts[top_key] = _original_counts.get(top_key, len(section_val))
110
129
  del result[top_key]
111
130
  trimmed_sections.append(f"{top_key}:dropped")
112
131
  elif isinstance(section_val, list) and len(section_val) > max_items:
132
+ _original_counts[top_key] = _original_counts.get(top_key, len(section_val))
113
133
  result[top_key] = section_val[:max_items]
114
134
  trimmed_sections.append(f"{top_key}≤{max_items}")
115
135
  else:
@@ -119,19 +139,29 @@ def trim_to_budget(data: dict, budget_bytes: int, *, label: str = "") -> dict:
119
139
  if inner_key not in section_val:
120
140
  continue
121
141
  inner_val = section_val[inner_key]
142
+ _inner_key = f"{top_key}.{inner_key}"
122
143
  if max_items == 0:
144
+ if isinstance(inner_val, list):
145
+ _original_counts[_inner_key] = _original_counts.get(_inner_key, len(inner_val))
123
146
  new_sec = dict(section_val)
124
147
  del new_sec[inner_key]
125
148
  result[top_key] = new_sec
126
- trimmed_sections.append(f"{top_key}.{inner_key}:dropped")
149
+ trimmed_sections.append(f"{_inner_key}:dropped")
127
150
  elif isinstance(inner_val, list) and len(inner_val) > max_items:
151
+ _original_counts[_inner_key] = _original_counts.get(_inner_key, len(inner_val))
128
152
  new_sec = dict(section_val)
129
153
  new_sec[inner_key] = inner_val[:max_items]
130
154
  result[top_key] = new_sec
131
- trimmed_sections.append(f"{top_key}.{inner_key}≤{max_items}")
155
+ trimmed_sections.append(f"{_inner_key}≤{max_items}")
132
156
 
133
157
  final_size = _serialized_size(result)
134
158
  if trimmed_sections:
159
+ # Build human-readable section summary for note/warning.
160
+ _section_summary_parts: list[str] = []
161
+ for _sk, _orig in _original_counts.items():
162
+ _cur_key = _sk.replace(".", "/") # normalize for display
163
+ _section_summary_parts.append(f"{_sk} ({_orig} total)")
164
+
135
165
  note = (
136
166
  f"Output trimmed {original_size // 1024}KB → {final_size // 1024}KB "
137
167
  f"(budget {budget_bytes // 1024}KB). "
@@ -142,6 +172,51 @@ def trim_to_budget(data: dict, budget_bytes: int, *, label: str = "") -> dict:
142
172
  note = f"[{label}] {note}"
143
173
  result["_budget_note"] = note
144
174
 
175
+ # Compute total omitted items across all truncated lists.
176
+ total_omitted = 0
177
+ for _sk, _orig in _original_counts.items():
178
+ if ":dropped" in "".join(s for s in trimmed_sections if _sk in s):
179
+ total_omitted += _orig
180
+ else:
181
+ # Find the last max_items cap applied to this key.
182
+ _caps = [
183
+ int(s.split("≤")[1])
184
+ for s in trimmed_sections
185
+ if s.startswith(_sk + "≤")
186
+ ]
187
+ _cap = min(_caps) if _caps else 0
188
+ total_omitted += max(0, _orig - _cap)
189
+
190
+ result["_truncation_summary"] = {
191
+ "total_omitted_items": total_omitted,
192
+ "original_size_kb": original_size // 1024,
193
+ "final_size_kb": final_size // 1024,
194
+ "budget_kb": budget_bytes // 1024,
195
+ }
196
+
197
+ if warn_stderr:
198
+ # Build per-section counts for the warning line.
199
+ _warn_sections: list[str] = []
200
+ for _sk, _orig in _original_counts.items():
201
+ _caps = [
202
+ int(s.split("≤")[1])
203
+ for s in trimmed_sections
204
+ if s.startswith(_sk + "≤")
205
+ ]
206
+ _shown = min(_caps) if _caps else 0
207
+ _warn_sections.append(f"{_sk} ({_shown}/{_orig})")
208
+ _warn_line = (
209
+ f"WARNING: Output will be trimmed "
210
+ f"({original_size // 1024}KB → {final_size // 1024}KB, "
211
+ f"budget {budget_bytes // 1024}KB). "
212
+ f"Affected: {', '.join(_warn_sections)}. "
213
+ "Use --output <file> to capture full output.\n"
214
+ )
215
+ if label:
216
+ _warn_line = f"[{label}] {_warn_line}"
217
+ sys.stderr.write(_warn_line)
218
+ sys.stderr.flush()
219
+
145
220
  return result
146
221
 
147
222
 
@@ -350,14 +350,15 @@ def _current_git_head(repo_root: Path) -> str:
350
350
 
351
351
 
352
352
  def _has_uncommitted_changes(repo_root: Path) -> bool:
353
- """Return True if working tree has staged or unstaged changes.
353
+ """Return True if working tree has staged or unstaged changes to tracked files.
354
354
 
355
- Uses ``git status --porcelain`` — any non-empty output means the working
356
- tree diverges from HEAD. Returns False on any error (non-git dirs, etc.).
355
+ Uses ``git status --porcelain --untracked-files=no`` so that untracked
356
+ files (e.g. legacy .sourcecode-cache/ directories) do not produce false
357
+ positives. Returns False on any error (non-git dirs, etc.).
357
358
  """
358
359
  try:
359
360
  result = subprocess.run(
360
- ["git", "-C", str(repo_root), "status", "--porcelain"],
361
+ ["git", "-C", str(repo_root), "status", "--porcelain", "--untracked-files=no"],
361
362
  capture_output=True,
362
363
  text=True,
363
364
  timeout=2,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes