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.
- {sourcecode-1.33.18 → sourcecode-1.33.19}/PKG-INFO +2 -2
- {sourcecode-1.33.18 → sourcecode-1.33.19}/README.md +1 -1
- {sourcecode-1.33.18 → sourcecode-1.33.19}/pyproject.toml +1 -1
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/cli.py +39 -4
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/license.py +12 -3
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/output_budget.py +80 -5
- {sourcecode-1.33.18 → sourcecode-1.33.19}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/.gitignore +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/.ruff.toml +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/CHANGELOG.md +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/CONTRIBUTING.md +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/LICENSE +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/SECURITY.md +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/raw +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.33.18 → sourcecode-1.33.19}/src/sourcecode/tree_utils.py +0 -0
- {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.
|
|
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
|
-

|
|
43
43
|

|
|
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
|
-

|
|
6
6
|

|
|
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.
|
|
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"
|
|
@@ -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
|
|
2505
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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"{
|
|
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"{
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|