@compaction/cli 0.1.2 → 0.1.4
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.
- package/README.md +1 -1
- package/dist/cli/commands/compact.js +6 -0
- package/dist/cli/commands/init.js +184 -157
- package/dist/cli/commands/run.js +5 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/onboarding/InitTui.d.ts +52 -0
- package/dist/cli/onboarding/InitTui.js +153 -0
- package/dist/cli/onboarding/model.d.ts +39 -0
- package/dist/cli/onboarding/model.js +77 -0
- package/dist/cli/onboarding/should-use-tui.d.ts +25 -0
- package/dist/cli/onboarding/should-use-tui.js +35 -0
- package/dist/cli/onboarding/wordmark.d.ts +28 -0
- package/dist/cli/onboarding/wordmark.js +72 -0
- package/dist/core/aggregate-format.d.ts +3 -1
- package/dist/core/aggregate-format.js +18 -0
- package/dist/core/report-generator.d.ts +13 -0
- package/dist/core/report-generator.js +16 -0
- package/dist/core/run-aggregator.d.ts +19 -0
- package/dist/core/run-aggregator.js +68 -3
- package/dist/core/session-aggregate.d.ts +43 -0
- package/dist/core/session-aggregate.js +89 -7
- package/dist/core/types.d.ts +22 -0
- package/package.json +5 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { classifyDistinctness } from "./trace-fingerprint.js";
|
|
3
4
|
const REQUIRED_STRING_FIELDS = ["run_id", "trace_title", "policy_name"];
|
|
4
5
|
const REQUIRED_NUMBER_FIELDS = [
|
|
5
6
|
"original_input_tokens",
|
|
@@ -25,6 +26,51 @@ function roundTo(value, digits) {
|
|
|
25
26
|
const factor = 10 ** digits;
|
|
26
27
|
return Math.round((value + Number.EPSILON) * factor) / factor;
|
|
27
28
|
}
|
|
29
|
+
/** Read the OPTIONAL content fingerprint digest from a report (older reports have none). */
|
|
30
|
+
function fingerprintDigestOf(report) {
|
|
31
|
+
const fp = report.trace_fingerprint;
|
|
32
|
+
return fp && typeof fp.content_sha256 === "string" && fp.content_sha256.length > 0 ? fp.content_sha256 : null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* De-duplicate loaded reports by content fingerprint for a duplicate-safe headline.
|
|
36
|
+
*
|
|
37
|
+
* Keeps the FIRST report per distinct fingerprint; later same-fingerprint reports are duplicates
|
|
38
|
+
* and are dropped from the totals so a re-captured run never double-counts its savings. Reports
|
|
39
|
+
* without a fingerprint are each kept (cannot dedup what cannot be identified). PURE; never
|
|
40
|
+
* inflates savings.
|
|
41
|
+
*/
|
|
42
|
+
function dedupeReportsByFingerprint(reports) {
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
const distinctReports = [];
|
|
45
|
+
let withFingerprint = 0;
|
|
46
|
+
let withoutFingerprint = 0;
|
|
47
|
+
for (const loaded of reports) {
|
|
48
|
+
const fp = fingerprintDigestOf(loaded.report);
|
|
49
|
+
if (fp !== null) {
|
|
50
|
+
withFingerprint += 1;
|
|
51
|
+
if (seen.has(fp))
|
|
52
|
+
continue;
|
|
53
|
+
seen.add(fp);
|
|
54
|
+
distinctReports.push(loaded);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
withoutFingerprint += 1;
|
|
58
|
+
distinctReports.push(loaded);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const classified = classifyDistinctness(reports.map((loaded) => ({ runId: loaded.report.run_id, content_sha256: fingerprintDigestOf(loaded.report) })));
|
|
62
|
+
const distinctRunCount = classified.distinct_verified_count + withoutFingerprint;
|
|
63
|
+
return {
|
|
64
|
+
distinctReports,
|
|
65
|
+
distinctness: {
|
|
66
|
+
total_runs: reports.length,
|
|
67
|
+
distinct_runs: distinctRunCount,
|
|
68
|
+
duplicate_runs: reports.length - distinctRunCount,
|
|
69
|
+
runs_with_fingerprint: withFingerprint,
|
|
70
|
+
runs_without_fingerprint: withoutFingerprint
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
28
74
|
function addReportToBucket(bucket, report) {
|
|
29
75
|
bucket.total_runs += 1;
|
|
30
76
|
bucket.total_original_input_tokens += report.original_input_tokens;
|
|
@@ -101,6 +147,9 @@ export async function readCompactionRunReports(runsDirectory = ".compaction/runs
|
|
|
101
147
|
return { reports, skippedFiles };
|
|
102
148
|
}
|
|
103
149
|
export function aggregateCompactionReports(reports, skippedFiles = [], generatedAt = new Date().toISOString(), sourceGlob = ".compaction/runs/*/report.json") {
|
|
150
|
+
// Duplicate-safe headline: fold totals over DISTINCT reports only (a run captured/aggregated
|
|
151
|
+
// twice with the same content fingerprint is counted once). Never inflates summed savings.
|
|
152
|
+
const { distinctReports, distinctness } = dedupeReportsByFingerprint(reports);
|
|
104
153
|
const summary = {
|
|
105
154
|
...emptyBucket(),
|
|
106
155
|
average_percent_reduction: 0,
|
|
@@ -108,11 +157,12 @@ export function aggregateCompactionReports(reports, skippedFiles = [], generated
|
|
|
108
157
|
savings_by_waste_pattern: {},
|
|
109
158
|
top_savings_runs: [],
|
|
110
159
|
skipped_files: skippedFiles,
|
|
160
|
+
distinctness,
|
|
111
161
|
generated_at: generatedAt,
|
|
112
162
|
source_glob: sourceGlob
|
|
113
163
|
};
|
|
114
164
|
let percentReductionTotal = 0;
|
|
115
|
-
for (const { path, report } of
|
|
165
|
+
for (const { path, report } of distinctReports) {
|
|
116
166
|
addReportToBucket(summary, report);
|
|
117
167
|
percentReductionTotal += report.percent_reduction;
|
|
118
168
|
const policyBucket = (summary.savings_by_policy[report.policy_name] ??= emptyBucket());
|
|
@@ -131,7 +181,8 @@ export function aggregateCompactionReports(reports, skippedFiles = [], generated
|
|
|
131
181
|
waste_pattern: report.waste_pattern
|
|
132
182
|
});
|
|
133
183
|
}
|
|
134
|
-
summary.average_percent_reduction =
|
|
184
|
+
summary.average_percent_reduction =
|
|
185
|
+
distinctReports.length === 0 ? 0 : roundTo(percentReductionTotal / distinctReports.length, 2);
|
|
135
186
|
normalizeBucket(summary);
|
|
136
187
|
for (const bucket of Object.values(summary.savings_by_policy)) {
|
|
137
188
|
normalizeBucket(bucket);
|
|
@@ -156,6 +207,18 @@ export async function createRunSummary(runsDirectory = ".compaction/runs") {
|
|
|
156
207
|
function formatCurrency(value) {
|
|
157
208
|
return `$${value.toFixed(6)}`;
|
|
158
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* "distinct runs N of M (K duplicates detected)" — a count of distinct captured runs by content
|
|
212
|
+
* fingerprint. The totals above are summed over the distinct set, so a re-captured run never
|
|
213
|
+
* double-counts. NOT a billing/provider/semantic claim.
|
|
214
|
+
*/
|
|
215
|
+
function distinctnessLine(d) {
|
|
216
|
+
const noFp = d.runs_without_fingerprint > 0
|
|
217
|
+
? `; ${d.runs_without_fingerprint} run(s) without a fingerprint counted as distinct (cannot de-duplicate)`
|
|
218
|
+
: "";
|
|
219
|
+
return (`distinct runs ${d.distinct_runs} of ${d.total_runs} (${d.duplicate_runs} duplicate(s) detected by content fingerprint, ` +
|
|
220
|
+
`excluded from the headline so savings are not double-counted)${noFp}`);
|
|
221
|
+
}
|
|
159
222
|
function formatBucketLines(bucket) {
|
|
160
223
|
return [
|
|
161
224
|
`runs: ${bucket.total_runs}`,
|
|
@@ -184,6 +247,7 @@ export function formatRunSummary(summary) {
|
|
|
184
247
|
`Generated at: ${summary.generated_at}`,
|
|
185
248
|
"",
|
|
186
249
|
"Totals",
|
|
250
|
+
`- ${distinctnessLine(summary.distinctness)}`,
|
|
187
251
|
...formatBucketLines(summary).map((line) => `- ${line}`),
|
|
188
252
|
`- average percent reduction: ${summary.average_percent_reduction.toFixed(2)}%`,
|
|
189
253
|
"",
|
|
@@ -213,6 +277,7 @@ export function formatRunSummaryMarkdown(summary) {
|
|
|
213
277
|
"",
|
|
214
278
|
"## Totals",
|
|
215
279
|
"",
|
|
280
|
+
`- Distinct runs: ${distinctnessLine(summary.distinctness)}`,
|
|
216
281
|
`- Total runs: ${summary.total_runs}`,
|
|
217
282
|
`- Total original input tokens: ${summary.total_original_input_tokens}`,
|
|
218
283
|
`- Total compacted input tokens: ${summary.total_compacted_input_tokens}`,
|
|
@@ -221,7 +286,7 @@ export function formatRunSummaryMarkdown(summary) {
|
|
|
221
286
|
`- Total cost before per run: ${formatCurrency(summary.total_cost_before_per_run)}`,
|
|
222
287
|
`- Total cost after per run: ${formatCurrency(summary.total_cost_after_per_run)}`,
|
|
223
288
|
`- Total saving per run: ${formatCurrency(summary.total_saving_per_run)}`,
|
|
224
|
-
`- Saving figures are estimated and summed over the ${summary.
|
|
289
|
+
`- Saving figures are estimated and summed over the ${summary.distinctness.distinct_runs} distinct recorded run${summary.distinctness.distinct_runs === 1 ? "" : "s"} (${summary.distinctness.duplicate_runs} duplicate(s) detected by content fingerprint and excluded).`,
|
|
225
290
|
"",
|
|
226
291
|
"## Savings by policy",
|
|
227
292
|
"",
|
|
@@ -46,6 +46,14 @@ export interface RunRecord {
|
|
|
46
46
|
run_directory: string;
|
|
47
47
|
report_path: string;
|
|
48
48
|
model: string;
|
|
49
|
+
/**
|
|
50
|
+
* Content-addressed distinctness fingerprint digest of the source trace, when the run's
|
|
51
|
+
* report.json carried one (`trace_fingerprint.content_sha256`). Used to de-duplicate a
|
|
52
|
+
* run captured/aggregated twice (same digest → counted once) so headline savings are never
|
|
53
|
+
* inflated. `null` for older reports without a fingerprint — those are treated as distinct
|
|
54
|
+
* (cannot dedup what cannot be identified) and noted honestly, never collapsed.
|
|
55
|
+
*/
|
|
56
|
+
trace_fingerprint: string | null;
|
|
49
57
|
/** Token evidence tier for this run (drives the provider-reported vs local-estimate split). */
|
|
50
58
|
token_evidence: RunTokenEvidence;
|
|
51
59
|
input_tokens_before: number;
|
|
@@ -73,6 +81,32 @@ export interface SkippedRunDirectory {
|
|
|
73
81
|
run_directory: string;
|
|
74
82
|
reason: string;
|
|
75
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Distinctness (duplicate-safety) summary over a set of runs, by content fingerprint.
|
|
86
|
+
*
|
|
87
|
+
* This is a count of DISTINCT captured runs by one-way content fingerprint — NOT a billing,
|
|
88
|
+
* provider, or semantic claim. A run captured/aggregated twice with the SAME fingerprint is a
|
|
89
|
+
* duplicate and is counted once; a run WITHOUT a fingerprint cannot be de-duplicated and is
|
|
90
|
+
* treated as distinct (and reported in `runs_without_fingerprint`), never collapsed.
|
|
91
|
+
*
|
|
92
|
+
* The headline token/cost/savings totals in `AggregateTotals` are computed over the
|
|
93
|
+
* duplicate-safe set (each distinct run once), so a re-captured run never double-counts.
|
|
94
|
+
*/
|
|
95
|
+
export interface DistinctnessSummary {
|
|
96
|
+
/** Total runs considered before de-duplication (M in "distinct N of M"). */
|
|
97
|
+
total_runs: number;
|
|
98
|
+
/**
|
|
99
|
+
* Distinct runs after de-duplication (N): distinct verified fingerprints + every run without
|
|
100
|
+
* a fingerprint (each unidentifiable run is its own distinct contribution).
|
|
101
|
+
*/
|
|
102
|
+
distinct_runs: number;
|
|
103
|
+
/** Duplicate runs detected and excluded from the headline double-count (K = total - distinct). */
|
|
104
|
+
duplicate_runs: number;
|
|
105
|
+
/** Runs whose fingerprint was present and verifiable (contributed to `distinct_runs`, de-duplicated). */
|
|
106
|
+
runs_with_fingerprint: number;
|
|
107
|
+
/** Runs with NO fingerprint (older reports): each counted as distinct; cannot be de-duplicated. */
|
|
108
|
+
runs_without_fingerprint: number;
|
|
109
|
+
}
|
|
76
110
|
/** Aggregated token/cost/savings totals over a set of runs, with the evidence split. */
|
|
77
111
|
export interface AggregateTotals {
|
|
78
112
|
run_count: number;
|
|
@@ -134,6 +168,8 @@ export interface SessionSummary {
|
|
|
134
168
|
};
|
|
135
169
|
totals: AggregateTotals;
|
|
136
170
|
verification: VerificationCounts;
|
|
171
|
+
/** Duplicate-safety summary for this session's runs (distinct N of M by content fingerprint). */
|
|
172
|
+
distinctness: DistinctnessSummary;
|
|
137
173
|
}
|
|
138
174
|
export interface AggregateReport {
|
|
139
175
|
aggregate_id: string;
|
|
@@ -146,6 +182,12 @@ export interface AggregateReport {
|
|
|
146
182
|
session_count: number;
|
|
147
183
|
totals: AggregateTotals;
|
|
148
184
|
verification: VerificationCounts;
|
|
185
|
+
/**
|
|
186
|
+
* Cross-session duplicate-safety summary (distinct N of M captured runs by content fingerprint).
|
|
187
|
+
* The headline `totals` above are computed over the duplicate-safe set, so a run captured/
|
|
188
|
+
* aggregated twice with the same fingerprint is counted once and never inflates savings.
|
|
189
|
+
*/
|
|
190
|
+
distinctness: DistinctnessSummary;
|
|
149
191
|
sessions: SessionSummary[];
|
|
150
192
|
/** Provider/runtime breakdown (operator-asserted provider label; unlabeled when absent). */
|
|
151
193
|
by_provider: BreakdownBucket[];
|
|
@@ -166,6 +208,7 @@ export interface AggregateEvidenceLabels {
|
|
|
166
208
|
compactions: string;
|
|
167
209
|
verification: string;
|
|
168
210
|
provider_runtime: string;
|
|
211
|
+
distinctness: string;
|
|
169
212
|
}
|
|
170
213
|
export declare const AGGREGATE_EVIDENCE_LABELS: AggregateEvidenceLabels;
|
|
171
214
|
/** True when a run passes every supplied local-label filter (case-sensitive exact match). */
|
|
@@ -33,6 +33,7 @@ import { randomUUID } from "node:crypto";
|
|
|
33
33
|
import { readdir, readFile } from "node:fs/promises";
|
|
34
34
|
import { join } from "node:path";
|
|
35
35
|
import { NO_LABEL, parseRunLabels } from "./run-labels.js";
|
|
36
|
+
import { classifyDistinctness } from "./trace-fingerprint.js";
|
|
36
37
|
export const AGGREGATE_ARTIFACT_DIRECTORY = ".compaction/aggregates";
|
|
37
38
|
export const AGGREGATE_EVIDENCE_LABELS = {
|
|
38
39
|
input_tokens: "input tokens: provider-reported where the run's token accounting was measured (provider usage " +
|
|
@@ -50,7 +51,12 @@ export const AGGREGATE_EVIDENCE_LABELS = {
|
|
|
50
51
|
"task-check outcomes copied from each run's strong-eval; runs without a strong-eval are not_evaluated, " +
|
|
51
52
|
"never counted as a pass. These are DETERMINISTIC recoverability checks, NOT semantic guarantees.",
|
|
52
53
|
provider_runtime: "provider/runtime: operator-ASSERTED local label (never provider-verified); runs without a provider " +
|
|
53
|
-
"label are grouped under 'unlabeled'. This label is distinct from the token-evidence tier."
|
|
54
|
+
"label are grouped under 'unlabeled'. This label is distinct from the token-evidence tier.",
|
|
55
|
+
distinctness: "distinct runs: a count of DISTINCT captured runs by one-way content fingerprint (SHA-256 over the " +
|
|
56
|
+
"normalized trace). A run captured/aggregated twice with the SAME fingerprint is a duplicate and is " +
|
|
57
|
+
"counted once, so headline savings are never inflated; runs without a fingerprint cannot be " +
|
|
58
|
+
"de-duplicated and are each counted as distinct. This is a local content-distinctness count — NOT a " +
|
|
59
|
+
"billing, provider, or semantic claim."
|
|
54
60
|
};
|
|
55
61
|
const STANDARD_LIMITATIONS = [
|
|
56
62
|
"Aggregated from LOCAL artifacts only (.compaction/runs/*/{report.json,strong-eval.json,run-labels.json}); no provider calls, no upload, no telemetry.",
|
|
@@ -59,11 +65,66 @@ const STANDARD_LIMITATIONS = [
|
|
|
59
65
|
"Output tokens are summed only over runs that recorded them; runs without a strong-eval contribute unknown output tokens and are counted separately, never inferred.",
|
|
60
66
|
"Verification outcomes are deterministic recoverability checks copied from each run's strong-eval; they are NOT semantic or meaning-preservation guarantees. Runs without a strong-eval are not_evaluated, never a pass.",
|
|
61
67
|
"Labels (project/workflow/provider/user-label/session) are operator-supplied local tags with no evidence weight; provider here is operator-asserted, not provider-verified.",
|
|
68
|
+
"Headline savings are duplicate-safe: runs sharing a content fingerprint (a re-captured/re-aggregated run) are counted once; runs without a fingerprint cannot be de-duplicated and are each counted as distinct. This is a local content-distinctness count, not a billing/provider/semantic claim.",
|
|
62
69
|
"This is a LOCAL, file-based rollup. There is no hosted dashboard, database, auth, billing, model routing, or provider integration."
|
|
63
70
|
];
|
|
64
71
|
function round6(value) {
|
|
65
72
|
return Number((value + Number.EPSILON).toFixed(6));
|
|
66
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* De-duplicate a set of runs by content fingerprint for duplicate-safe headline totals.
|
|
76
|
+
*
|
|
77
|
+
* Keeps the FIRST run per distinct verified fingerprint; later runs with the same fingerprint
|
|
78
|
+
* are duplicates and are dropped from the headline (so a re-captured run never double-counts its
|
|
79
|
+
* savings). Runs WITHOUT a fingerprint are kept as-is — each is its own distinct contribution,
|
|
80
|
+
* because distinctness that cannot be verified is never collapsed (the anti-overcount contract in
|
|
81
|
+
* `classifyDistinctness`). Returns the duplicate-safe run set plus an honest distinctness summary.
|
|
82
|
+
*
|
|
83
|
+
* PURE: order-stable for the kept runs; makes savings MORE conservative, never larger.
|
|
84
|
+
*/
|
|
85
|
+
function dedupeRunsByFingerprint(runs) {
|
|
86
|
+
const seenFingerprints = new Set();
|
|
87
|
+
const distinctRuns = [];
|
|
88
|
+
let runsWithFingerprint = 0;
|
|
89
|
+
let runsWithoutFingerprint = 0;
|
|
90
|
+
for (const run of runs) {
|
|
91
|
+
const fp = run.trace_fingerprint;
|
|
92
|
+
if (typeof fp === "string" && fp.length > 0) {
|
|
93
|
+
runsWithFingerprint += 1;
|
|
94
|
+
if (seenFingerprints.has(fp))
|
|
95
|
+
continue; // duplicate fingerprint: excluded from headline.
|
|
96
|
+
seenFingerprints.add(fp);
|
|
97
|
+
distinctRuns.push(run);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
runsWithoutFingerprint += 1;
|
|
101
|
+
distinctRuns.push(run); // unidentifiable: always its own distinct contribution.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// classifyDistinctness gives the verified-distinct count over the SAME records; distinct_runs is
|
|
105
|
+
// that verified-distinct count plus every unidentifiable run (each distinct by construction).
|
|
106
|
+
const classified = classifyDistinctness(runs.map((run) => ({ runId: run.run_id, content_sha256: run.trace_fingerprint })));
|
|
107
|
+
const distinctRunCount = classified.distinct_verified_count + runsWithoutFingerprint;
|
|
108
|
+
return {
|
|
109
|
+
distinctRuns,
|
|
110
|
+
distinctness: {
|
|
111
|
+
total_runs: runs.length,
|
|
112
|
+
distinct_runs: distinctRunCount,
|
|
113
|
+
duplicate_runs: runs.length - distinctRunCount,
|
|
114
|
+
runs_with_fingerprint: runsWithFingerprint,
|
|
115
|
+
runs_without_fingerprint: runsWithoutFingerprint
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function emptyDistinctness() {
|
|
120
|
+
return {
|
|
121
|
+
total_runs: 0,
|
|
122
|
+
distinct_runs: 0,
|
|
123
|
+
duplicate_runs: 0,
|
|
124
|
+
runs_with_fingerprint: 0,
|
|
125
|
+
runs_without_fingerprint: 0
|
|
126
|
+
};
|
|
127
|
+
}
|
|
67
128
|
function emptyTotals() {
|
|
68
129
|
return {
|
|
69
130
|
run_count: 0,
|
|
@@ -209,9 +270,12 @@ function buildSessions(runs) {
|
|
|
209
270
|
}
|
|
210
271
|
return [...sessions.entries()]
|
|
211
272
|
.map(([sessionId, { explicit, runs: sessionRuns }]) => {
|
|
273
|
+
// Duplicate-safe: fold totals/verification over distinct runs only (re-captured runs with
|
|
274
|
+
// the same fingerprint are excluded from the double-count). run_ids still lists every run.
|
|
275
|
+
const { distinctRuns, distinctness } = dedupeRunsByFingerprint(sessionRuns);
|
|
212
276
|
const totals = emptyTotals();
|
|
213
277
|
const verification = emptyVerification();
|
|
214
|
-
for (const run of
|
|
278
|
+
for (const run of distinctRuns) {
|
|
215
279
|
addRunToTotals(totals, run);
|
|
216
280
|
addRunToVerification(verification, run);
|
|
217
281
|
}
|
|
@@ -227,7 +291,8 @@ function buildSessions(runs) {
|
|
|
227
291
|
user_labels: distinct(sessionRuns.map((r) => r.labels.user_label))
|
|
228
292
|
},
|
|
229
293
|
totals,
|
|
230
|
-
verification
|
|
294
|
+
verification,
|
|
295
|
+
distinctness
|
|
231
296
|
};
|
|
232
297
|
})
|
|
233
298
|
.sort((a, b) => b.totals.total_input_tokens_saved - a.totals.total_input_tokens_saved ||
|
|
@@ -258,9 +323,13 @@ export function buildAggregateReport(runs, options = {}) {
|
|
|
258
323
|
const generatedAt = options.generatedAt ?? new Date().toISOString();
|
|
259
324
|
const filters = options.filters ?? {};
|
|
260
325
|
const matched = runs.filter((run) => runMatchesFilters(run, filters));
|
|
326
|
+
// Duplicate-safe headline: fold cross-session totals/verification over the distinct run set only.
|
|
327
|
+
// A run captured/aggregated twice with the same content fingerprint is counted once; runs without
|
|
328
|
+
// a fingerprint are each distinct (cannot dedup what cannot be identified). Never inflates savings.
|
|
329
|
+
const { distinctRuns, distinctness } = dedupeRunsByFingerprint(matched);
|
|
261
330
|
const totals = emptyTotals();
|
|
262
331
|
const verification = emptyVerification();
|
|
263
|
-
for (const run of
|
|
332
|
+
for (const run of distinctRuns) {
|
|
264
333
|
addRunToTotals(totals, run);
|
|
265
334
|
addRunToVerification(verification, run);
|
|
266
335
|
}
|
|
@@ -275,10 +344,13 @@ export function buildAggregateReport(runs, options = {}) {
|
|
|
275
344
|
session_count: sessions.length,
|
|
276
345
|
totals,
|
|
277
346
|
verification,
|
|
347
|
+
distinctness,
|
|
278
348
|
sessions,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
349
|
+
// Breakdowns also fold over the duplicate-safe distinct run set so per-bucket savings match the
|
|
350
|
+
// duplicate-safe headline (a re-captured run never double-counts inside its label bucket either).
|
|
351
|
+
by_provider: buildBreakdown(distinctRuns, (run) => run.labels.provider ?? NO_LABEL),
|
|
352
|
+
by_project: buildBreakdown(distinctRuns, (run) => run.labels.project ?? NO_LABEL),
|
|
353
|
+
by_workflow: buildBreakdown(distinctRuns, (run) => run.labels.workflow ?? NO_LABEL),
|
|
282
354
|
skipped_run_directories: options.skippedRunDirectories ?? [],
|
|
283
355
|
evidence_labels: AGGREGATE_EVIDENCE_LABELS,
|
|
284
356
|
limitations: STANDARD_LIMITATIONS
|
|
@@ -320,6 +392,15 @@ function readinessFrom(status) {
|
|
|
320
392
|
export function assembleRunRecord(input) {
|
|
321
393
|
const { report, strongEval } = input;
|
|
322
394
|
const notes = [];
|
|
395
|
+
// Content-addressed run identity: read the OPTIONAL trace_fingerprint digest if the report
|
|
396
|
+
// carried one. Older reports omit it; such a run cannot be de-duplicated and is treated as
|
|
397
|
+
// distinct (noted), never collapsed. Read defensively (digest string only; no content).
|
|
398
|
+
const fingerprintDigest = isRecord(report.trace_fingerprint) && typeof report.trace_fingerprint.content_sha256 === "string"
|
|
399
|
+
? report.trace_fingerprint.content_sha256
|
|
400
|
+
: null;
|
|
401
|
+
if (fingerprintDigest === null) {
|
|
402
|
+
notes.push("No trace_fingerprint on report.json: this run is treated as distinct (cannot de-duplicate an unidentifiable run).");
|
|
403
|
+
}
|
|
323
404
|
let tokenEvidence = "local_estimate";
|
|
324
405
|
let inputBefore = report.original_input_tokens;
|
|
325
406
|
let inputAfter = report.compacted_input_tokens;
|
|
@@ -389,6 +470,7 @@ export function assembleRunRecord(input) {
|
|
|
389
470
|
run_directory: input.runDirectory,
|
|
390
471
|
report_path: input.reportPath,
|
|
391
472
|
model,
|
|
473
|
+
trace_fingerprint: fingerprintDigest,
|
|
392
474
|
token_evidence: tokenEvidence,
|
|
393
475
|
input_tokens_before: inputBefore,
|
|
394
476
|
input_tokens_after: inputAfter,
|
package/dist/core/types.d.ts
CHANGED
|
@@ -225,6 +225,28 @@ export interface CompactionReport {
|
|
|
225
225
|
* price-table), NOT billing-confirmed and NOT realized savings.
|
|
226
226
|
*/
|
|
227
227
|
spend_by_source?: SpendBySourceSummary;
|
|
228
|
+
/**
|
|
229
|
+
* Content-addressed per-run distinctness fingerprint of the source trace (additive,
|
|
230
|
+
* OPTIONAL, backward-compatible). When present, it lets the aggregate attribute this
|
|
231
|
+
* run's savings to a verifiable run identity and de-duplicate a run captured/aggregated
|
|
232
|
+
* twice (same fingerprint → counted once) so the headline savings are never inflated.
|
|
233
|
+
*
|
|
234
|
+
* Older reports omit this field; a run WITHOUT a fingerprint cannot be de-duplicated and
|
|
235
|
+
* is treated as distinct (you cannot dedup what you cannot identify) and noted honestly.
|
|
236
|
+
*
|
|
237
|
+
* Privacy: this is a one-way SHA-256 digest plus non-sensitive structural counts — NO
|
|
238
|
+
* message/prompt/tool-output text. Structurally matches `TraceFingerprint` from
|
|
239
|
+
* `src/core/trace-fingerprint.ts` (kept inline here so `types.ts` stays import-free).
|
|
240
|
+
* This is an attribution/integrity signal, NOT a billing, provider, or semantic claim.
|
|
241
|
+
*/
|
|
242
|
+
trace_fingerprint?: {
|
|
243
|
+
/** Algorithm + canonicalization version (e.g. `sha256-canonical-content-v1`). */
|
|
244
|
+
algorithm: string;
|
|
245
|
+
/** Hex SHA-256 digest over the canonical normalized trace content (one-way; never raw content). */
|
|
246
|
+
content_sha256: string;
|
|
247
|
+
/** Non-sensitive structural count: number of messages hashed. */
|
|
248
|
+
message_count: number;
|
|
249
|
+
};
|
|
228
250
|
}
|
|
229
251
|
export interface CompactionPolicy {
|
|
230
252
|
policy_name: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compaction/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -79,10 +79,14 @@
|
|
|
79
79
|
"dependencies": {
|
|
80
80
|
"chalk": "^5.4.1",
|
|
81
81
|
"commander": "^12.1.0",
|
|
82
|
+
"ink": "^5.2.1",
|
|
83
|
+
"react": "^18.3.1",
|
|
82
84
|
"zod": "^3.24.1"
|
|
83
85
|
},
|
|
84
86
|
"devDependencies": {
|
|
85
87
|
"@types/node": "^22.10.2",
|
|
88
|
+
"@types/react": "^18.3.31",
|
|
89
|
+
"ink-testing-library": "^4.0.0",
|
|
86
90
|
"tsx": "^4.19.2",
|
|
87
91
|
"typescript": "^5.7.2",
|
|
88
92
|
"vitest": "^4.1.8"
|