sourcecode 1.33.18__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.18 → sourcecode-1.33.19}/PKG-INFO +2 -2
  2. {sourcecode-1.33.18 → sourcecode-1.33.19}/README.md +1 -1
  3. {sourcecode-1.33.18 → sourcecode-1.33.19}/pyproject.toml +1 -1
  4. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/cli.py +39 -4
  6. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/license.py +12 -3
  7. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/output_budget.py +80 -5
  8. {sourcecode-1.33.18 → sourcecode-1.33.19}/.github/workflows/build-windows.yml +0 -0
  9. {sourcecode-1.33.18 → sourcecode-1.33.19}/.gitignore +0 -0
  10. {sourcecode-1.33.18 → sourcecode-1.33.19}/.ruff.toml +0 -0
  11. {sourcecode-1.33.18 → sourcecode-1.33.19}/CHANGELOG.md +0 -0
  12. {sourcecode-1.33.18 → sourcecode-1.33.19}/CONTRIBUTING.md +0 -0
  13. {sourcecode-1.33.18 → sourcecode-1.33.19}/LICENSE +0 -0
  14. {sourcecode-1.33.18 → sourcecode-1.33.19}/SECURITY.md +0 -0
  15. {sourcecode-1.33.18 → sourcecode-1.33.19}/raw +0 -0
  16. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/adaptive_scanner.py +0 -0
  17. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/architecture_analyzer.py +0 -0
  18. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/architecture_summary.py +0 -0
  19. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/ast_extractor.py +0 -0
  20. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/cache.py +0 -0
  21. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/canonical_ir.py +0 -0
  22. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/classifier.py +0 -0
  23. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/code_notes_analyzer.py +0 -0
  24. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/confidence_analyzer.py +0 -0
  25. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/context_scorer.py +0 -0
  26. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/context_summarizer.py +0 -0
  27. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/contract_model.py +0 -0
  28. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/contract_pipeline.py +0 -0
  29. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/coverage_parser.py +0 -0
  30. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/dependency_analyzer.py +0 -0
  31. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/__init__.py +0 -0
  32. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/base.py +0 -0
  33. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/csproj_parser.py +0 -0
  34. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/dart.py +0 -0
  35. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/dotnet.py +0 -0
  36. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/elixir.py +0 -0
  37. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/go.py +0 -0
  38. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/heuristic.py +0 -0
  39. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/hybrid.py +0 -0
  40. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/java.py +0 -0
  41. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/jvm_ext.py +0 -0
  42. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/nodejs.py +0 -0
  43. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/parsers.py +0 -0
  44. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/php.py +0 -0
  45. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/project.py +0 -0
  46. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/python.py +0 -0
  47. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/ruby.py +0 -0
  48. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/rust.py +0 -0
  49. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/systems.py +0 -0
  50. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/terraform.py +0 -0
  51. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/tooling.py +0 -0
  52. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/doc_analyzer.py +0 -0
  53. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/entrypoint_classifier.py +0 -0
  54. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/env_analyzer.py +0 -0
  55. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/error_schema.py +0 -0
  56. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/file_classifier.py +0 -0
  57. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/flow_analyzer.py +0 -0
  58. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/git_analyzer.py +0 -0
  59. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/graph_analyzer.py +0 -0
  60. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/__init__.py +0 -0
  61. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  62. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  63. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  64. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  65. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  66. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/orchestrator.py +0 -0
  67. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/registry.py +0 -0
  68. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/runner.py +0 -0
  69. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/server.py +0 -0
  70. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp_nudge.py +0 -0
  71. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/metrics_analyzer.py +0 -0
  72. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/path_filters.py +0 -0
  73. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/pr_comment_renderer.py +0 -0
  74. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/prepare_context.py +0 -0
  75. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/progress.py +0 -0
  76. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/ranking_engine.py +0 -0
  77. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/redactor.py +0 -0
  78. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/relevance_scorer.py +0 -0
  79. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/repo_classifier.py +0 -0
  80. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/repository_ir.py +0 -0
  81. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/ris.py +0 -0
  82. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/runtime_classifier.py +0 -0
  83. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/scanner.py +0 -0
  84. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/schema.py +0 -0
  85. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/semantic_analyzer.py +0 -0
  86. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/serializer.py +0 -0
  87. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/summarizer.py +0 -0
  88. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/telemetry/__init__.py +0 -0
  89. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/telemetry/config.py +0 -0
  90. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/telemetry/consent.py +0 -0
  91. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/telemetry/events.py +0 -0
  92. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/telemetry/filters.py +0 -0
  93. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/telemetry/transport.py +0 -0
  94. {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/tree_utils.py +0 -0
  95. {sourcecode-1.33.18 → 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.18
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.18-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.18-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.18"
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.18"
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:
@@ -1970,9 +1978,11 @@ def main(
1970
1978
  changed_only = False
1971
1979
  if _git_confirmed_clean:
1972
1980
  _nc_payload = json.dumps({
1981
+ "changed_files_count": 0,
1973
1982
  "changed_files": [],
1974
1983
  "message": "no uncommitted changes detected",
1975
1984
  "analysis_scope": "empty",
1985
+ "note": "No uncommitted changes detected. No output produced — use without --changed-only for full context.",
1976
1986
  "_meta": {"changed_only": True},
1977
1987
  }, ensure_ascii=False)
1978
1988
  write_output(_nc_payload, output=output)
@@ -2078,8 +2088,9 @@ def main(
2078
2088
  if not no_redact:
2079
2089
  data = redact_dict(data)
2080
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.
2081
2092
  from sourcecode.output_budget import trim_to_budget as _trim, BUDGET_AGENT
2082
- 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))
2083
2094
  # FIX-P0-2: agent mode must honour --format yaml (previously always emitted JSON).
2084
2095
  if format == "yaml":
2085
2096
  from io import StringIO
@@ -2122,8 +2133,9 @@ def main(
2122
2133
  if not no_redact:
2123
2134
  data = redact_dict(data)
2124
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.
2125
2137
  from sourcecode.output_budget import trim_to_budget as _trim_c, BUDGET_COMPACT
2126
- 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))
2127
2139
  if format == "yaml":
2128
2140
  from io import StringIO
2129
2141
  from ruamel.yaml import YAML as _YAML
@@ -2501,8 +2513,11 @@ def prepare_context_cmd(
2501
2513
  # Pro gate: generate-tests and delta require an active Pro license.
2502
2514
  _PRO_TASKS: frozenset[str] = frozenset({"generate-tests", "delta"})
2503
2515
  if task in _PRO_TASKS:
2504
- from sourcecode.license import require_pro as _require_pro
2505
- _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)
2506
2521
 
2507
2522
  # Validate --format: only "json" and "github-comment" are valid for prepare-context.
2508
2523
  # "yaml" is intentionally NOT supported here (use main command for yaml output).
@@ -3300,6 +3315,10 @@ def impact_cmd(
3300
3315
  - transactional_boundaries_touched — @Transactional classes in the call chain
3301
3316
  - risk_score / risk_level — quantified change risk
3302
3317
 
3318
+ \b
3319
+ NOTE: This feature requires a Pro license. Run 'sourcecode license' for details.
3320
+ Upgrade: sourcecode activate <license_key>
3321
+
3303
3322
  \b
3304
3323
  Examples:
3305
3324
  sourcecode impact UserService
@@ -4326,10 +4345,19 @@ def mcp_status() -> None:
4326
4345
  typer.echo(f" Fix: sourcecode mcp init --target {client.slug}")
4327
4346
  typer.echo("")
4328
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
+
4329
4356
  # Stage 3: Process liveness — is the client app currently running?
4330
4357
  # This is independent from config: a running app may still need restart to pick up config.
4331
4358
  typer.echo("Runtime (client app process running?)")
4332
4359
  any_installed = any(c.app_installed for c in clients)
4360
+ _action_required: list[str] = []
4333
4361
  if not any_installed:
4334
4362
  typer.echo(" (no client apps found — nothing to check)")
4335
4363
  else:
@@ -4338,11 +4366,18 @@ def mcp_status() -> None:
4338
4366
  continue
4339
4367
  if is_client_running(client):
4340
4368
  typer.echo(f" {client.name:<20} ✓ running")
4369
+ if client.slug not in _configured_clients:
4370
+ _action_required.append(client.name)
4341
4371
  else:
4342
4372
  typer.echo(f" {client.name:<20} ✗ not running")
4343
4373
  typer.echo(f" Fix: open {client.name}, then run sourcecode mcp status")
4344
4374
 
4345
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("")
4346
4381
  typer.echo(" Note: 'configured' and 'running' are checked independently.")
4347
4382
  typer.echo(" A running app still needs restart after first-time config.")
4348
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
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes