terse-mcp 0.3.1__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 (94) hide show
  1. terse_mcp-0.3.1/.github/workflows/release.yml +71 -0
  2. terse_mcp-0.3.1/.github/workflows/tests.yml +42 -0
  3. terse_mcp-0.3.1/.gitignore +38 -0
  4. terse_mcp-0.3.1/.gitleaksignore +2 -0
  5. terse_mcp-0.3.1/.runechoguardignore +106 -0
  6. terse_mcp-0.3.1/BENCHMARKS.md +151 -0
  7. terse_mcp-0.3.1/CHANGELOG.md +58 -0
  8. terse_mcp-0.3.1/LICENSE +21 -0
  9. terse_mcp-0.3.1/PKG-INFO +345 -0
  10. terse_mcp-0.3.1/README.md +320 -0
  11. terse_mcp-0.3.1/TECHNICAL.md +389 -0
  12. terse_mcp-0.3.1/USAGE.md +648 -0
  13. terse_mcp-0.3.1/VERIFY.md +92 -0
  14. terse_mcp-0.3.1/corpus/.gitkeep +0 -0
  15. terse_mcp-0.3.1/docs/architecture.svg +661 -0
  16. terse_mcp-0.3.1/policy.example.json +38 -0
  17. terse_mcp-0.3.1/pyproject.toml +97 -0
  18. terse_mcp-0.3.1/scripts/bench/benchmark.py +139 -0
  19. terse_mcp-0.3.1/scripts/bench/corpus/gh_commits.json +1 -0
  20. terse_mcp-0.3.1/scripts/bench/corpus/gh_commits_flat.json +1 -0
  21. terse_mcp-0.3.1/scripts/bench/corpus/gh_dir_listing.json +1 -0
  22. terse_mcp-0.3.1/scripts/bench/corpus/gh_issues.json +1 -0
  23. terse_mcp-0.3.1/scripts/bench/corpus/gh_labels.json +1 -0
  24. terse_mcp-0.3.1/scripts/bench/corpus/gh_pulls.json +1 -0
  25. terse_mcp-0.3.1/scripts/bench/corpus/gh_rate_limit.json +1 -0
  26. terse_mcp-0.3.1/scripts/bench/corpus/gh_repo_single.json +1 -0
  27. terse_mcp-0.3.1/scripts/bench/corpus/gh_workflow_runs.json +1 -0
  28. terse_mcp-0.3.1/scripts/bench/diff_demo.py +149 -0
  29. terse_mcp-0.3.1/scripts/bench/fetch_corpus.sh +17 -0
  30. terse_mcp-0.3.1/scripts/bench/package-lock.json +19 -0
  31. terse_mcp-0.3.1/scripts/bench/package.json +8 -0
  32. terse_mcp-0.3.1/scripts/bench/toon_encode.mjs +16 -0
  33. terse_mcp-0.3.1/scripts/bench/width_sweep.py +76 -0
  34. terse_mcp-0.3.1/scripts/gen_stress_corpus.py +116 -0
  35. terse_mcp-0.3.1/src/terse/__init__.py +27 -0
  36. terse_mcp-0.3.1/src/terse/__main__.py +8 -0
  37. terse_mcp-0.3.1/src/terse/_secure_io.py +76 -0
  38. terse_mcp-0.3.1/src/terse/_version.py +24 -0
  39. terse_mcp-0.3.1/src/terse/capture.py +236 -0
  40. terse_mcp-0.3.1/src/terse/cli.py +1289 -0
  41. terse_mcp-0.3.1/src/terse/dropeval.py +388 -0
  42. terse_mcp-0.3.1/src/terse/fluency/__init__.py +102 -0
  43. terse_mcp-0.3.1/src/terse/fluency/answerers.py +57 -0
  44. terse_mcp-0.3.1/src/terse/fluency/harnesses.py +360 -0
  45. terse_mcp-0.3.1/src/terse/fluency/pack.py +112 -0
  46. terse_mcp-0.3.1/src/terse/fluency/questions.py +371 -0
  47. terse_mcp-0.3.1/src/terse/fluency/scoring.py +97 -0
  48. terse_mcp-0.3.1/src/terse/history.py +81 -0
  49. terse_mcp-0.3.1/src/terse/html_report.py +483 -0
  50. terse_mcp-0.3.1/src/terse/install_mcp.py +518 -0
  51. terse_mcp-0.3.1/src/terse/lossy.py +291 -0
  52. terse_mcp-0.3.1/src/terse/measure.py +125 -0
  53. terse_mcp-0.3.1/src/terse/multiproxy.py +1127 -0
  54. terse_mcp-0.3.1/src/terse/policy.py +385 -0
  55. terse_mcp-0.3.1/src/terse/policy_gen.py +296 -0
  56. terse_mcp-0.3.1/src/terse/probes.py +249 -0
  57. terse_mcp-0.3.1/src/terse/proxy.py +1037 -0
  58. terse_mcp-0.3.1/src/terse/report.py +1018 -0
  59. terse_mcp-0.3.1/src/terse/stats.py +305 -0
  60. terse_mcp-0.3.1/src/terse/terminal_report.py +253 -0
  61. terse_mcp-0.3.1/src/terse/text_diff.py +165 -0
  62. terse_mcp-0.3.1/src/terse/tokenize.py +59 -0
  63. terse_mcp-0.3.1/src/terse/transforms.py +625 -0
  64. terse_mcp-0.3.1/src/terse/transport.py +412 -0
  65. terse_mcp-0.3.1/tests/conftest.py +14 -0
  66. terse_mcp-0.3.1/tests/fake_mcp_server.py +71 -0
  67. terse_mcp-0.3.1/tests/test_capture.py +72 -0
  68. terse_mcp-0.3.1/tests/test_cli.py +665 -0
  69. terse_mcp-0.3.1/tests/test_codec_hotpath.py +141 -0
  70. terse_mcp-0.3.1/tests/test_diff.py +101 -0
  71. terse_mcp-0.3.1/tests/test_diff_soak.py +267 -0
  72. terse_mcp-0.3.1/tests/test_drop.py +165 -0
  73. terse_mcp-0.3.1/tests/test_dropeval.py +264 -0
  74. terse_mcp-0.3.1/tests/test_fluency.py +646 -0
  75. terse_mcp-0.3.1/tests/test_fuzz.py +142 -0
  76. terse_mcp-0.3.1/tests/test_history.py +82 -0
  77. terse_mcp-0.3.1/tests/test_html_report.py +113 -0
  78. terse_mcp-0.3.1/tests/test_install_mcp.py +789 -0
  79. terse_mcp-0.3.1/tests/test_lossy.py +101 -0
  80. terse_mcp-0.3.1/tests/test_measure.py +137 -0
  81. terse_mcp-0.3.1/tests/test_multiproxy.py +1100 -0
  82. terse_mcp-0.3.1/tests/test_policy.py +331 -0
  83. terse_mcp-0.3.1/tests/test_policy_gen.py +200 -0
  84. terse_mcp-0.3.1/tests/test_probes.py +203 -0
  85. terse_mcp-0.3.1/tests/test_proxy.py +1202 -0
  86. terse_mcp-0.3.1/tests/test_report.py +70 -0
  87. terse_mcp-0.3.1/tests/test_roundtrip.py +164 -0
  88. terse_mcp-0.3.1/tests/test_secure_io.py +186 -0
  89. terse_mcp-0.3.1/tests/test_stats.py +264 -0
  90. terse_mcp-0.3.1/tests/test_terminal_report.py +198 -0
  91. terse_mcp-0.3.1/tests/test_text_diff.py +106 -0
  92. terse_mcp-0.3.1/tests/test_transport.py +414 -0
  93. terse_mcp-0.3.1/tests/test_verify.py +90 -0
  94. terse_mcp-0.3.1/uv.lock +673 -0
