tokenfold 0.3.1__tar.gz → 0.3.3__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.
- {tokenfold-0.3.1 → tokenfold-0.3.3}/Cargo.lock +10 -10
- {tokenfold-0.3.1 → tokenfold-0.3.3}/Cargo.toml +1 -1
- {tokenfold-0.3.1 → tokenfold-0.3.3}/PKG-INFO +22 -17
- {tokenfold-0.3.1 → tokenfold-0.3.3}/README.md +21 -16
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/modes.rs +21 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/pipeline.rs +11 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/report.rs +52 -0
- tokenfold-0.3.3/crates/tokenfold-core/src/transforms/log_fold.rs +296 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/transforms/mod.rs +1 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-py/Cargo.toml +1 -1
- {tokenfold-0.3.1 → tokenfold-0.3.3}/LICENSE +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/Cargo.toml +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/benches/THRESHOLDS.toml +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/benches/compression_bench.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/examples/quickstart.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/budget.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/errors.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/filters.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/input.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/lib.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/retrieval_store.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/safety.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/stats.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/status.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/token_estimator.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/transforms/diff.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/transforms/json.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/transforms/json_dict.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/transforms/json_fold.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/transforms/logs.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/transforms/redaction.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/src/transforms/schema.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/tests/golden.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/tests/integration.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-core/tests/property_tests.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/crates/tokenfold-py/src/lib.rs +0 -0
- {tokenfold-0.3.1 → tokenfold-0.3.3}/pyproject.toml +0 -0
|
@@ -1013,7 +1013,7 @@ dependencies = [
|
|
|
1013
1013
|
|
|
1014
1014
|
[[package]]
|
|
1015
1015
|
name = "tokenfold-adapters"
|
|
1016
|
-
version = "0.3.
|
|
1016
|
+
version = "0.3.3"
|
|
1017
1017
|
dependencies = [
|
|
1018
1018
|
"serde",
|
|
1019
1019
|
"serde_json",
|
|
@@ -1022,7 +1022,7 @@ dependencies = [
|
|
|
1022
1022
|
|
|
1023
1023
|
[[package]]
|
|
1024
1024
|
name = "tokenfold-admin"
|
|
1025
|
-
version = "0.3.
|
|
1025
|
+
version = "0.3.3"
|
|
1026
1026
|
dependencies = [
|
|
1027
1027
|
"ed25519-dalek",
|
|
1028
1028
|
"serde",
|
|
@@ -1032,7 +1032,7 @@ dependencies = [
|
|
|
1032
1032
|
|
|
1033
1033
|
[[package]]
|
|
1034
1034
|
name = "tokenfold-cli"
|
|
1035
|
-
version = "0.3.
|
|
1035
|
+
version = "0.3.3"
|
|
1036
1036
|
dependencies = [
|
|
1037
1037
|
"clap",
|
|
1038
1038
|
"serde",
|
|
@@ -1045,7 +1045,7 @@ dependencies = [
|
|
|
1045
1045
|
|
|
1046
1046
|
[[package]]
|
|
1047
1047
|
name = "tokenfold-core"
|
|
1048
|
-
version = "0.3.
|
|
1048
|
+
version = "0.3.3"
|
|
1049
1049
|
dependencies = [
|
|
1050
1050
|
"proptest",
|
|
1051
1051
|
"regex",
|
|
@@ -1059,25 +1059,25 @@ dependencies = [
|
|
|
1059
1059
|
|
|
1060
1060
|
[[package]]
|
|
1061
1061
|
name = "tokenfold-image"
|
|
1062
|
-
version = "0.3.
|
|
1062
|
+
version = "0.3.3"
|
|
1063
1063
|
|
|
1064
1064
|
[[package]]
|
|
1065
1065
|
name = "tokenfold-learn"
|
|
1066
|
-
version = "0.3.
|
|
1066
|
+
version = "0.3.3"
|
|
1067
1067
|
dependencies = [
|
|
1068
1068
|
"tokenfold-core",
|
|
1069
1069
|
]
|
|
1070
1070
|
|
|
1071
1071
|
[[package]]
|
|
1072
1072
|
name = "tokenfold-output"
|
|
1073
|
-
version = "0.3.
|
|
1073
|
+
version = "0.3.3"
|
|
1074
1074
|
dependencies = [
|
|
1075
1075
|
"tokenfold-core",
|
|
1076
1076
|
]
|
|
1077
1077
|
|
|
1078
1078
|
[[package]]
|
|
1079
1079
|
name = "tokenfold-proxy"
|
|
1080
|
-
version = "0.3.
|
|
1080
|
+
version = "0.3.3"
|
|
1081
1081
|
dependencies = [
|
|
1082
1082
|
"clap",
|
|
1083
1083
|
"serde",
|
|
@@ -1089,7 +1089,7 @@ dependencies = [
|
|
|
1089
1089
|
|
|
1090
1090
|
[[package]]
|
|
1091
1091
|
name = "tokenfold-py"
|
|
1092
|
-
version = "0.3.
|
|
1092
|
+
version = "0.3.3"
|
|
1093
1093
|
dependencies = [
|
|
1094
1094
|
"pyo3",
|
|
1095
1095
|
"serde_json",
|
|
@@ -1098,7 +1098,7 @@ dependencies = [
|
|
|
1098
1098
|
|
|
1099
1099
|
[[package]]
|
|
1100
1100
|
name = "tokenfold-rag"
|
|
1101
|
-
version = "0.3.
|
|
1101
|
+
version = "0.3.3"
|
|
1102
1102
|
|
|
1103
1103
|
[[package]]
|
|
1104
1104
|
name = "toml"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tokenfold
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
@@ -10,7 +10,9 @@ Summary: Token-compression toolkit for LLM payloads (Python binding over the tok
|
|
|
10
10
|
Requires-Python: >=3.9
|
|
11
11
|
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
<div align="center">
|
|
14
|
+
|
|
15
|
+
# TOKENFOLD
|
|
14
16
|
|
|
15
17
|
**Send less noise. Fit more context. Pay for fewer input tokens.**
|
|
16
18
|
|
|
@@ -20,6 +22,8 @@ Local, provider-neutral compression for prompts, tool schemas, JSON, logs, and d
|
|
|
20
22
|
|
|
21
23
|
[Quick start](#quick-start) · [Why tokenfold](#why-tokenfold) · [Integrations](#pick-your-integration) · [Benchmarks](#reproduce-the-numbers)
|
|
22
24
|
|
|
25
|
+
</div>
|
|
26
|
+
|
|
23
27
|
---
|
|
24
28
|
|
|
25
29
|
## Proven compression, not projections
|
|
@@ -45,8 +49,8 @@ cargo add tokenfold-core # Rust library
|
|
|
45
49
|
cargo install tokenfold-cli # Rust CLI
|
|
46
50
|
```
|
|
47
51
|
|
|
48
|
-
Or download the
|
|
49
|
-
[GitHub Releases](https://github.com/snchimata/tokenfold/releases/
|
|
52
|
+
Or download the CLI for Linux, macOS, or Windows from
|
|
53
|
+
[GitHub Releases](https://github.com/snchimata/tokenfold/releases/latest),
|
|
50
54
|
then verify it with the adjacent `.sha256` file.
|
|
51
55
|
|
|
52
56
|
Or install the Python package:
|
|
@@ -79,12 +83,21 @@ the canonical compression receipt:
|
|
|
79
83
|
```typescript
|
|
80
84
|
import { compress } from "tokenfold";
|
|
81
85
|
|
|
86
|
+
const input = new TextEncoder().encode(JSON.stringify({
|
|
87
|
+
results: [
|
|
88
|
+
{ id: 101, region: "us-east-1", plan: "pro" },
|
|
89
|
+
{ id: 102, region: "us-east-1", plan: "pro" },
|
|
90
|
+
{ id: 103, region: "us-east-1", plan: "pro" },
|
|
91
|
+
],
|
|
92
|
+
}, null, 2));
|
|
93
|
+
|
|
82
94
|
const { payload, report } = await compress(input, {
|
|
83
95
|
format: "json",
|
|
84
96
|
mode: "balanced",
|
|
85
97
|
});
|
|
86
98
|
|
|
87
99
|
console.log(`saved ${report.saved_tokens} tokens`);
|
|
100
|
+
console.log(new TextDecoder().decode(payload));
|
|
88
101
|
```
|
|
89
102
|
|
|
90
103
|
Want to try the CLI from source? Inspect the bundled request without changing it:
|
|
@@ -96,10 +109,12 @@ cargo run --release --locked -p tokenfold-cli -- \
|
|
|
96
109
|
inspect examples/openai_payload.json --format openai
|
|
97
110
|
```
|
|
98
111
|
|
|
112
|
+
Across 100 requests with the same payload shape, those savings add up to:
|
|
113
|
+
|
|
99
114
|
```text
|
|
100
|
-
json_minify
|
|
101
|
-
schema_compaction
|
|
102
|
-
TOTAL
|
|
115
|
+
json_minify 34,600 → 22,900 saved 11,700
|
|
116
|
+
schema_compaction 22,900 → 21,300 saved 1,600
|
|
117
|
+
TOTAL 34,600 → 21,300 saved 13,300 (38.4% reduction, estimated)
|
|
103
118
|
```
|
|
104
119
|
|
|
105
120
|
## Why tokenfold
|
|
@@ -229,16 +244,6 @@ cargo run --release --locked -p tokenfold-cli -- \
|
|
|
229
244
|
The sample reports 382 → 206 estimated tokens, a **46.1% reduction**. Ragged
|
|
230
245
|
or compact inputs may save little; Tokenfold reports that result honestly.
|
|
231
246
|
|
|
232
|
-
## Status
|
|
233
|
-
|
|
234
|
-
Version **0.3.1** provides SHA-256-checksummed (not signed) CLI binaries for
|
|
235
|
-
Linux, macOS, and Windows from [GitHub Releases](https://github.com/snchimata/tokenfold/releases/tag/v0.3.1),
|
|
236
|
-
plus CycloneDX SBOMs. Registry packages are published to
|
|
237
|
-
[PyPI](https://pypi.org/project/tokenfold/), [npm](https://www.npmjs.com/package/tokenfold),
|
|
238
|
-
and [crates.io](https://crates.io/crates/tokenfold-core). npm installs the
|
|
239
|
-
matching native binary package without a post-install download or local Rust
|
|
240
|
-
build.
|
|
241
|
-
|
|
242
247
|
## Contributing
|
|
243
248
|
|
|
244
249
|
Issues and pull requests are welcome. Run the core checks before opening a PR:
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# TOKENFOLD
|
|
2
4
|
|
|
3
5
|
**Send less noise. Fit more context. Pay for fewer input tokens.**
|
|
4
6
|
|
|
@@ -8,6 +10,8 @@ Local, provider-neutral compression for prompts, tool schemas, JSON, logs, and d
|
|
|
8
10
|
|
|
9
11
|
[Quick start](#quick-start) · [Why tokenfold](#why-tokenfold) · [Integrations](#pick-your-integration) · [Benchmarks](#reproduce-the-numbers)
|
|
10
12
|
|
|
13
|
+
</div>
|
|
14
|
+
|
|
11
15
|
---
|
|
12
16
|
|
|
13
17
|
## Proven compression, not projections
|
|
@@ -33,8 +37,8 @@ cargo add tokenfold-core # Rust library
|
|
|
33
37
|
cargo install tokenfold-cli # Rust CLI
|
|
34
38
|
```
|
|
35
39
|
|
|
36
|
-
Or download the
|
|
37
|
-
[GitHub Releases](https://github.com/snchimata/tokenfold/releases/
|
|
40
|
+
Or download the CLI for Linux, macOS, or Windows from
|
|
41
|
+
[GitHub Releases](https://github.com/snchimata/tokenfold/releases/latest),
|
|
38
42
|
then verify it with the adjacent `.sha256` file.
|
|
39
43
|
|
|
40
44
|
Or install the Python package:
|
|
@@ -67,12 +71,21 @@ the canonical compression receipt:
|
|
|
67
71
|
```typescript
|
|
68
72
|
import { compress } from "tokenfold";
|
|
69
73
|
|
|
74
|
+
const input = new TextEncoder().encode(JSON.stringify({
|
|
75
|
+
results: [
|
|
76
|
+
{ id: 101, region: "us-east-1", plan: "pro" },
|
|
77
|
+
{ id: 102, region: "us-east-1", plan: "pro" },
|
|
78
|
+
{ id: 103, region: "us-east-1", plan: "pro" },
|
|
79
|
+
],
|
|
80
|
+
}, null, 2));
|
|
81
|
+
|
|
70
82
|
const { payload, report } = await compress(input, {
|
|
71
83
|
format: "json",
|
|
72
84
|
mode: "balanced",
|
|
73
85
|
});
|
|
74
86
|
|
|
75
87
|
console.log(`saved ${report.saved_tokens} tokens`);
|
|
88
|
+
console.log(new TextDecoder().decode(payload));
|
|
76
89
|
```
|
|
77
90
|
|
|
78
91
|
Want to try the CLI from source? Inspect the bundled request without changing it:
|
|
@@ -84,10 +97,12 @@ cargo run --release --locked -p tokenfold-cli -- \
|
|
|
84
97
|
inspect examples/openai_payload.json --format openai
|
|
85
98
|
```
|
|
86
99
|
|
|
100
|
+
Across 100 requests with the same payload shape, those savings add up to:
|
|
101
|
+
|
|
87
102
|
```text
|
|
88
|
-
json_minify
|
|
89
|
-
schema_compaction
|
|
90
|
-
TOTAL
|
|
103
|
+
json_minify 34,600 → 22,900 saved 11,700
|
|
104
|
+
schema_compaction 22,900 → 21,300 saved 1,600
|
|
105
|
+
TOTAL 34,600 → 21,300 saved 13,300 (38.4% reduction, estimated)
|
|
91
106
|
```
|
|
92
107
|
|
|
93
108
|
## Why tokenfold
|
|
@@ -217,16 +232,6 @@ cargo run --release --locked -p tokenfold-cli -- \
|
|
|
217
232
|
The sample reports 382 → 206 estimated tokens, a **46.1% reduction**. Ragged
|
|
218
233
|
or compact inputs may save little; Tokenfold reports that result honestly.
|
|
219
234
|
|
|
220
|
-
## Status
|
|
221
|
-
|
|
222
|
-
Version **0.3.1** provides SHA-256-checksummed (not signed) CLI binaries for
|
|
223
|
-
Linux, macOS, and Windows from [GitHub Releases](https://github.com/snchimata/tokenfold/releases/tag/v0.3.1),
|
|
224
|
-
plus CycloneDX SBOMs. Registry packages are published to
|
|
225
|
-
[PyPI](https://pypi.org/project/tokenfold/), [npm](https://www.npmjs.com/package/tokenfold),
|
|
226
|
-
and [crates.io](https://crates.io/crates/tokenfold-core). npm installs the
|
|
227
|
-
matching native binary package without a post-install download or local Rust
|
|
228
|
-
build.
|
|
229
|
-
|
|
230
235
|
## Contributing
|
|
231
236
|
|
|
232
237
|
Issues and pull requests are welcome. Run the core checks before opening a PR:
|
|
@@ -15,6 +15,7 @@ pub enum TransformId {
|
|
|
15
15
|
JsonFieldFold,
|
|
16
16
|
JsonValueDict,
|
|
17
17
|
SchemaCompaction,
|
|
18
|
+
LogFieldFold,
|
|
18
19
|
LogCompaction,
|
|
19
20
|
DiffCompaction,
|
|
20
21
|
}
|
|
@@ -26,6 +27,7 @@ impl TransformId {
|
|
|
26
27
|
TransformId::JsonFieldFold => "json_field_fold",
|
|
27
28
|
TransformId::JsonValueDict => "json_value_dict",
|
|
28
29
|
TransformId::SchemaCompaction => "schema_compaction",
|
|
30
|
+
TransformId::LogFieldFold => "log_field_fold",
|
|
29
31
|
TransformId::LogCompaction => "log_compaction",
|
|
30
32
|
TransformId::DiffCompaction => "diff_compaction",
|
|
31
33
|
}
|
|
@@ -140,6 +142,25 @@ pub static ALL_ENTRIES: &[ModeEntry] = &[
|
|
|
140
142
|
task_scopes: &[TaskScope::All],
|
|
141
143
|
applicable_formats: &[InputFormat::OpenAiJson, InputFormat::AnthropicJson],
|
|
142
144
|
},
|
|
145
|
+
// log_field_fold (v0.4): reversible columnar fold of TEMPLATED log lines — the log-line
|
|
146
|
+
// analogue of json_field_fold (emit each shared line skeleton once + per-line captured fields).
|
|
147
|
+
// Lossless (round-trip gated in the pipeline), so max_ratio is unrestricted (1.0), but like
|
|
148
|
+
// log_compaction it restructures what the model sees, so it stays out of Conservative and ships
|
|
149
|
+
// behind --experimental until its fidelity gate is green (the same path json_field_fold and
|
|
150
|
+
// log_compaction took). Runs before the lossy log_compaction (lossless-before-lossy ordering).
|
|
151
|
+
ModeEntry {
|
|
152
|
+
transform_id: TransformId::LogFieldFold,
|
|
153
|
+
version: "1.0.0",
|
|
154
|
+
conservative_enabled: false,
|
|
155
|
+
balanced_enabled: false,
|
|
156
|
+
aggressive_enabled: false,
|
|
157
|
+
experimental: true,
|
|
158
|
+
max_ratio_conservative: 0.0,
|
|
159
|
+
max_ratio_balanced: 1.0,
|
|
160
|
+
max_ratio_aggressive: 1.0,
|
|
161
|
+
task_scopes: &[TaskScope::All],
|
|
162
|
+
applicable_formats: &[InputFormat::PlainText, InputFormat::CommandOutput],
|
|
163
|
+
},
|
|
143
164
|
ModeEntry {
|
|
144
165
|
transform_id: TransformId::LogCompaction,
|
|
145
166
|
version: "1.0.0",
|
|
@@ -366,6 +366,10 @@ fn apply_single_transform(
|
|
|
366
366
|
// not that Phase 2 ship distinct values per mode).
|
|
367
367
|
transforms::schema::compact_schema(bytes, 1).map_err(|e| e.to_string())
|
|
368
368
|
}
|
|
369
|
+
TransformId::LogFieldFold => {
|
|
370
|
+
let text = std::str::from_utf8(bytes).map_err(|e| e.to_string())?;
|
|
371
|
+
Ok(transforms::log_fold::fold_log(text).into_bytes())
|
|
372
|
+
}
|
|
369
373
|
TransformId::LogCompaction => {
|
|
370
374
|
let text = std::str::from_utf8(bytes).map_err(|e| e.to_string())?;
|
|
371
375
|
Ok(transforms::logs::compact(text, false).into_bytes())
|
|
@@ -423,6 +427,13 @@ fn validate_safety(
|
|
|
423
427
|
}
|
|
424
428
|
}
|
|
425
429
|
}
|
|
430
|
+
// log_field_fold restructures templated log lines into a columnar form, so its safety
|
|
431
|
+
// invariant is exact reversibility: unfolding the output must reproduce the input bytes.
|
|
432
|
+
TransformId::LogFieldFold => {
|
|
433
|
+
if !transforms::log_fold::round_trips(before, after) {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
426
437
|
TransformId::LogCompaction | TransformId::DiffCompaction => {}
|
|
427
438
|
}
|
|
428
439
|
safety::protected_segments_present(protected, after)
|
|
@@ -16,6 +16,10 @@ pub struct CompressionReport {
|
|
|
16
16
|
pub format: String,
|
|
17
17
|
pub task_scope: String,
|
|
18
18
|
pub request_id: Option<String>,
|
|
19
|
+
/// F-055: staged `raw -> RTK -> tokenfold` accounting. `None` for the common
|
|
20
|
+
/// single-stage path; populated only by RTK-composed `wrap --rtk` runs.
|
|
21
|
+
#[serde(default)]
|
|
22
|
+
pub pipeline: Option<PipelineReport>,
|
|
19
23
|
pub quality: Option<QualityReport>,
|
|
20
24
|
pub budget: Option<BudgetReport>,
|
|
21
25
|
pub cache: Option<CacheReport>,
|
|
@@ -61,6 +65,7 @@ impl CompressionReport {
|
|
|
61
65
|
format,
|
|
62
66
|
task_scope,
|
|
63
67
|
request_id: None,
|
|
68
|
+
pipeline: None,
|
|
64
69
|
quality: None,
|
|
65
70
|
budget: None,
|
|
66
71
|
cache: None,
|
|
@@ -82,6 +87,53 @@ pub struct EstimatorInfo {
|
|
|
82
87
|
pub is_exact: bool,
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
/// F-055: separates savings and recoverability across composed stages (RTK then
|
|
91
|
+
/// tokenfold) so RTK's savings are never credited to tokenfold. The top-level
|
|
92
|
+
/// `original_tokens` keeps its v1 meaning — tokens *entering* `tokenfold_core`,
|
|
93
|
+
/// which is the post-RTK count when composed. See INTERFACES.md §1.9 / §2.
|
|
94
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
95
|
+
pub struct PipelineReport {
|
|
96
|
+
/// Pre-RTK byte count. `Some` only when a complete raw capture was observed.
|
|
97
|
+
pub raw_input_bytes: Option<usize>,
|
|
98
|
+
/// Pre-RTK token count. `Some` only when raw capture is complete.
|
|
99
|
+
pub raw_input_tokens: Option<usize>,
|
|
100
|
+
pub final_output_bytes: usize,
|
|
101
|
+
/// Equals top-level `compressed_tokens`.
|
|
102
|
+
pub final_output_tokens: usize,
|
|
103
|
+
/// Populated only when raw and final counts use the same estimator.
|
|
104
|
+
pub total_saved_tokens: Option<usize>,
|
|
105
|
+
/// `"complete"`, `"partial"`, `"unavailable"`, or `"not_applicable"`.
|
|
106
|
+
pub raw_capture: String,
|
|
107
|
+
/// `"full"`, `"tokenfold_only"`, `"none"`, or `"not_applicable"`.
|
|
108
|
+
pub upstream_recoverability: String,
|
|
109
|
+
pub stages: Vec<PipelineStageReport>,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// F-055: one composed stage. Count fields are nullable because an unavailable
|
|
113
|
+
/// stage or missing pre-stage capture cannot be measured honestly.
|
|
114
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
115
|
+
pub struct PipelineStageReport {
|
|
116
|
+
/// `"rtk"` or `"tokenfold"`.
|
|
117
|
+
pub id: String,
|
|
118
|
+
pub version: Option<String>,
|
|
119
|
+
pub input_bytes: Option<usize>,
|
|
120
|
+
pub output_bytes: Option<usize>,
|
|
121
|
+
pub saved_bytes: Option<usize>,
|
|
122
|
+
pub input_tokens: Option<usize>,
|
|
123
|
+
pub output_tokens: Option<usize>,
|
|
124
|
+
pub saved_tokens: Option<usize>,
|
|
125
|
+
pub estimator: Option<EstimatorInfo>,
|
|
126
|
+
/// `"applied"`, `"passthrough"`, `"unavailable"`, `"incompatible"`, or `"failed"`.
|
|
127
|
+
pub status: String,
|
|
128
|
+
pub duration_ms: Option<f64>,
|
|
129
|
+
pub bypass_reason: Option<String>,
|
|
130
|
+
/// e.g. `"external:rtk@0.4.1"` or `"tokenfold_core"`.
|
|
131
|
+
pub provenance: String,
|
|
132
|
+
/// `"full"`, `"partial"`, `"none"`, or `"not_applicable"`.
|
|
133
|
+
pub recoverability: String,
|
|
134
|
+
pub evidence_ref: Option<String>,
|
|
135
|
+
}
|
|
136
|
+
|
|
85
137
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
86
138
|
pub struct BudgetReport {
|
|
87
139
|
pub target_tokens: Option<usize>,
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
//! `log_field_fold` transform (canonical id `"log_field_fold"`, v1.0.0).
|
|
2
|
+
//!
|
|
3
|
+
//! Content-aware, **losslessly reversible** structural compression for line-oriented text
|
|
4
|
+
//! (`InputFormat::PlainText` / `CommandOutput`). It is the log-line analogue of
|
|
5
|
+
//! [`json_field_fold`](super::json_fold): where that folds arrays of homogeneous *objects* by
|
|
6
|
+
//! emitting each repeated key once, this folds runs of *templated* log lines by emitting each
|
|
7
|
+
//! repeated line **template** once.
|
|
8
|
+
//!
|
|
9
|
+
//! Most log lines share a fixed skeleton and vary only in a few fields (timestamps, ids, counts):
|
|
10
|
+
//!
|
|
11
|
+
//! ```text
|
|
12
|
+
//! 2026-07-01T10:05:11Z req=req-0311 status=200 ms=41
|
|
13
|
+
//! 2026-07-01T10:05:12Z req=req-0312 status=200 ms=44
|
|
14
|
+
//! ```
|
|
15
|
+
//!
|
|
16
|
+
//! Replacing each variable token (a run of digits, or a `0x…` hex literal) with a placeholder
|
|
17
|
+
//! yields one shared template `2026-\x00-\x00T\x00:\x00:\x00Z req=req-\x00 status=\x00 ms=\x00`
|
|
18
|
+
//! plus a per-line tuple of the captured values. The fold emits every distinct template once and
|
|
19
|
+
//! a compact row per original line (`template_id` + its captured values), so the skeleton text is
|
|
20
|
+
//! paid for once instead of per line — a large win on repetitive logs, unlike
|
|
21
|
+
//! [`log_compaction`](super::logs) which only collapses *identical adjacent* lines.
|
|
22
|
+
//!
|
|
23
|
+
//! It is a pure structural rewrite: `unfold_log` reconstructs the original bytes exactly, and the
|
|
24
|
+
//! pipeline gates adoption on that round-trip ([`round_trips`]) — a fold that would ever lose data
|
|
25
|
+
//! (or a genuine input that happens to look like our framing) is rolled back rather than emitted.
|
|
26
|
+
|
|
27
|
+
/// Canonical transform id, as registered with the pipeline.
|
|
28
|
+
pub const TRANSFORM_ID: &str = "log_field_fold";
|
|
29
|
+
|
|
30
|
+
/// Semantic version of this transform's output behavior.
|
|
31
|
+
pub const TRANSFORM_VERSION: &str = "1.0.0";
|
|
32
|
+
|
|
33
|
+
/// First line of the folded blob. Collision-unlikely in real logs; any actual collision is caught
|
|
34
|
+
/// by the pipeline's round-trip safety gate, so it never corrupts data.
|
|
35
|
+
const HEADER: &str = "__tf_logfold1__";
|
|
36
|
+
|
|
37
|
+
/// Placeholder standing in for a captured variable token inside a template.
|
|
38
|
+
const PH: char = '\u{0}';
|
|
39
|
+
|
|
40
|
+
/// Minimum number of lines worth folding, and the maximum distinct-template fraction below which a
|
|
41
|
+
/// fold can pay off. Both are cheap early-outs; the pipeline also rolls back any net token
|
|
42
|
+
/// regression, so these are heuristics, not the correctness boundary.
|
|
43
|
+
const MIN_LINES: usize = 3;
|
|
44
|
+
|
|
45
|
+
use regex::Regex;
|
|
46
|
+
use std::sync::OnceLock;
|
|
47
|
+
|
|
48
|
+
/// Matches a maximal variable token: a `0x…` hex literal or a run of decimal digits. Linear-time
|
|
49
|
+
/// (no backtracking); see `deny.toml`.
|
|
50
|
+
fn var_pattern() -> &'static Regex {
|
|
51
|
+
static RE: OnceLock<Regex> = OnceLock::new();
|
|
52
|
+
RE.get_or_init(|| Regex::new(r"0x[0-9a-fA-F]+|[0-9]+").expect("var_pattern is a valid literal"))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Replaces each variable token in `segment` with [`PH`], returning the template and the captured
|
|
56
|
+
/// tokens in left-to-right order. `caps` never contain [`PH`], `\n`, or spaces (the pattern only
|
|
57
|
+
/// matches `[0-9a-fA-F]`/`x`), which keeps the row serialization below unambiguous.
|
|
58
|
+
fn templatize(segment: &str) -> (String, Vec<&str>) {
|
|
59
|
+
let re = var_pattern();
|
|
60
|
+
let mut template = String::with_capacity(segment.len());
|
|
61
|
+
let mut caps = Vec::new();
|
|
62
|
+
let mut last = 0;
|
|
63
|
+
for m in re.find_iter(segment) {
|
|
64
|
+
template.push_str(&segment[last..m.start()]);
|
|
65
|
+
template.push(PH);
|
|
66
|
+
caps.push(m.as_str());
|
|
67
|
+
last = m.end();
|
|
68
|
+
}
|
|
69
|
+
template.push_str(&segment[last..]);
|
|
70
|
+
(template, caps)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Folds runs of templated lines in `input` into a header + a JSON array of distinct templates +
|
|
74
|
+
/// one compact row (`template_id` then captured values) per original line. Returns `input`
|
|
75
|
+
/// unchanged when folding cannot help (too few lines, no shared templates, or a line contains the
|
|
76
|
+
/// placeholder char). Never panics.
|
|
77
|
+
pub fn fold_log(input: &str) -> String {
|
|
78
|
+
if input.is_empty() || input.contains(PH) {
|
|
79
|
+
return input.to_string();
|
|
80
|
+
}
|
|
81
|
+
// split_inclusive keeps each line's trailing '\n' as part of the segment, so reassembly is
|
|
82
|
+
// byte-exact (CRLF, blank lines, and a missing final newline are all preserved).
|
|
83
|
+
let segments: Vec<&str> = input.split_inclusive('\n').collect();
|
|
84
|
+
if segments.len() < MIN_LINES {
|
|
85
|
+
return input.to_string();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let mut templates: Vec<String> = Vec::new();
|
|
89
|
+
let mut ids: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
|
|
90
|
+
let mut rows: Vec<(usize, Vec<&str>)> = Vec::with_capacity(segments.len());
|
|
91
|
+
for seg in &segments {
|
|
92
|
+
let (template, caps) = templatize(seg);
|
|
93
|
+
let id = *ids.entry(template.clone()).or_insert_with(|| {
|
|
94
|
+
templates.push(template);
|
|
95
|
+
templates.len() - 1
|
|
96
|
+
});
|
|
97
|
+
rows.push((id, caps));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Only worth it if templates are actually shared (skeleton text amortizes). If every line is
|
|
101
|
+
// its own template there is nothing to save; let the pipeline keep the original.
|
|
102
|
+
if templates.len() >= segments.len() {
|
|
103
|
+
return input.to_string();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let mut out = String::with_capacity(input.len() / 2);
|
|
107
|
+
out.push_str(HEADER);
|
|
108
|
+
out.push('\n');
|
|
109
|
+
out.push_str(&serde_json::to_string(&templates).expect("Vec<String> always serializes"));
|
|
110
|
+
for (id, caps) in &rows {
|
|
111
|
+
out.push('\n');
|
|
112
|
+
out.push_str(&id.to_string());
|
|
113
|
+
for cap in caps {
|
|
114
|
+
out.push(' ');
|
|
115
|
+
out.push_str(cap);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
out
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Inverse of [`fold_log`]: expands a folded blob back to the original text. Returns `input`
|
|
122
|
+
/// unchanged if it is not a well-formed folded blob (so a genuine log that merely starts with the
|
|
123
|
+
/// header is not mangled — the pipeline's [`round_trips`] gate makes that safe either way).
|
|
124
|
+
pub fn unfold_log(input: &str) -> String {
|
|
125
|
+
match try_unfold(input) {
|
|
126
|
+
Some(s) => s,
|
|
127
|
+
None => input.to_string(),
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn try_unfold(input: &str) -> Option<String> {
|
|
132
|
+
let mut lines = input.split('\n');
|
|
133
|
+
if lines.next()? != HEADER {
|
|
134
|
+
return None;
|
|
135
|
+
}
|
|
136
|
+
let templates: Vec<String> = serde_json::from_str(lines.next()?).ok()?;
|
|
137
|
+
let mut out = String::with_capacity(input.len() * 2);
|
|
138
|
+
for row in lines {
|
|
139
|
+
let mut fields = row.split(' ');
|
|
140
|
+
let id: usize = fields.next()?.parse().ok()?;
|
|
141
|
+
let template = templates.get(id)?;
|
|
142
|
+
let mut caps = fields;
|
|
143
|
+
// Interleave the template's constant segments with the captured values. The number of
|
|
144
|
+
// placeholders must equal the number of captures, else this isn't our framing.
|
|
145
|
+
let mut parts = template.split(PH);
|
|
146
|
+
out.push_str(parts.next()?);
|
|
147
|
+
for part in parts {
|
|
148
|
+
out.push_str(caps.next()?);
|
|
149
|
+
out.push_str(part);
|
|
150
|
+
}
|
|
151
|
+
if caps.next().is_some() {
|
|
152
|
+
return None; // more captures than placeholders → malformed
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
Some(out)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// True iff unfolding `after` reproduces `before` exactly. The pipeline's safety gate for this
|
|
159
|
+
/// transform — folding is only adopted when this holds.
|
|
160
|
+
pub fn round_trips(before: &[u8], after: &[u8]) -> bool {
|
|
161
|
+
let (Ok(before_s), Ok(after_s)) = (std::str::from_utf8(before), std::str::from_utf8(after))
|
|
162
|
+
else {
|
|
163
|
+
return false;
|
|
164
|
+
};
|
|
165
|
+
unfold_log(after_s) == before_s
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[cfg(test)]
|
|
169
|
+
mod tests {
|
|
170
|
+
use super::*;
|
|
171
|
+
|
|
172
|
+
fn assert_lossless(input: &str) {
|
|
173
|
+
let folded = fold_log(input);
|
|
174
|
+
assert!(
|
|
175
|
+
round_trips(input.as_bytes(), folded.as_bytes()),
|
|
176
|
+
"round_trips() rejected the fold of {input:?}"
|
|
177
|
+
);
|
|
178
|
+
assert_eq!(
|
|
179
|
+
unfold_log(&folded),
|
|
180
|
+
input,
|
|
181
|
+
"unfold != original for {input:?}"
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[test]
|
|
186
|
+
fn folds_templated_log_and_emits_skeleton_once() {
|
|
187
|
+
// A realistically-sized run of one skeleton, so the shared template amortizes: the header +
|
|
188
|
+
// template JSON is paid once, the per-line skeleton text is not.
|
|
189
|
+
let input: String = (0..40)
|
|
190
|
+
.map(|i| format!("req=req-{i:04} status=200 ms={}\n", 30 + i % 20))
|
|
191
|
+
.collect();
|
|
192
|
+
let folded = fold_log(&input);
|
|
193
|
+
assert!(
|
|
194
|
+
folded.starts_with(HEADER),
|
|
195
|
+
"expected folded form, got {folded:?}"
|
|
196
|
+
);
|
|
197
|
+
// the shared skeleton word "status" is emitted once (in the single template), not per line.
|
|
198
|
+
assert_eq!(folded.matches("status").count(), 1);
|
|
199
|
+
assert!(
|
|
200
|
+
folded.len() < input.len(),
|
|
201
|
+
"fold ({}) not smaller than input ({})",
|
|
202
|
+
folded.len(),
|
|
203
|
+
input.len()
|
|
204
|
+
);
|
|
205
|
+
assert_lossless(&input);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#[test]
|
|
209
|
+
fn preserves_crlf_blank_lines_and_missing_final_newline() {
|
|
210
|
+
assert_lossless("a=1\r\na=2\r\na=3\r\n");
|
|
211
|
+
assert_lossless("x=1\n\nx=2\n\nx=3\n");
|
|
212
|
+
assert_lossless("x=1\nx=2\nx=3"); // no trailing newline
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[test]
|
|
216
|
+
fn no_shared_templates_is_left_unchanged() {
|
|
217
|
+
// every line a distinct skeleton → nothing to fold.
|
|
218
|
+
let input = "alpha\nbeta gamma\ndelta epsilon zeta\n";
|
|
219
|
+
assert_eq!(fold_log(input), input);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[test]
|
|
223
|
+
fn fewer_than_min_lines_is_left_unchanged() {
|
|
224
|
+
let input = "req=1 ok\nreq=2 ok\n";
|
|
225
|
+
assert_eq!(fold_log(input), input);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#[test]
|
|
229
|
+
fn empty_input_is_a_noop() {
|
|
230
|
+
assert_eq!(fold_log(""), "");
|
|
231
|
+
assert_eq!(unfold_log(""), "");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#[test]
|
|
235
|
+
fn lines_with_no_variable_tokens_still_fold_when_identical() {
|
|
236
|
+
// three identical constant lines share one (capture-less) template.
|
|
237
|
+
let input = "heartbeat ok\nheartbeat ok\nheartbeat ok\n";
|
|
238
|
+
assert_lossless(input);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[test]
|
|
242
|
+
fn hex_and_decimal_tokens_both_captured() {
|
|
243
|
+
let input = "addr=0x1f val=10\naddr=0x2a val=20\naddr=0x3b val=30\n";
|
|
244
|
+
assert_lossless(input);
|
|
245
|
+
// the "addr=" / "val=" skeleton is shared → one template.
|
|
246
|
+
assert!(fold_log(input).starts_with(HEADER));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#[test]
|
|
250
|
+
fn input_containing_the_placeholder_char_is_not_folded() {
|
|
251
|
+
let input = "a\u{0}b\na\u{0}c\na\u{0}d\n";
|
|
252
|
+
assert_eq!(fold_log(input), input);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#[test]
|
|
256
|
+
fn genuine_input_shaped_like_the_header_round_trips_safely() {
|
|
257
|
+
// If real text starts with our header, unfold must not silently corrupt it: fold_log of a
|
|
258
|
+
// 2-line input is a no-op (< MIN_LINES), and round_trips gates the pipeline regardless.
|
|
259
|
+
let input = "__tf_logfold1__\n[\"x\"]\n0 boom";
|
|
260
|
+
let folded = fold_log(input);
|
|
261
|
+
// Either it wasn't folded (identity) or, if it were, the pipeline would only adopt it when
|
|
262
|
+
// round_trips holds — so the original is always recoverable either way.
|
|
263
|
+
assert_eq!(unfold_log(&folded), input);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
use proptest::prelude::*;
|
|
267
|
+
|
|
268
|
+
// Lines built from a small alphabet of skeletons + digit/hex fields, so templates recur (the
|
|
269
|
+
// regime the fold targets) while still exercising CRLF, blanks, and missing final newlines.
|
|
270
|
+
fn arb_log() -> impl Strategy<Value = String> {
|
|
271
|
+
let line = prop_oneof![
|
|
272
|
+
(0u32..999u32).prop_map(|n| format!("req={n} status=200")),
|
|
273
|
+
(0u32..999u32).prop_map(|n| format!("addr=0x{n:x} ok")),
|
|
274
|
+
Just("heartbeat".to_string()),
|
|
275
|
+
"[a-z ]{0,8}".prop_map(|s| s),
|
|
276
|
+
];
|
|
277
|
+
(prop::collection::vec(line, 0..40), any::<bool>()).prop_map(|(lines, trailing)| {
|
|
278
|
+
let mut s = lines.join("\n");
|
|
279
|
+
if trailing && !s.is_empty() {
|
|
280
|
+
s.push('\n');
|
|
281
|
+
}
|
|
282
|
+
s
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
proptest! {
|
|
287
|
+
// Core safety guarantee: folding never loses data. For ANY log-ish input, unfolding the
|
|
288
|
+
// folded form reproduces it exactly, and round_trips() (the pipeline's gate) agrees.
|
|
289
|
+
#[test]
|
|
290
|
+
fn fold_then_unfold_is_the_identity(input in arb_log()) {
|
|
291
|
+
let folded = fold_log(&input);
|
|
292
|
+
prop_assert!(round_trips(input.as_bytes(), folded.as_bytes()));
|
|
293
|
+
prop_assert_eq!(unfold_log(&folded), input);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -16,7 +16,7 @@ name = "tokenfold_py"
|
|
|
16
16
|
crate-type = ["cdylib", "rlib"]
|
|
17
17
|
|
|
18
18
|
[dependencies]
|
|
19
|
-
tokenfold-core = { version = "0.3.
|
|
19
|
+
tokenfold-core = { version = "0.3.3", path = "../tokenfold-core" }
|
|
20
20
|
serde_json = { workspace = true }
|
|
21
21
|
pyo3 = { version = "0.29.0", features = ["abi3-py39"] }
|
|
22
22
|
|
|
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
|