@compaction/cli 0.1.4 → 0.2.0

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.
@@ -0,0 +1,135 @@
1
+ import { evaluateContextStoreFixture } from "./context-store-eval.js";
2
+ /**
3
+ * V0.4 context-store INTERNAL sufficiency report (rung-1).
4
+ *
5
+ * Applies the founder-accepted threshold defaults
6
+ * (`docs/design/v0.4-context-store-thresholds.md`) to the eval fixtures, purely as an
7
+ * INTERNAL regression/confidence gate. It is consumed only by the eval harness/tests — it is
8
+ * NOT wired to any CLI command and produces NO user-facing output.
9
+ *
10
+ * WHAT THIS IS NOT (load-bearing):
11
+ * - NOT a public claim. Passing means the deterministic retriever still behaves on SYNTHETIC
12
+ * fixtures — it is NOT evidence of retrieval correctness for users, savings, semantic
13
+ * preservation, or any large-memory / 300M readiness. Those remain gated.
14
+ * - NO model replay, NO embeddings, NO network.
15
+ *
16
+ * Model:
17
+ * - `sufficiency` fixtures are GATED against the thresholds below (hard axes fail; soft axes
18
+ * warn; latency is recorded only).
19
+ * - `adversarial` fixtures are NOT gated — their low/partial scores are correct. They are
20
+ * regression ANCHORS: the measured score must keep matching the fixture's `expected`.
21
+ */
22
+ export const CONTEXT_STORE_SUFFICIENCY_VERSION = "context-store-sufficiency-v1";
23
+ /**
24
+ * Founder-accepted defaults (2026-06-24): hard recall / source coverage / recoverability
25
+ * (= 1.0 on sufficiency fixtures); soft continuation (advisory ≥ 0.9 — it is a marker proxy,
26
+ * not model replay); latency report-only. INTERNAL thresholds, not public claims.
27
+ */
28
+ export const CONTEXT_STORE_SUFFICIENCY_THRESHOLDS = {
29
+ recall: { kind: "hard", min: 1 },
30
+ source_coverage: { kind: "hard", min: 1 },
31
+ recoverability: { kind: "hard", min: 1 },
32
+ continuation_success: { kind: "soft", min: 0.9 },
33
+ latency_ms: { kind: "report" }
34
+ };
35
+ /** Float tolerance for threshold/anchor comparisons (e.g. recall = 2/3). */
36
+ const TOLERANCE = 1e-9;
37
+ /** The quality axes a fixture is gated/anchored on (latency is handled separately). */
38
+ const QUALITY_AXES = ["recall", "source_coverage", "recoverability", "continuation_success"];
39
+ /**
40
+ * Run every fixture and build the internal sufficiency report. Deterministic apart from the
41
+ * recorded latency (use an injected clock for reproducible reports).
42
+ */
43
+ export function evaluateSufficiency(fixtures, thresholds = CONTEXT_STORE_SUFFICIENCY_THRESHOLDS, options = {}) {
44
+ const hard_failures = [];
45
+ const warnings = [];
46
+ const anchor_failures = [];
47
+ const latency = [];
48
+ let sufficiencyCount = 0;
49
+ let adversarialCount = 0;
50
+ for (const fixture of fixtures) {
51
+ const result = evaluateContextStoreFixture(fixture, { clock: options.clock });
52
+ latency.push({ fixture: fixture.name, latency_ms: result.latency_ms }); // report-only
53
+ const scores = {
54
+ recall: result.recall,
55
+ source_coverage: result.source_coverage,
56
+ recoverability: result.recoverability,
57
+ continuation_success: result.continuation_success
58
+ };
59
+ if ((fixture.role ?? "sufficiency") === "adversarial") {
60
+ adversarialCount += 1;
61
+ const expected = fixture.expected ?? {};
62
+ for (const axis of QUALITY_AXES) {
63
+ const exp = expected[axis];
64
+ if (exp !== undefined && Math.abs(scores[axis] - exp) > TOLERANCE) {
65
+ anchor_failures.push({ fixture: fixture.name, axis, measured: scores[axis], expected: exp });
66
+ }
67
+ }
68
+ }
69
+ else {
70
+ sufficiencyCount += 1;
71
+ for (const axis of QUALITY_AXES) {
72
+ const gate = thresholds[axis];
73
+ if (gate.kind === "report")
74
+ continue;
75
+ if (scores[axis] + TOLERANCE < gate.min) {
76
+ if (gate.kind === "hard") {
77
+ hard_failures.push({ fixture: fixture.name, axis, measured: scores[axis], min: gate.min });
78
+ }
79
+ else {
80
+ warnings.push({ fixture: fixture.name, axis, measured: scores[axis], min: gate.min });
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ return {
87
+ version: CONTEXT_STORE_SUFFICIENCY_VERSION,
88
+ passed: hard_failures.length === 0 && anchor_failures.length === 0,
89
+ sufficiency_fixtures: sufficiencyCount,
90
+ adversarial_fixtures: adversarialCount,
91
+ hard_failures,
92
+ warnings,
93
+ anchor_failures,
94
+ latency
95
+ };
96
+ }
97
+ function pct(value) {
98
+ return `${(value * 100).toFixed(1)}%`;
99
+ }
100
+ /**
101
+ * Internal markdown rendering of the sufficiency report. For local/CI inspection only —
102
+ * not a user-facing surface and not a claim.
103
+ */
104
+ export function formatSufficiencyReportMarkdown(report) {
105
+ const lines = [
106
+ "## Context-store sufficiency report (INTERNAL — not a claim)",
107
+ "",
108
+ `Result: ${report.passed ? "PASS" : "FAIL"} · sufficiency fixtures: ${report.sufficiency_fixtures} · adversarial anchors: ${report.adversarial_fixtures}`,
109
+ ""
110
+ ];
111
+ if (report.hard_failures.length > 0) {
112
+ lines.push("### Hard-axis failures (FAIL)");
113
+ for (const f of report.hard_failures) {
114
+ lines.push(`- ${f.fixture} · ${f.axis}: ${pct(f.measured)} < required ${pct(f.min)}`);
115
+ }
116
+ lines.push("");
117
+ }
118
+ if (report.anchor_failures.length > 0) {
119
+ lines.push("### Adversarial anchor drift (FAIL)");
120
+ for (const f of report.anchor_failures) {
121
+ lines.push(`- ${f.fixture} · ${f.axis}: measured ${pct(f.measured)} ≠ expected ${pct(f.expected)}`);
122
+ }
123
+ lines.push("");
124
+ }
125
+ if (report.warnings.length > 0) {
126
+ lines.push("### Soft warnings (advisory — do not fail)");
127
+ for (const w of report.warnings) {
128
+ lines.push(`- ${w.fixture} · ${w.axis}: ${pct(w.measured)} < advisory ${pct(w.min)}`);
129
+ }
130
+ lines.push("");
131
+ }
132
+ lines.push("_Internal regression gate over synthetic fixtures. Hard axes: recall, source coverage,", "recoverability. Soft (advisory): continuation success (a marker proxy, NOT model replay).", "Latency: recorded only. Passing is NOT evidence of user-facing retrieval correctness,", "savings, semantic preservation, or any large-memory / 300M readiness._");
133
+ return lines.join("\n");
134
+ }
135
+ //# sourceMappingURL=context-store-sufficiency.js.map
@@ -0,0 +1,155 @@
1
+ import type { AgentTrace, StateCapsule, StateCapsuleProvenanceEntry } from "./types.js";
2
+ /**
3
+ * V0.4 context store — rung-1 (local, deterministic, source-grounded).
4
+ *
5
+ * SCOPE (accepted design doc `docs/design/v0.4-context-store-retrieval.md`,
6
+ * 2026-06-24): a durable, local, file-based store of source-pointed context items
7
+ * accumulated across runs, plus DETERMINISTIC local retrieval and assembly under a
8
+ * token budget. This module is the rung-1 core.
9
+ *
10
+ * HARD NON-GOALS (load-bearing — do not add here):
11
+ * - NO semantic / embedding retrieval. Retrieval ranks by deterministic signals only
12
+ * (source-pointer match, lexical overlap, recency). Semantic retrieval needs a model
13
+ * → not free-CLI local-first → deferred to the gated private engine/API.
14
+ * - NO network call, credential, upload, or model invocation. This module is pure +
15
+ * local-file I/O only (see `loadContextStore` / `appendContextItems`).
16
+ * - NO hosted DB / vector store / auth.
17
+ * - NO large-memory / 300M-token / retrieval-correctness claim. Those are gated on the
18
+ * four-axis eval harness (a follow-up increment) and rung-6 evidence.
19
+ *
20
+ * HONESTY: token figures here are `local_estimate` (chars/4), never provider-reported.
21
+ * Every stored item retains its `source_pointer` + recoverability so the assembled
22
+ * context stays recoverable to its sources (the substrate invariant).
23
+ */
24
+ /** Algorithm + schema version, so a persisted store is self-describing. */
25
+ export declare const CONTEXT_STORE_ITEM_VERSION = "context-store-item-v1";
26
+ /**
27
+ * A single source-pointed context item accumulated in the store.
28
+ *
29
+ * `content` is local-only (like a captured trace) and is NEVER uploaded; the store
30
+ * lives under the gitignored `.compaction/` tree. `content_sha256` is the dedup key.
31
+ */
32
+ export interface ContextStoreItem {
33
+ /** Schema version for forward-compatible reads. */
34
+ version: string;
35
+ /** SHA-256 of `content` — the dedup key (reuses the fingerprint discipline). */
36
+ content_sha256: string;
37
+ /** The retained context text (local-only; never uploaded). */
38
+ content: string;
39
+ /** Local token estimate (chars/4) for assembly budgeting. NOT provider-reported. */
40
+ estimated_tokens: number;
41
+ /** Recoverable pointer back to the source (kept so assembly stays recoverable). */
42
+ source_pointer?: string;
43
+ /** Source recoverability, carried straight from the substrate. */
44
+ recoverability: boolean | "unknown";
45
+ /** Per-run distinctness digest (from `computeTraceFingerprint`), if known. */
46
+ trace_fingerprint?: string;
47
+ /** The originating trace id, when known (a deterministic retrieval signal). */
48
+ source_trace_id?: string;
49
+ /** Provenance entry carried from the state-capsule, when available. */
50
+ provenance?: StateCapsuleProvenanceEntry;
51
+ /** Free-form deterministic labels (e.g. "retained_fact", "open_question"). */
52
+ labels: string[];
53
+ /** ISO-8601 creation timestamp (recency signal). Stamped at append time. */
54
+ created_at: string;
55
+ }
56
+ /** Input to `makeContextItem` — everything except the derived hash/token fields. */
57
+ export interface ContextItemInput {
58
+ content: string;
59
+ source_pointer?: string;
60
+ recoverability?: boolean | "unknown";
61
+ trace_fingerprint?: string;
62
+ source_trace_id?: string;
63
+ provenance?: StateCapsuleProvenanceEntry;
64
+ labels?: string[];
65
+ created_at: string;
66
+ }
67
+ /**
68
+ * Build a single context item, deriving the dedup hash + local token estimate.
69
+ * Pure — no I/O, no clock (the caller supplies `created_at` so this stays
70
+ * deterministic and testable).
71
+ */
72
+ export declare function makeContextItem(input: ContextItemInput): ContextStoreItem;
73
+ /**
74
+ * Derive context items from a `StateCapsule`. Each retained fact / open question /
75
+ * safety note becomes a source-pointed item, inheriting the capsule's source pointer +
76
+ * recoverability + the originating trace id (and the supplied per-run fingerprint).
77
+ * Pure — `created_at` is supplied by the caller.
78
+ */
79
+ export declare function contextItemsFromCapsule(capsule: StateCapsule, opts: {
80
+ created_at: string;
81
+ trace_fingerprint?: string;
82
+ }): ContextStoreItem[];
83
+ /**
84
+ * Derive context items from a normalized `AgentTrace` (the local artifact `compaction capture`
85
+ * / `import` produces). Each non-empty message becomes a source-pointed item recoverable to its
86
+ * `trace=<id> message=<id>` origin, tagged with the run's distinctness fingerprint. This is the
87
+ * free-CLI populate path (state-capsules require the gated engine; trace messages do not). Pure —
88
+ * `created_at` is supplied by the caller.
89
+ */
90
+ export declare function contextItemsFromTrace(trace: AgentTrace, opts: {
91
+ created_at: string;
92
+ }): ContextStoreItem[];
93
+ /**
94
+ * Dedup by `content_sha256`, keeping the EARLIEST item deterministically (ties broken
95
+ * by `created_at`, then by the existing order). Reuses the fingerprint discipline:
96
+ * byte-identical content is one item, not many. Output order follows the
97
+ * first-occurrence order of each distinct hash in the input array.
98
+ */
99
+ export declare function dedupeContextItems(items: ContextStoreItem[]): ContextStoreItem[];
100
+ /** A retrieval query: the current step's context + optional source/trace hints. */
101
+ export interface RetrievalQuery {
102
+ /** Free text describing the current step (task, recent messages, etc.). */
103
+ text: string;
104
+ /** Source pointers known to be relevant to the current step (exact-match boost). */
105
+ source_pointers?: string[];
106
+ /** Trace ids known to be relevant to the current step (exact-match boost). */
107
+ trace_ids?: string[];
108
+ }
109
+ export interface RetrievalOptions {
110
+ /** Max items to return (after ranking). Default: all candidates with score > 0. */
111
+ limit?: number;
112
+ /** Weight on exact source-pointer / trace-id match. Default 1. */
113
+ sourceMatchWeight?: number;
114
+ /** Weight on lexical overlap (overlap coefficient in [0,1]). Default 1. */
115
+ lexicalWeight?: number;
116
+ }
117
+ export interface RankedContextItem {
118
+ item: ContextStoreItem;
119
+ score: number;
120
+ source_match: boolean;
121
+ lexical_overlap: number;
122
+ }
123
+ /**
124
+ * Deterministic, local retrieval. Ranks items by:
125
+ * score = sourceMatchWeight * (exact source/trace match ? 1 : 0)
126
+ * + lexicalWeight * lexical overlap coefficient in [0,1]
127
+ *
128
+ * Ties are broken deterministically by recency (newer first) then `content_sha256`
129
+ * ascending, so the same store + query always yields the same order. No model, no
130
+ * embeddings, no network — overlap is a pure word-set intersection.
131
+ */
132
+ export declare function retrieveContextItems(items: ContextStoreItem[], query: RetrievalQuery, options?: RetrievalOptions): RankedContextItem[];
133
+ export interface AssembleOptions {
134
+ /** Token budget for the assembled context (local estimate). */
135
+ tokenBudget: number;
136
+ }
137
+ export interface AssembledContext {
138
+ /** The assembled context text (included items joined, in ranked order). */
139
+ text: string;
140
+ /** Items that fit within the budget (provenance preserved on each). */
141
+ included: ContextStoreItem[];
142
+ /** Items dropped because the budget was exhausted (still recoverable from source). */
143
+ dropped: ContextStoreItem[];
144
+ /** Local token estimate of the assembled context. */
145
+ estimated_tokens: number;
146
+ /** Whether everything offered fit within the budget. */
147
+ within_budget: boolean;
148
+ }
149
+ /**
150
+ * Assemble ranked items into the sufficient active context under a token budget.
151
+ * Greedy in ranked order; dedups by `content_sha256`; PRESERVES the source pointer +
152
+ * provenance on every included item (so the assembled context stays recoverable).
153
+ * Token figures are local estimates.
154
+ */
155
+ export declare function assembleContext(ranked: Array<RankedContextItem | ContextStoreItem>, options: AssembleOptions): AssembledContext;
@@ -0,0 +1,228 @@
1
+ import { contentHash } from "./content-hash.js";
2
+ import { estimateTextTokens } from "./token-estimator.js";
3
+ import { computeTraceFingerprint } from "./trace-fingerprint.js";
4
+ /**
5
+ * V0.4 context store — rung-1 (local, deterministic, source-grounded).
6
+ *
7
+ * SCOPE (accepted design doc `docs/design/v0.4-context-store-retrieval.md`,
8
+ * 2026-06-24): a durable, local, file-based store of source-pointed context items
9
+ * accumulated across runs, plus DETERMINISTIC local retrieval and assembly under a
10
+ * token budget. This module is the rung-1 core.
11
+ *
12
+ * HARD NON-GOALS (load-bearing — do not add here):
13
+ * - NO semantic / embedding retrieval. Retrieval ranks by deterministic signals only
14
+ * (source-pointer match, lexical overlap, recency). Semantic retrieval needs a model
15
+ * → not free-CLI local-first → deferred to the gated private engine/API.
16
+ * - NO network call, credential, upload, or model invocation. This module is pure +
17
+ * local-file I/O only (see `loadContextStore` / `appendContextItems`).
18
+ * - NO hosted DB / vector store / auth.
19
+ * - NO large-memory / 300M-token / retrieval-correctness claim. Those are gated on the
20
+ * four-axis eval harness (a follow-up increment) and rung-6 evidence.
21
+ *
22
+ * HONESTY: token figures here are `local_estimate` (chars/4), never provider-reported.
23
+ * Every stored item retains its `source_pointer` + recoverability so the assembled
24
+ * context stays recoverable to its sources (the substrate invariant).
25
+ */
26
+ /** Algorithm + schema version, so a persisted store is self-describing. */
27
+ export const CONTEXT_STORE_ITEM_VERSION = "context-store-item-v1";
28
+ function normalizeContent(content) {
29
+ return content.trim().replace(/\s+/g, " ");
30
+ }
31
+ /**
32
+ * Build a single context item, deriving the dedup hash + local token estimate.
33
+ * Pure — no I/O, no clock (the caller supplies `created_at` so this stays
34
+ * deterministic and testable).
35
+ */
36
+ export function makeContextItem(input) {
37
+ const content = normalizeContent(input.content);
38
+ return {
39
+ version: CONTEXT_STORE_ITEM_VERSION,
40
+ content_sha256: contentHash(content),
41
+ content,
42
+ estimated_tokens: estimateTextTokens(content),
43
+ source_pointer: input.source_pointer,
44
+ recoverability: input.recoverability ?? "unknown",
45
+ trace_fingerprint: input.trace_fingerprint,
46
+ source_trace_id: input.source_trace_id,
47
+ provenance: input.provenance,
48
+ labels: input.labels ? [...input.labels] : [],
49
+ created_at: input.created_at
50
+ };
51
+ }
52
+ /**
53
+ * Derive context items from a `StateCapsule`. Each retained fact / open question /
54
+ * safety note becomes a source-pointed item, inheriting the capsule's source pointer +
55
+ * recoverability + the originating trace id (and the supplied per-run fingerprint).
56
+ * Pure — `created_at` is supplied by the caller.
57
+ */
58
+ export function contextItemsFromCapsule(capsule, opts) {
59
+ const primaryProvenance = capsule.provenance_entries?.[0];
60
+ const base = {
61
+ source_pointer: capsule.source_pointer ?? primaryProvenance?.source_pointer,
62
+ recoverability: capsule.source_recoverable ?? primaryProvenance?.source_recoverable ?? "unknown",
63
+ source_trace_id: capsule.source_trace_id ?? capsule.traceId,
64
+ provenance: primaryProvenance,
65
+ trace_fingerprint: opts.trace_fingerprint,
66
+ created_at: opts.created_at
67
+ };
68
+ const items = [];
69
+ for (const fact of capsule.retainedFacts) {
70
+ if (fact.trim().length === 0)
71
+ continue;
72
+ items.push(makeContextItem({ ...base, content: fact, labels: ["retained_fact"] }));
73
+ }
74
+ for (const question of capsule.openQuestions) {
75
+ if (question.trim().length === 0)
76
+ continue;
77
+ items.push(makeContextItem({ ...base, content: question, labels: ["open_question"] }));
78
+ }
79
+ for (const note of capsule.safetyNotes) {
80
+ if (note.trim().length === 0)
81
+ continue;
82
+ items.push(makeContextItem({ ...base, content: note, labels: ["safety_note"] }));
83
+ }
84
+ return dedupeContextItems(items);
85
+ }
86
+ /**
87
+ * Derive context items from a normalized `AgentTrace` (the local artifact `compaction capture`
88
+ * / `import` produces). Each non-empty message becomes a source-pointed item recoverable to its
89
+ * `trace=<id> message=<id>` origin, tagged with the run's distinctness fingerprint. This is the
90
+ * free-CLI populate path (state-capsules require the gated engine; trace messages do not). Pure —
91
+ * `created_at` is supplied by the caller.
92
+ */
93
+ export function contextItemsFromTrace(trace, opts) {
94
+ const trace_fingerprint = computeTraceFingerprint(trace).content_sha256;
95
+ const items = [];
96
+ for (const message of trace.messages) {
97
+ if (message.content.trim().length === 0)
98
+ continue;
99
+ const labels = [message.role];
100
+ if (message.toolName)
101
+ labels.push(message.toolName);
102
+ items.push(makeContextItem({
103
+ content: message.content,
104
+ source_pointer: `trace=${trace.id} message=${message.id}`,
105
+ source_trace_id: trace.id,
106
+ recoverability: true,
107
+ trace_fingerprint,
108
+ labels,
109
+ created_at: opts.created_at
110
+ }));
111
+ }
112
+ return dedupeContextItems(items);
113
+ }
114
+ /**
115
+ * Dedup by `content_sha256`, keeping the EARLIEST item deterministically (ties broken
116
+ * by `created_at`, then by the existing order). Reuses the fingerprint discipline:
117
+ * byte-identical content is one item, not many. Output order follows the
118
+ * first-occurrence order of each distinct hash in the input array.
119
+ */
120
+ export function dedupeContextItems(items) {
121
+ const byHash = new Map();
122
+ for (const item of items) {
123
+ const existing = byHash.get(item.content_sha256);
124
+ if (!existing || item.created_at < existing.created_at) {
125
+ // Merge labels so a later duplicate's labels are not lost.
126
+ if (existing) {
127
+ const labels = Array.from(new Set([...item.labels, ...existing.labels])).sort();
128
+ byHash.set(item.content_sha256, { ...item, labels });
129
+ }
130
+ else {
131
+ byHash.set(item.content_sha256, item);
132
+ }
133
+ }
134
+ else {
135
+ const labels = Array.from(new Set([...existing.labels, ...item.labels])).sort();
136
+ byHash.set(item.content_sha256, { ...existing, labels });
137
+ }
138
+ }
139
+ return Array.from(byHash.values());
140
+ }
141
+ const WORD_RE = /[a-z0-9]+/g;
142
+ function tokenSet(text) {
143
+ const set = new Set();
144
+ const matches = text.toLowerCase().match(WORD_RE);
145
+ if (matches) {
146
+ for (const word of matches)
147
+ set.add(word);
148
+ }
149
+ return set;
150
+ }
151
+ /**
152
+ * Deterministic, local retrieval. Ranks items by:
153
+ * score = sourceMatchWeight * (exact source/trace match ? 1 : 0)
154
+ * + lexicalWeight * lexical overlap coefficient in [0,1]
155
+ *
156
+ * Ties are broken deterministically by recency (newer first) then `content_sha256`
157
+ * ascending, so the same store + query always yields the same order. No model, no
158
+ * embeddings, no network — overlap is a pure word-set intersection.
159
+ */
160
+ export function retrieveContextItems(items, query, options = {}) {
161
+ const sourceWeight = options.sourceMatchWeight ?? 1;
162
+ const lexicalWeight = options.lexicalWeight ?? 1;
163
+ const queryWords = tokenSet(query.text);
164
+ const wantedPointers = new Set(query.source_pointers ?? []);
165
+ const wantedTraces = new Set(query.trace_ids ?? []);
166
+ const ranked = items.map((item) => {
167
+ const sourceMatch = (item.source_pointer !== undefined && wantedPointers.has(item.source_pointer)) ||
168
+ (item.source_trace_id !== undefined && wantedTraces.has(item.source_trace_id));
169
+ let overlap = 0;
170
+ if (queryWords.size > 0) {
171
+ const itemWords = tokenSet(item.content);
172
+ if (itemWords.size > 0) {
173
+ let intersection = 0;
174
+ const [small, large] = queryWords.size <= itemWords.size ? [queryWords, itemWords] : [itemWords, queryWords];
175
+ for (const word of small) {
176
+ if (large.has(word))
177
+ intersection += 1;
178
+ }
179
+ // Overlap coefficient: intersection / min(|q|, |item|) — in [0, 1].
180
+ overlap = intersection / Math.min(queryWords.size, itemWords.size);
181
+ }
182
+ }
183
+ const score = sourceWeight * (sourceMatch ? 1 : 0) + lexicalWeight * overlap;
184
+ return { item, score, source_match: sourceMatch, lexical_overlap: overlap };
185
+ });
186
+ ranked.sort((a, b) => {
187
+ if (b.score !== a.score)
188
+ return b.score - a.score;
189
+ if (a.item.created_at !== b.item.created_at)
190
+ return a.item.created_at < b.item.created_at ? 1 : -1;
191
+ return a.item.content_sha256 < b.item.content_sha256 ? -1 : 1;
192
+ });
193
+ const withSignal = ranked.filter((entry) => entry.score > 0);
194
+ return options.limit !== undefined ? withSignal.slice(0, options.limit) : withSignal;
195
+ }
196
+ /**
197
+ * Assemble ranked items into the sufficient active context under a token budget.
198
+ * Greedy in ranked order; dedups by `content_sha256`; PRESERVES the source pointer +
199
+ * provenance on every included item (so the assembled context stays recoverable).
200
+ * Token figures are local estimates.
201
+ */
202
+ export function assembleContext(ranked, options) {
203
+ const items = ranked.map((entry) => ("item" in entry ? entry.item : entry));
204
+ const included = [];
205
+ const dropped = [];
206
+ const seen = new Set();
207
+ let total = 0;
208
+ for (const item of items) {
209
+ if (seen.has(item.content_sha256))
210
+ continue;
211
+ seen.add(item.content_sha256);
212
+ if (total + item.estimated_tokens <= options.tokenBudget) {
213
+ included.push(item);
214
+ total += item.estimated_tokens;
215
+ }
216
+ else {
217
+ dropped.push(item);
218
+ }
219
+ }
220
+ return {
221
+ text: included.map((item) => item.content).join("\n"),
222
+ included,
223
+ dropped,
224
+ estimated_tokens: total,
225
+ within_budget: dropped.length === 0
226
+ };
227
+ }
228
+ //# sourceMappingURL=context-store.js.map
@@ -1,5 +1,5 @@
1
1
  import type { SkillInjectionAdvisory } from "./skill-injection-detector.js";
2
- import type { AgentTrace, CompactionPolicy, CompactionReport, CostEstimate, TokenEstimate, WasteFinding } from "./types.js";
2
+ import type { AgentTrace, CompactionPolicy, CompactionReport, CostEstimate, SavingsEvidence, TokenEstimate, WasteFinding } from "./types.js";
3
3
  import type { UsageMetadata } from "./usage-metadata.js";
4
4
  /**
5
5
  * Attach the content-addressed per-run trace fingerprint to a compaction report (PURE,
@@ -14,6 +14,24 @@ import type { UsageMetadata } from "./usage-metadata.js";
14
14
  * attribution/integrity signal, NOT a billing, provider, or semantic-preservation claim.
15
15
  */
16
16
  export declare function withTraceFingerprint(report: CompactionReport, trace: AgentTrace): CompactionReport;
17
+ /**
18
+ * Attach the composed per-run SAVINGS EVIDENCE record to a compaction report (PURE,
19
+ * additive, backward-compatible). V0.2 "evidence labels per result": it composes the
20
+ * report's EXISTING numbers into ONE explicitly-labeled record and asserts the weakest
21
+ * honest rung — a single run is rung 1 (local estimate). It makes NO new/stronger claim:
22
+ * `billing_confirmed` is always false, `semantic_preservation` always `not_evaluated`, and
23
+ * the measured rung 1.5 (`measured_caveated_estimate_delta`) is NEVER produced here (only by
24
+ * the aggregate increment across N≥3 distinct runs).
25
+ *
26
+ * `costSource` defaults to the weakest honest label (`local_estimate`); pass a stronger
27
+ * source ONLY when the run genuinely carries it. `recoverability` defaults to
28
+ * `not_evaluated` (the eval is the separate `compact --eval` path); pass the eval result
29
+ * when one ran. Returns a new object; the input report is not mutated.
30
+ */
31
+ export declare function withSavingsEvidence(report: CompactionReport, opts?: {
32
+ costSource?: SavingsEvidence["cost_source"];
33
+ recoverability?: SavingsEvidence["recoverability"];
34
+ }): CompactionReport;
17
35
  export declare function formatAnalyzeReport(trace: AgentTrace, tokens: TokenEstimate, cost: CostEstimate, findings?: WasteFinding[], usage?: UsageMetadata, skillInjectionAdvisory?: SkillInjectionAdvisory): string;
18
36
  export declare function formatCompactionReport(report: CompactionReport): string;
19
37
  export declare function formatCompactionMarkdownReport(report: CompactionReport, policy: CompactionPolicy): string;
@@ -16,6 +16,56 @@ import { computeTraceFingerprint } from "./trace-fingerprint.js";
16
16
  export function withTraceFingerprint(report, trace) {
17
17
  return { ...report, trace_fingerprint: computeTraceFingerprint(trace) };
18
18
  }
19
+ /**
20
+ * Attach the composed per-run SAVINGS EVIDENCE record to a compaction report (PURE,
21
+ * additive, backward-compatible). V0.2 "evidence labels per result": it composes the
22
+ * report's EXISTING numbers into ONE explicitly-labeled record and asserts the weakest
23
+ * honest rung — a single run is rung 1 (local estimate). It makes NO new/stronger claim:
24
+ * `billing_confirmed` is always false, `semantic_preservation` always `not_evaluated`, and
25
+ * the measured rung 1.5 (`measured_caveated_estimate_delta`) is NEVER produced here (only by
26
+ * the aggregate increment across N≥3 distinct runs).
27
+ *
28
+ * `costSource` defaults to the weakest honest label (`local_estimate`); pass a stronger
29
+ * source ONLY when the run genuinely carries it. `recoverability` defaults to
30
+ * `not_evaluated` (the eval is the separate `compact --eval` path); pass the eval result
31
+ * when one ran. Returns a new object; the input report is not mutated.
32
+ */
33
+ export function withSavingsEvidence(report, opts = {}) {
34
+ const cost_source = opts.costSource ?? "local_estimate";
35
+ const recoverability = opts.recoverability ?? "not_evaluated";
36
+ const savings_evidence = {
37
+ trace_identity: report.trace_fingerprint ? "fingerprinted" : "unidentified",
38
+ cost_source,
39
+ recoverability,
40
+ semantic_preservation: "not_evaluated",
41
+ evidence_rung: "local_estimate_single_run",
42
+ billing_confirmed: false,
43
+ label: `Per-run ${cost_source.replace(/_/g, " ")} delta (single run → local-estimate rung). ` +
44
+ `Recoverability: ${recoverability}` +
45
+ (recoverability === "not_evaluated" ? " (run `compact --eval`)" : "") +
46
+ ". Semantic preservation: not_evaluated. NOT billing-confirmed, NOT realized savings, NOT extrapolated."
47
+ };
48
+ return { ...report, savings_evidence };
49
+ }
50
+ /** Render the composed per-run savings-evidence record for the markdown report. */
51
+ function formatSavingsEvidenceLines(report) {
52
+ const se = report.savings_evidence;
53
+ if (!se) {
54
+ return [];
55
+ }
56
+ return [
57
+ "",
58
+ "## Savings Evidence (per-run, labeled)",
59
+ "",
60
+ `- Trace identity: ${se.trace_identity}`,
61
+ `- Cost source: ${se.cost_source}`,
62
+ `- Recoverability: ${se.recoverability}`,
63
+ `- Semantic preservation: ${se.semantic_preservation}`,
64
+ `- Evidence rung: ${se.evidence_rung}`,
65
+ `- Billing-confirmed: ${se.billing_confirmed}`,
66
+ `- ${se.label}`
67
+ ];
68
+ }
19
69
  function formatTokenBreakdown(tokens) {
20
70
  return `${tokens.totalTokens} total (${tokens.inputTokens} input, ${tokens.outputTokens} output)`;
21
71
  }
@@ -217,6 +267,7 @@ export function formatCompactionMarkdownReport(report, policy) {
217
267
  `- Saving per run: $${report.saving_per_run.toFixed(6)} (estimated)`,
218
268
  ...formatValueProofLines(report),
219
269
  ...formatSpendBySourceLines(report),
270
+ ...formatSavingsEvidenceLines(report),
220
271
  "",
221
272
  "## Waste Pattern",
222
273
  "",
@@ -35,8 +35,44 @@ export interface SkippedReportFile {
35
35
  path: string;
36
36
  reason: string;
37
37
  }
38
+ /**
39
+ * V0.2 Increment 2 — the MEASURED-across-distinct-runs estimate delta. Aggregates the per-run
40
+ * estimate reductions over N DISTINCT runs (dedup by content fingerprint) into a mean + dispersion,
41
+ * and applies the accepted criterion (`docs/design/billing-delta-loop.md`): **N ≥ 3 distinct runs
42
+ * AND the mean ±2·SE interval excludes zero**. When BOTH hold, the figure earns the
43
+ * `measured_caveated_estimate_delta` rung (claims-and-evidence-ladder rung 1.5) — a measured
44
+ * estimate, NOT a single anecdote. Below the bar it stays rung 1 (`local_estimate_single_run`).
45
+ *
46
+ * It is STILL an estimate (chars/4 + price-table): explicitly NOT `provider-reported`, NOT
47
+ * eval-backed (rung 3), and NEVER `billing-confirmed` (rung 5). No time-period extrapolation.
48
+ */
49
+ export interface MeasuredEstimateDelta {
50
+ /** N distinct runs used (≥3 required to qualify). */
51
+ distinct_run_count: number;
52
+ /** Mean per-run percent reduction across the distinct runs (estimate). */
53
+ mean_percent_reduction: number;
54
+ /** Standard error of the mean percent reduction (0 when N<2 — no dispersion). */
55
+ std_error: number;
56
+ /** Lower / upper bound of the mean ±2·SE interval. */
57
+ ci_low: number;
58
+ ci_high: number;
59
+ /** Does the ±2·SE interval exclude zero (lower bound > 0)? */
60
+ excludes_zero: boolean;
61
+ /** Criterion met: N≥3 AND the ±2·SE interval excludes zero. */
62
+ qualifies: boolean;
63
+ /** The claims-ladder rung this aggregate stands on. */
64
+ evidence_rung: "measured_caveated_estimate_delta" | "local_estimate_single_run";
65
+ /** One-line honest label — estimate-class, never billing-confirmed, never eval-backed. */
66
+ label: string;
67
+ }
38
68
  export interface RunSummary extends RunSummaryBucket {
39
69
  average_percent_reduction: number;
70
+ /**
71
+ * V0.2 measured-across-distinct-runs estimate delta (the "measured" rung). Mean ±2·SE over the
72
+ * distinct runs' estimate reductions; `qualifies` only when N≥3 AND the interval excludes zero.
73
+ * Estimate-class — never billing-confirmed, never eval-backed.
74
+ */
75
+ measured_estimate_delta: MeasuredEstimateDelta;
40
76
  savings_by_policy: Record<string, RunSummaryBucket>;
41
77
  savings_by_waste_pattern: Record<string, RunSummaryBucket>;
42
78
  top_savings_runs: TopSavingsRun[];
@@ -54,6 +90,15 @@ interface LoadedReport {
54
90
  path: string;
55
91
  report: CompactionReport;
56
92
  }
93
+ /** Minimum DISTINCT runs for a measured (not single-anecdote) estimate delta. */
94
+ export declare const MIN_RUNS_FOR_MEASURED_DELTA = 3;
95
+ /**
96
+ * Compute the measured-across-distinct-runs estimate delta over a set of per-run percent
97
+ * reductions (PURE). Applies the accepted criterion: N ≥ 3 AND the mean ±2·SE interval excludes
98
+ * zero. Estimate-class only — the result is NEVER billing-confirmed or eval-backed. Uses the
99
+ * sample standard deviation (n−1); with n<2 there is no dispersion (SE 0) so it cannot qualify.
100
+ */
101
+ export declare function computeMeasuredEstimateDelta(percentReductions: number[]): MeasuredEstimateDelta;
57
102
  export declare function validateCompactionReport(value: unknown): {
58
103
  report?: CompactionReport;
59
104
  reason?: string;