@@ -0,0 +1,71 @@
1
+ name: release
2
+ # Fires when a version tag is pushed (e.g. `v0.1.0`). Builds the sdist+wheel, cuts a
3
+ # GitHub Release, and publishes to PyPI via Trusted Publishing (OIDC — no stored token).
4
+ # The tag IS the version (hatch-vcs) — a build here resolves to the tag, byte-for-byte.
5
+ #
6
+ # The distribution name is `terse-mcp` (the bare `terse` is taken on PyPI); it normalizes
7
+ # to `terse_mcp` in wheel filenames (e.g. v0.3.0 -> terse_mcp-0.3.0-*.whl).
8
+ on:
9
+ push:
10
+ tags:
11
+ - "v*"
12
+
13
+ permissions: {} # least privilege — each job opts into exactly what it needs
14
+
15
+ jobs:
16
+ release:
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ contents: write # create the GitHub Release + upload artifacts; nothing more
20
+ steps:
21
+ # fetch-depth: 0 is REQUIRED — hatch-vcs derives the version from `git describe`,
22
+ # which needs full history + the tag. A shallow checkout yields a wrong/dev version.
23
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
24
+ with:
25
+ fetch-depth: 0
26
+ - uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5
27
+ with:
28
+ enable-cache: true
29
+ - name: Build sdist + wheel
30
+ run: uv build
31
+ - name: Sanity-check the built version matches the tag
32
+ # Fail loudly if hatch-vcs and the tag disagree — the artifact name must contain
33
+ # the tag without its leading `v`.
34
+ run: |
35
+ expected="${GITHUB_REF_NAME#v}"
36
+ if ! ls dist/terse_mcp-"${expected}"-*.whl >/dev/null 2>&1; then
37
+ echo "::error::built wheel does not match tag ${GITHUB_REF_NAME} (expected version ${expected})"; ls -1 dist/; exit 1
38
+ fi
39
+ echo "built version ${expected} matches tag ${GITHUB_REF_NAME}"
40
+ # Build ONCE — the same artifacts go to both the GitHub Release and PyPI.
41
+ - name: Upload dist for the publish job
42
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+ if-no-files-found: error
47
+ - name: Publish GitHub Release
48
+ env:
49
+ GH_TOKEN: ${{ github.token }}
50
+ run: gh release create "${GITHUB_REF_NAME}" dist/* --generate-notes --title "${GITHUB_REF_NAME}"
51
+
52
+ pypi-publish:
53
+ needs: release
54
+ runs-on: ubuntu-latest
55
+ # `pypi` MUST match the Trusted Publisher's environment on PyPI, or the OIDC exchange is
56
+ # rejected. Gate it with a required reviewer in Settings -> Environments for human approval.
57
+ environment:
58
+ name: pypi
59
+ url: https://pypi.org/project/terse-mcp/
60
+ permissions:
61
+ id-token: write # OIDC is the ONLY credential — no PyPI token is stored anywhere
62
+ steps:
63
+ - name: Download the built dist
64
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
65
+ with:
66
+ name: dist
67
+ path: dist/
68
+ # Trusted Publishing is automatic (no `with:` creds); PEP 740 attestations default on;
69
+ # uploads everything in dist/ (packages-dir defaults to dist/).
70
+ - name: Publish to PyPI (Trusted Publishing + attestations)
71
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
@@ -0,0 +1,42 @@
1
+ name: tests
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ permissions:
7
+ contents: read # least privilege — the workflow only reads the repo
8
+ jobs:
9
+ lint:
10
+ # Deterministic and version-independent — its own job so a lint failure reports as
11
+ # one named check, not 3 identical red X's under the pytest matrix.
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
15
+ with:
16
+ fetch-depth: 0 # hatch-vcs derives the version from git history + tags
17
+ - uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5
18
+ with:
19
+ enable-cache: true
20
+ - run: uv sync --dev # no anthropic extra: keeps CI keyless, matches design
21
+ - run: uv run ruff check .
22
+ - run: uv run mypy # checked annotations (audit fix #1); config in pyproject
23
+ pytest:
24
+ runs-on: ubuntu-latest
25
+ strategy:
26
+ fail-fast: false
27
+ matrix:
28
+ # Floor (the requires-python promise) through current stable. Test both ends so
29
+ # a ">=3.11" claim is actually verified, not just the dev default. 3.14 is left
30
+ # off until tiktoken ships wheels for it (else CI compiles the Rust ext + fails).
31
+ python-version: ["3.11", "3.12", "3.13"]
32
+ env:
33
+ UV_PYTHON: ${{ matrix.python-version }}
34
+ steps:
35
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
36
+ with:
37
+ fetch-depth: 0 # hatch-vcs derives the version from git history + tags
38
+ - uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5
39
+ with:
40
+ enable-cache: true
41
+ - run: uv sync --dev # no anthropic extra: keeps CI keyless, matches design
42
+ - run: uv run pytest -q
@@ -0,0 +1,38 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ *.egg-info/
6
+ .pytest_cache/
7
+
8
+ # uv
9
+ # uv.lock IS committed (pinned deps) — do not ignore it.
10
+
11
+ # Captured corpus payloads may contain tool output; keep the dir, ignore contents.
12
+ corpus/*
13
+ !corpus/.gitkeep
14
+
15
+ # Synthetic stress corpus for the fluency eval — derived; regenerate with
16
+ # `python scripts/gen_stress_corpus.py`.
17
+ corpus-stress/
18
+
19
+ # Ad-hoc eval corpora for cross-call diffing (synthetic pair + real gh captures) — derived.
20
+ corpus-diff/
21
+ corpus-real/
22
+
23
+ # Spike reports
24
+ reports/
25
+
26
+ # runecho stray index dirs (created if `runecho-ir` is run with cwd here)
27
+ .ai/
28
+
29
+ # Ad-hoc experiment harnesses (e.g. the fidelity probe) — not part of the lib
30
+ scratch_fidelity/
31
+ .codegraph
32
+ .coverage
33
+
34
+ # hatch-vcs build-time version file (generated from the git tag; never committed)
35
+ src/terse/_version.py
36
+
37
+ # Benchmark: pinned TOON encoder deps (regenerate with `cd scripts/bench && npm install`)
38
+ scripts/bench/node_modules/
@@ -0,0 +1,2 @@
1
+ # False positives only — verify before adding (fake test fixture data, not a real secret).
2
+ tests/test_cli.py:generic-api-key:193
@@ -0,0 +1,106 @@
1
+ # Symbols the guard mis-flags as unresolved. The guard parses `from X import Y`
2
+ # only within a staged diff hunk, so a new line using a stdlib symbol whose import
3
+ # line isn't also in the hunk gets a false positive. A genuinely missing import of
4
+ # these is caught immediately by pytest/import, so suppressing them here is safe.
5
+ Path
6
+ OrderedDict
7
+ Lock
8
+ Counter
9
+ Thread
10
+ field
11
+ dataclass
12
+ # Also mis-flagged: a callback PARAMETER invoked as a bare call (`drop_sink(h, v)` in
13
+ # policy.apply). The guard resolves module symbols/imports, not function parameters, so a
14
+ # param called by name reads as unresolved. A genuinely wrong name is caught by pytest.
15
+ drop_sink
16
+ answerer
17
+ rule_for
18
+ # cli.py's _build_answerers(args, make_openai, make_anthropic, ...) — the two answerer-
19
+ # factory PARAMETERS, called bare (make_openai(base, key, m) / make_anthropic(model)).
20
+ # Same false-positive class as drop_sink/answerer/rule_for above.
21
+ make_openai
22
+ make_anthropic
23
+ # fluency.py's _aggregate_by_model(pairs, answerers, trials, payload_fn) — the eval
24
+ # PARAMETER (run_diff_payload/run_text_diff_payload), called bare as payload_fn(a, b,
25
+ # fn, tool, trials=trials). Same false-positive class as drop_sink/answerer above.
26
+ payload_fn
27
+ # Also mis-flagged: a local variable holding a function REFERENCE, called bare in a
28
+ # monkeypatch wrapper (`real = mod.fn; def wrapper(*a): return real(*a)`), same
29
+ # root cause as the callback-parameter case above — the guard resolves module
30
+ # symbols/imports, not locals. Confirmed correct: pytest passes with these wrappers.
31
+ real_apply
32
+ real
33
+ orig_send
34
+ # test_run_text_diff_payload_computes_wire_exactly_once's `original = fluency.text_diff.
35
+ # text_diff_wire`, called bare inside its counting_wire wrapper. Same class as real/
36
+ # orig_send above.
37
+ original
38
+ # Also mis-flagged: plain English words inside a docstring that happen to look like
39
+ # `word(...)` bare calls to the guard's pattern match (e.g. "the client's stdin",
40
+ # "server->client", "closes the transport in turn"). Not code at all.
41
+ client
42
+ wire
43
+ cleanly
44
+ server
45
+ args
46
+ keys
47
+ measurement
48
+ one
49
+ unconditionally
50
+ flight
51
+ rows
52
+ inner
53
+ built
54
+ wide
55
+ name
56
+ callers
57
+ detail
58
+ scope
59
+ scopes
60
+ implicitly
61
+ # report.py's _build_diff_style_report docstring: "pre-split into lines (not a single
62
+ # string)" — "lines (not" reads as a bare call. Not code.
63
+ lines
64
+ # fluency.py's run_text_diff_payload docstring: "computes the diff wire exactly ONCE
65
+ # (unlike calling gen_text_diff_questions..." — "ONCE (unlike" reads as a bare call.
66
+ ONCE
67
+ # Also mis-flagged: importlib.metadata.version, imported locally inside
68
+ # _attestation_card (html_report.py) the same way report.py's build_verify_header
69
+ # already does — same hunk-splitting false positive as the stdlib symbols above.
70
+ # Confirmed resolving correctly: `terse verify --html` prints the real version.
71
+ version
72
+ # probes.py / cli.py #64 cross-server probe: hunk-split import + same-file false
73
+ # positives — the guard parses `from X import Y` only within the staged hunk and does not
74
+ # resolve same-file top-level defs (e.g. value_redundancy called inside cross_server_*).
75
+ # All confirmed defined/imported; a genuinely missing one is caught by pytest/import.
76
+ combinations
77
+ extract_records
78
+ encode_cl100k
79
+ value_redundancy
80
+ DEFAULT_PROBE_REPORT
81
+ # probes.py cross_call_overlap docstring: "When `idf` is supplied (from `token_idf`)" —
82
+ # "supplied (from" reads as a bare call to the guard's pattern match. Not code.
83
+ supplied
84
+ # proxy.py _emit_audit: `audit = self.audit` narrowed local (mypy baseline, audit
85
+ # fix #1), called bare — same local-variable false-positive class as real/orig_send.
86
+ audit
87
+ # test_codec_hotpath.py's counting wrapper: `real_minify = transforms.minify`, called
88
+ # bare inside the monkeypatch lambda. Same local-variable class as real/orig_send.
89
+ real_minify
90
+ # proxy.py run_proxy docstring (stats ledger): "record per result (sizes + decision"
91
+ # and "ON by default (cli.py resolves" — English words reading as bare calls. Not code.
92
+ result
93
+ default
94
+ # test_stats.py locals holding build_stats_writer's returned closure, called bare
95
+ # (writer(...) / quiet(...) / loud(...)). Same local-variable class as real/orig_send.
96
+ writer
97
+ quiet
98
+ loud
99
+ # policy.py select / proxy.py run_proxy docstrings (#83): English prose the guard's
100
+ # pattern match reads as bare calls — "when `server` is known (#83)", "self-prefix
101
+ # their own tool names (kb names ...)", "a server-scoped rule (`runecho.*`)", "the
102
+ # command basename (kb behind ...)". Not code.
103
+ known
104
+ names
105
+ rule
106
+ basename
@@ -0,0 +1,151 @@
1
+ # terse — Benchmarks
2
+
3
+ **Last updated: 2026-07-17.** All figures below were produced on that date and are
4
+ **reproducible end-to-end** with the commands shown — nothing here is hand-typed or
5
+ estimated. If you re-run and get different numbers, the code changed; open an issue.
6
+
7
+ ## What is being measured
8
+
9
+ - **Token reduction** = how many fewer tokens the compressed form costs vs the raw JSON,
10
+ counted in **`cl100k_base`** (the tiktoken vocabulary terse uses). Higher is better.
11
+ A payload that is *already* compact (no pretty-print whitespace) makes every number here
12
+ a *pure structural* gain — the hardest honest case.
13
+ - **Lossless** = `decompress(compress(x)) == x` exactly. Every terse row below is verified
14
+ lossless per payload; a payload is dropped from a total if either tool fails its round-trip.
15
+ - "raw", "terse", "TOON" columns are % fewer tokens than the raw JSON.
16
+
17
+ ## Reproduce everything
18
+
19
+ ```bash
20
+ uv sync
21
+ cd scripts/bench && npm install # pins the official @toon-format/toon encoder
22
+ cd -
23
+ uv run scripts/bench/benchmark.py # §1 terse vs TOON on real GitHub API payloads
24
+ uv run scripts/bench/width_sweep.py # §2 the column-width sweep
25
+ uv run scripts/bench/diff_demo.py # §3 cross-call diff (terse's own axis)
26
+ ```
27
+
28
+ ---
29
+
30
+ ## §1 — terse vs TOON on real, public GitHub API payloads
31
+
32
+ The corpus is real GitHub API output (`scripts/bench/corpus/`) — the nested, record-shaped
33
+ tool traffic terse targets. `cl100k` tokens, all lossless.
34
+
35
+ | payload | records | raw tok | **terse** | TOON |
36
+ |---|--:|--:|--:|--:|
37
+ | gh_pulls | 30 | 151,165 | **76.1%** | −8.4% |
38
+ | gh_workflow_runs | 20 | 76,032 | **80.3%** | −7.5% |
39
+ | gh_issues | 30 | 48,032 | **32.7%** | −8.0% |
40
+ | gh_commits | 30 | 69,652 | **26.5%** | −4.5% |
41
+ | gh_dir_listing | 24 | 6,736 | **31.4%** | −7.7% |
42
+ | gh_rate_limit | 1 obj | 357 | **13.4%** | −36.7% |
43
+ | gh_repo_single | 1 obj | 1,652 | 0.0% | −4.4% |
44
+ | gh_commits_flat | 30 | 10,886 | **2.4%** | 1.7% |
45
+ | gh_labels | 9 | 632 | 15.2% | **19.0%** |
46
+ | **weighted total** | | **365,144** | **58.3%** | **−7.1%** |
47
+
48
+ **Plain reading:** on real nested records terse cuts tokens **58%**; TOON *regresses* to −7%
49
+ (worse than raw) because it adds a key-path per nesting level, while terse folds the repeated
50
+ subtrees and long repeated strings (e.g. `gh_pulls` = 60 copies of the same repo object
51
+ collapsed to one legend entry → 76%). TOON wins only on `gh_labels` — a flat, short-valued
52
+ uniform table, its designed sweet spot.
53
+
54
+ ---
55
+
56
+ ## §2 — Column-width sweep: is there a "narrow vs wide" crossover? (No.)
57
+
58
+ A natural hypothesis is that TOON overtakes terse once records get *wide* (many columns),
59
+ because TOON writes the header once per table. We tested it directly: **40 rows held fixed,
60
+ column count swept 2→12**, seeded, each row verified lossless for both tools.
61
+
62
+ | columns | terse% | TOON% | winner |
63
+ |--:|--:|--:|--:|
64
+ | 2 | 40.4 | 44.0 | TOON +3.6 |
65
+ | 3 | 52.1 | 48.7 | terse +3.4 |
66
+ | 4 | 46.8 | 48.8 | TOON +2.0 |
67
+ | 5 | 52.6 | 50.7 | terse +1.9 |
68
+ | 6 | 48.9 | 50.5 | TOON +1.6 |
69
+ | 7 | 52.7 | 51.5 | terse +1.2 |
70
+ | 8 | 50.0 | 51.2 | TOON +1.2 |
71
+ | 9 | 52.9 | 52.1 | terse +0.8 |
72
+ | 10 | 50.6 | 51.7 | TOON +1.1 |
73
+ | 11 | 52.9 | 52.3 | terse +0.6 |
74
+ | 12 | 51.1 | 52.1 | TOON +1.0 |
75
+
76
+ **Plain reading:** there is **no clean column-count crossover.** The winner oscillates by
77
+ parity, the margins are ~1–4 points, and they **converge toward a tie** as width grows — the
78
+ opposite of "TOON pulls decisively ahead when wide." (An earlier draft of this repo's README
79
+ claimed a ≤3/≥4-column boundary from a single synthetic construction; a seeded sweep does not
80
+ reproduce it, and the claim was corrected.)
81
+
82
+ **The real dividing axis is value repetition, not width.** On these stripped-flat synthetic
83
+ tables — no nesting, no long repeated strings — terse's dictionary/subtree tiers have little
84
+ to fold, so the two tools tie. terse's decisive §1 win comes precisely from the redundancy
85
+ that real records have and synthetic flat tables don't.
86
+
87
+ ---
88
+
89
+ ## §3 — Cross-call diff (an axis no stateless encoder has)
90
+
91
+ When the same tool is called again (poll a list, re-read a file), terse can emit a lossless
92
+ *delta* against the prior result instead of the whole payload. TOON, minify, and terse's own
93
+ single-shot codec all pay the full column every call. Modeling one repeat call per payload
94
+ (`diff_demo.py`), the **second** call costs:
95
+
96
+ | repeated call | full re-send | diff | smaller by |
97
+ |---|--:|--:|--:|
98
+ | gh_commits_flat | 10,681 | 812 | **92.4%** |
99
+ | gh_issues | 32,608 | 4,448 | **86.4%** |
100
+ | gh_pulls | 37,776 | 15,292 | **59.5%** |
101
+ | **weighted total** | 152,837 | 40,138 | **73.7%** |
102
+
103
+ **Honest caveat (read this):** these are *modeled* repeat-call savings. How *often* the
104
+ pattern occurs in a real agent loop is workload-dependent and is being measured directly (the
105
+ proxy now records a per-result `diff_reason` — run `terse stats` to see the breakdown for your
106
+ own traffic). Do **not** read §3 as a claim about aggregate real-world savings; read §1 for that.
107
+
108
+ ---
109
+
110
+ ## §4 — Competitor landscape (hands-on, tested 2026-07-17)
111
+
112
+ Installed and tested, not cited from marketing. Only TOON (§1) is directly comparable on a
113
+ lossless token axis; the rest measure *different guarantees*, so no head-to-head % is claimed.
114
+
115
+ | Tool | What it is (verified) | Comparable? |
116
+ |---|---|---|
117
+ | **headroom** (`headroom-ai`, v0.32.0) | JSON compressor is a **deterministic Rust transform, not ML**: lossless on uniform arrays, but **drops rows** on larger/irregular sets, recoverable only via a `retrieve` round-trip against a **time-boxed cache** (default 30-min TTL). Measured on our corpus: 33.1% (lossless reformat) to 42.5%/64.1% (lossy, dropping 13/30 and 13/20 rows). A separate optional text compressor *is* ML. | No — its larger numbers come from *dropping data* with time-limited recovery; terse's are unconditionally lossless. |
118
+ | **LLMLingua-2** (Microsoft) | Lossy prompt token-classifier. Fed JSON it strips syntax (`{`,`}`,`:`,`"`) as low-information and emits **invalid, unparseable JSON**; truncates past 512 tokens. ~50% on both prose and JSON. | No — different axis (prompts, not tool output), lossy, corrupts structure. |
119
+ | **Atlassian mcp-compressor** | Primarily lossless schema/description compression at connect time — **complementary and stackable** with terse (`terse proxy -- mcp-compressor -- <server>`). An opt-in `--toonify` flag also reformats results into TOON (off by default; no diffing/policy/state). | Adjacent, not competing. |
120
+ | **Anthropic / OpenAI context editing** | Native, server-side, **lossy** history-pruning; no local artifact to run keylessly. | Different mechanism (drops old results server-side). |
121
+
122
+ ---
123
+
124
+ ## §5 — Generate your own real-session evidence
125
+
126
+ The benchmarks above are a fixed corpus. terse also keeps an always-on, **payload-free**
127
+ savings ledger from your real sessions (sizes + decisions only, never content), so you can
128
+ measure what terse saved *you*, not a synthetic corpus:
129
+
130
+ ```bash
131
+ terse stats # rollup: results, decisions, tokens saved, per-tool rows
132
+ terse stats --since 7d # windowed
133
+ ```
134
+
135
+ This is terse's "evidence over stars" path: your own numbers, on your own traffic, verifiable
136
+ without trusting this file.
137
+
138
+ ---
139
+
140
+ ## Methodology & honesty notes
141
+
142
+ - Tokenizer is `cl100k_base`; absolute % shift under a different vocabulary but the ranking is
143
+ stable (terse's cross-tokenizer-invariance claim, tested separately in the suite).
144
+ - §1 corpus is real, public GitHub API output. §2 is **synthetic and seeded** — illustrative
145
+ of a mechanism, not production-representative; the exact numbers depend on the construction
146
+ (short keys, value cardinality), which is why §2's takeaway is "no crossover," not a constant.
147
+ - Every terse figure is verified lossless per payload. §4's headroom row is the only place a
148
+ "reduction %" is reported for a tool that achieves it by *discarding* data — flagged inline.
149
+ - Adoption honesty: terse is new (pre-PyPI as of this date); TOON and headroom are far more
150
+ established. terse's wedge is narrow and specific — *unconditionally lossless, no ML, no
151
+ egress* — not breadth of adoption.
@@ -0,0 +1,58 @@
1
+ # Changelog
2
+
3
+ All notable changes to terse are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+ Releases are cut from git tags (`vX.Y.Z`, via hatch-vcs) — an entry moves from
8
+ `[Unreleased]` to a versioned section when its tag is pushed.
9
+
10
+ ## [Unreleased]
11
+
12
+ ## [0.3.1] - 2026-07-18
13
+
14
+ ### Added
15
+ - Automated PyPI publishing via GitHub Actions **Trusted Publishing** (OIDC) with
16
+ PEP 740 provenance attestations — tagged releases upload to PyPI with no stored
17
+ token. This is the first PyPI release of `terse-mcp`. (#111)
18
+
19
+ ### Fixed
20
+ - `release` workflow: corrected the built-wheel name check for the `terse-mcp`
21
+ rename (`terse_mcp-*.whl`), which had blocked the tagged release run. (#110)
22
+
23
+ ## [0.3.0] - 2026-07-18
24
+
25
+ ### Added
26
+ - **Installable package** — MIT license and PyPI-ready metadata. The distribution is
27
+ named `terse-mcp` (the bare `terse` is taken on PyPI); the import package stays
28
+ `terse`, so `python -m terse` is unchanged. (#103)
29
+ - `verify --json`: emit the lossless-gate verdict and cl100k savings totals as JSON
30
+ on stdout instead of the markdown report — CI-checkable (`… | jq -e
31
+ .lossless_gate.ok`), parity with `stats --json` / `mcp-status --json`. (#107)
32
+ - diff-moat instrumentation: `stats` records why the cross-call diff tier did or did
33
+ not fire per call, to measure the diff feature's real-world reach (Phase 0+1). (#101)
34
+ - Property-based fuzzing of the lossless round-trip guarantee. (#105)
35
+
36
+ ### Documentation
37
+ - Onboarding quickstart + per-client install recipes. (#108)
38
+ - Codeshot architecture diagram embedded in TECHNICAL.md. (#104)
39
+
40
+ ## [0.2.0] - 2026-07-17
41
+
42
+ ### Added
43
+ - `mcp-status`: each wrapped server now shows what it actually fronts
44
+ (`wraps=<downstream cmd/url>`), whether the cross-call diff tier is
45
+ on/off/default, and whether the stats ledger is on; a policy file that has gone
46
+ missing since install is flagged `(MISSING)`; new `--json` output for
47
+ scripts/CI. (#97)
48
+ - `tune`: each drop-candidate bucket ends with a savings rollup — estimated gross
49
+ tokens the whole bucket would evict and its share of the corpus. (#98)
50
+ - `fluency --html`: writes the forest-plot comprehension-gap report next to
51
+ `--out` for the paired diff-family evals (`--diff`, `--diff-soak`,
52
+ `--text-diff-eval`); same inline-SVG/no-JS/no-CDN form as `measure --html`. (#99)
53
+
54
+ ### Fixed
55
+ - `stats`: the per-tool table falls back to character columns when tiktoken token
56
+ counts are absent (was rendering as all-zeros); an empty `--since` window now
57
+ reports the window rather than "nothing ever recorded"; added a per-tool
58
+ cross-call diff hit-rate (`diff%`) column. (#96)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eric Minish
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.