@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.
- package/README.md +11 -1
- package/dist/cli/commands/compact.js +15 -6
- package/dist/cli/commands/context.d.ts +2 -0
- package/dist/cli/commands/context.js +130 -0
- package/dist/cli/commands/import.js +3 -1
- package/dist/cli/commands/run.js +13 -6
- package/dist/cli/index.js +4 -1
- package/dist/core/context-store-eval-cases.d.ts +6 -0
- package/dist/core/context-store-eval-cases.js +331 -0
- package/dist/core/context-store-eval.d.ts +140 -0
- package/dist/core/context-store-eval.js +138 -0
- package/dist/core/context-store-fs.d.ts +73 -0
- package/dist/core/context-store-fs.js +157 -0
- package/dist/core/context-store-sufficiency.d.ts +98 -0
- package/dist/core/context-store-sufficiency.js +135 -0
- package/dist/core/context-store.d.ts +155 -0
- package/dist/core/context-store.js +228 -0
- package/dist/core/report-generator.d.ts +19 -1
- package/dist/core/report-generator.js +51 -0
- package/dist/core/run-aggregator.d.ts +45 -0
- package/dist/core/run-aggregator.js +64 -0
- package/dist/core/trace-adapters.d.ts +8 -0
- package/dist/core/trace-adapters.js +4 -0
- package/dist/core/types.d.ts +33 -0
- package/package.json +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { type ContextStoreItem, type RetrievalOptions, type RetrievalQuery } from "./context-store.js";
|
|
2
|
+
/**
|
|
3
|
+
* V0.4 context-store eval harness — rung-1 Increment 2 (local, deterministic, fixture-based).
|
|
4
|
+
*
|
|
5
|
+
* PURPOSE: MEASURE whether deterministic retrieval + assembly surfaces the *sufficient*
|
|
6
|
+
* active context on fixtures, along four axes from the accepted design doc
|
|
7
|
+
* (`docs/design/v0.4-context-store-retrieval.md`):
|
|
8
|
+
* - recall — did the task-critical source-pointed items survive into the
|
|
9
|
+
* assembled context?
|
|
10
|
+
* - source coverage — are the required sources represented in the assembled context?
|
|
11
|
+
* - latency — observed retrieval + assembly wall time (a measurement, not a
|
|
12
|
+
* gate; it varies by machine).
|
|
13
|
+
* - continuation success — can a fresh agent continue from the assembled context?
|
|
14
|
+
* **FIXTURE-BASED** (required markers present), NOT live model
|
|
15
|
+
* replay (true model replay is a future, gated axis — mirrors the
|
|
16
|
+
* engine harness's `task_check` honesty note).
|
|
17
|
+
* Plus a local recoverability signal (fraction of assembled items carrying a recoverable
|
|
18
|
+
* source pointer — the substrate invariant).
|
|
19
|
+
*
|
|
20
|
+
* WHAT THIS IS NOT (load-bearing):
|
|
21
|
+
* - This harness MAKES NO CLAIM. It reports per-fixture + aggregate metrics; it does NOT
|
|
22
|
+
* decide "sufficient" and bakes in NO pass/fail thresholds. The thresholds that would gate
|
|
23
|
+
* a rung-6 large-memory claim are an explicit OPEN founder decision (design doc open
|
|
24
|
+
* question 4) — deliberately not encoded here.
|
|
25
|
+
* - NO model, NO embeddings, NO network. Continuation success is a deterministic substring
|
|
26
|
+
* check against the assembled text, NOT model re-execution.
|
|
27
|
+
* - NO 300M / large-memory / retrieval-correctness claim is produced by running this.
|
|
28
|
+
*/
|
|
29
|
+
export declare const CONTEXT_STORE_EVAL_VERSION = "context-store-eval-v1";
|
|
30
|
+
/** A monotonic clock returning milliseconds. Injected so latency is testable/deterministic. */
|
|
31
|
+
export type Clock = () => number;
|
|
32
|
+
/** The four quality axes a fixture is scored on (latency is a separate measurement). */
|
|
33
|
+
export interface AxisScores {
|
|
34
|
+
recall: number;
|
|
35
|
+
source_coverage: number;
|
|
36
|
+
continuation_success: number;
|
|
37
|
+
recoverability: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Threshold role of a fixture:
|
|
41
|
+
* - `sufficiency` — the task-critical context SHOULD be surfaced; the fixture is GATED against
|
|
42
|
+
* the sufficiency thresholds (see context-store-sufficiency.ts).
|
|
43
|
+
* - `adversarial` — a low/partial score is the CORRECT behavior; the fixture is NOT gated, it is
|
|
44
|
+
* a regression ANCHOR (its measured scores must keep matching `expected`).
|
|
45
|
+
* Unlabeled fixtures default to `sufficiency`.
|
|
46
|
+
*/
|
|
47
|
+
export type ContextStoreFixtureRole = "sufficiency" | "adversarial";
|
|
48
|
+
export interface ContextStoreEvalFixture {
|
|
49
|
+
/** Stable fixture name. */
|
|
50
|
+
name: string;
|
|
51
|
+
/** Optional human description of what the fixture exercises. */
|
|
52
|
+
description?: string;
|
|
53
|
+
/** Threshold role (default `sufficiency`). Metadata only — `evaluate` ignores it. */
|
|
54
|
+
role?: ContextStoreFixtureRole;
|
|
55
|
+
/** For `adversarial` fixtures: the expected (often low/partial) per-axis anchor outcome. */
|
|
56
|
+
expected?: Partial<AxisScores>;
|
|
57
|
+
/** The store under evaluation (the accumulated items). */
|
|
58
|
+
store: ContextStoreItem[];
|
|
59
|
+
/** The retrieval query (current step's context + optional source hints). */
|
|
60
|
+
query: RetrievalQuery;
|
|
61
|
+
/** Token budget for assembly (local estimate). */
|
|
62
|
+
tokenBudget: number;
|
|
63
|
+
/** Optional retrieval options (weights, limit). */
|
|
64
|
+
retrievalOptions?: RetrievalOptions;
|
|
65
|
+
/**
|
|
66
|
+
* Task-critical item CONTENT that MUST survive into the assembled context. Compared by
|
|
67
|
+
* normalized content hash (so authors write the text, not the hash).
|
|
68
|
+
*/
|
|
69
|
+
goldItems: string[];
|
|
70
|
+
/** Source pointers that MUST be represented among the assembled items (optional). */
|
|
71
|
+
requiredSourcePointers?: string[];
|
|
72
|
+
/**
|
|
73
|
+
* FIXTURE-BASED continuation requirements: markers/substrings that MUST appear in the
|
|
74
|
+
* assembled context text for a fresh agent to continue. NOT live model replay.
|
|
75
|
+
*/
|
|
76
|
+
continuationRequirements: string[];
|
|
77
|
+
}
|
|
78
|
+
export interface ContextStoreEvalResult {
|
|
79
|
+
version: string;
|
|
80
|
+
name: string;
|
|
81
|
+
/** [0,1] — fraction of gold (task-critical) items present in the assembled context. */
|
|
82
|
+
recall: number;
|
|
83
|
+
/** [0,1] — fraction of required sources represented (1 when none are required). */
|
|
84
|
+
source_coverage: number;
|
|
85
|
+
/** [0,1] — fraction of FIXTURE-BASED continuation markers present in the assembled text. */
|
|
86
|
+
continuation_success: number;
|
|
87
|
+
/**
|
|
88
|
+
* [0,1] — fraction of assembled items that are recoverable, i.e. carry BOTH a recoverable
|
|
89
|
+
* flag AND a source pointer to recover to (1 if nothing was assembled — vacuously true).
|
|
90
|
+
*/
|
|
91
|
+
recoverability: number;
|
|
92
|
+
/** Observed retrieval + assembly time in ms (measurement, machine-dependent). */
|
|
93
|
+
latency_ms: number;
|
|
94
|
+
/** Local token estimate of the assembled context. */
|
|
95
|
+
assembled_token_estimate: number;
|
|
96
|
+
/** Whether all retrieved items fit under the budget. */
|
|
97
|
+
within_budget: boolean;
|
|
98
|
+
/** Raw counts behind the ratios (for transparency). */
|
|
99
|
+
detail: {
|
|
100
|
+
gold_total: number;
|
|
101
|
+
gold_present: number;
|
|
102
|
+
required_sources_total: number;
|
|
103
|
+
required_sources_covered: number;
|
|
104
|
+
continuation_total: number;
|
|
105
|
+
continuation_met: number;
|
|
106
|
+
assembled_item_count: number;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Evaluate a single fixture. Deterministic apart from `latency_ms` (wall time). Pure with
|
|
111
|
+
* respect to the inputs — no model, no network.
|
|
112
|
+
*/
|
|
113
|
+
export declare function evaluateContextStoreFixture(fixture: ContextStoreEvalFixture, options?: {
|
|
114
|
+
clock?: Clock;
|
|
115
|
+
}): ContextStoreEvalResult;
|
|
116
|
+
export interface ContextStoreEvalAggregate {
|
|
117
|
+
version: string;
|
|
118
|
+
fixture_count: number;
|
|
119
|
+
/** Mean across fixtures (each in [0,1]). */
|
|
120
|
+
mean_recall: number;
|
|
121
|
+
mean_source_coverage: number;
|
|
122
|
+
mean_continuation_success: number;
|
|
123
|
+
mean_recoverability: number;
|
|
124
|
+
/** Mean observed latency (ms) — measurement, not a gate. */
|
|
125
|
+
mean_latency_ms: number;
|
|
126
|
+
results: ContextStoreEvalResult[];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Evaluate a suite of fixtures and roll up the per-axis means. REPORTS metrics only — no
|
|
130
|
+
* pass/fail verdict, no thresholds (those are a gated founder decision, by design).
|
|
131
|
+
*/
|
|
132
|
+
export declare function evaluateContextStoreFixtures(fixtures: ContextStoreEvalFixture[], options?: {
|
|
133
|
+
clock?: Clock;
|
|
134
|
+
}): ContextStoreEvalAggregate;
|
|
135
|
+
/**
|
|
136
|
+
* Markdown report for a fixture suite. States the four axes, the honest non-claim, and the
|
|
137
|
+
* fixture-based-continuation caveat explicitly — so a reader cannot mistake these metrics for
|
|
138
|
+
* a large-memory / retrieval-correctness claim.
|
|
139
|
+
*/
|
|
140
|
+
export declare function formatContextStoreEvalMarkdown(aggregate: ContextStoreEvalAggregate): string;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { contentHash } from "./content-hash.js";
|
|
2
|
+
import { assembleContext, retrieveContextItems } from "./context-store.js";
|
|
3
|
+
/**
|
|
4
|
+
* V0.4 context-store eval harness — rung-1 Increment 2 (local, deterministic, fixture-based).
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE: MEASURE whether deterministic retrieval + assembly surfaces the *sufficient*
|
|
7
|
+
* active context on fixtures, along four axes from the accepted design doc
|
|
8
|
+
* (`docs/design/v0.4-context-store-retrieval.md`):
|
|
9
|
+
* - recall — did the task-critical source-pointed items survive into the
|
|
10
|
+
* assembled context?
|
|
11
|
+
* - source coverage — are the required sources represented in the assembled context?
|
|
12
|
+
* - latency — observed retrieval + assembly wall time (a measurement, not a
|
|
13
|
+
* gate; it varies by machine).
|
|
14
|
+
* - continuation success — can a fresh agent continue from the assembled context?
|
|
15
|
+
* **FIXTURE-BASED** (required markers present), NOT live model
|
|
16
|
+
* replay (true model replay is a future, gated axis — mirrors the
|
|
17
|
+
* engine harness's `task_check` honesty note).
|
|
18
|
+
* Plus a local recoverability signal (fraction of assembled items carrying a recoverable
|
|
19
|
+
* source pointer — the substrate invariant).
|
|
20
|
+
*
|
|
21
|
+
* WHAT THIS IS NOT (load-bearing):
|
|
22
|
+
* - This harness MAKES NO CLAIM. It reports per-fixture + aggregate metrics; it does NOT
|
|
23
|
+
* decide "sufficient" and bakes in NO pass/fail thresholds. The thresholds that would gate
|
|
24
|
+
* a rung-6 large-memory claim are an explicit OPEN founder decision (design doc open
|
|
25
|
+
* question 4) — deliberately not encoded here.
|
|
26
|
+
* - NO model, NO embeddings, NO network. Continuation success is a deterministic substring
|
|
27
|
+
* check against the assembled text, NOT model re-execution.
|
|
28
|
+
* - NO 300M / large-memory / retrieval-correctness claim is produced by running this.
|
|
29
|
+
*/
|
|
30
|
+
export const CONTEXT_STORE_EVAL_VERSION = "context-store-eval-v1";
|
|
31
|
+
function defaultClock() {
|
|
32
|
+
return globalThis.performance.now();
|
|
33
|
+
}
|
|
34
|
+
function normalizeContent(content) {
|
|
35
|
+
return content.trim().replace(/\s+/g, " ");
|
|
36
|
+
}
|
|
37
|
+
function ratio(numerator, denominator, whenEmpty) {
|
|
38
|
+
return denominator === 0 ? whenEmpty : numerator / denominator;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Evaluate a single fixture. Deterministic apart from `latency_ms` (wall time). Pure with
|
|
42
|
+
* respect to the inputs — no model, no network.
|
|
43
|
+
*/
|
|
44
|
+
export function evaluateContextStoreFixture(fixture, options = {}) {
|
|
45
|
+
const clock = options.clock ?? defaultClock;
|
|
46
|
+
const start = clock();
|
|
47
|
+
const ranked = retrieveContextItems(fixture.store, fixture.query, fixture.retrievalOptions);
|
|
48
|
+
const assembled = assembleContext(ranked, { tokenBudget: fixture.tokenBudget });
|
|
49
|
+
const latency_ms = clock() - start;
|
|
50
|
+
const includedHashes = new Set(assembled.included.map((item) => item.content_sha256));
|
|
51
|
+
const includedPointers = new Set(assembled.included.map((item) => item.source_pointer).filter((p) => p !== undefined));
|
|
52
|
+
const goldHashes = fixture.goldItems.map((content) => contentHash(normalizeContent(content)));
|
|
53
|
+
const goldPresent = goldHashes.filter((hash) => includedHashes.has(hash)).length;
|
|
54
|
+
const requiredSources = fixture.requiredSourcePointers ?? [];
|
|
55
|
+
const sourcesCovered = requiredSources.filter((pointer) => includedPointers.has(pointer)).length;
|
|
56
|
+
const assembledLower = assembled.text.toLowerCase();
|
|
57
|
+
const continuationMet = fixture.continuationRequirements.filter((marker) => assembledLower.includes(marker.toLowerCase())).length;
|
|
58
|
+
// Recoverable requires BOTH a recoverable flag AND a source pointer to recover *to* — an
|
|
59
|
+
// item flagged recoverable but carrying no source pointer cannot actually be recovered.
|
|
60
|
+
const recoverableCount = assembled.included.filter((item) => item.recoverability === true && item.source_pointer !== undefined).length;
|
|
61
|
+
return {
|
|
62
|
+
version: CONTEXT_STORE_EVAL_VERSION,
|
|
63
|
+
name: fixture.name,
|
|
64
|
+
recall: ratio(goldPresent, goldHashes.length, 1),
|
|
65
|
+
source_coverage: ratio(sourcesCovered, requiredSources.length, 1),
|
|
66
|
+
continuation_success: ratio(continuationMet, fixture.continuationRequirements.length, 1),
|
|
67
|
+
recoverability: ratio(recoverableCount, assembled.included.length, 1),
|
|
68
|
+
latency_ms,
|
|
69
|
+
assembled_token_estimate: assembled.estimated_tokens,
|
|
70
|
+
within_budget: assembled.within_budget,
|
|
71
|
+
detail: {
|
|
72
|
+
gold_total: goldHashes.length,
|
|
73
|
+
gold_present: goldPresent,
|
|
74
|
+
required_sources_total: requiredSources.length,
|
|
75
|
+
required_sources_covered: sourcesCovered,
|
|
76
|
+
continuation_total: fixture.continuationRequirements.length,
|
|
77
|
+
continuation_met: continuationMet,
|
|
78
|
+
assembled_item_count: assembled.included.length
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function mean(values) {
|
|
83
|
+
return values.length === 0 ? 0 : values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Evaluate a suite of fixtures and roll up the per-axis means. REPORTS metrics only — no
|
|
87
|
+
* pass/fail verdict, no thresholds (those are a gated founder decision, by design).
|
|
88
|
+
*/
|
|
89
|
+
export function evaluateContextStoreFixtures(fixtures, options = {}) {
|
|
90
|
+
const results = fixtures.map((fixture) => evaluateContextStoreFixture(fixture, options));
|
|
91
|
+
return {
|
|
92
|
+
version: CONTEXT_STORE_EVAL_VERSION,
|
|
93
|
+
fixture_count: results.length,
|
|
94
|
+
mean_recall: mean(results.map((r) => r.recall)),
|
|
95
|
+
mean_source_coverage: mean(results.map((r) => r.source_coverage)),
|
|
96
|
+
mean_continuation_success: mean(results.map((r) => r.continuation_success)),
|
|
97
|
+
mean_recoverability: mean(results.map((r) => r.recoverability)),
|
|
98
|
+
mean_latency_ms: mean(results.map((r) => r.latency_ms)),
|
|
99
|
+
results
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function pct(value) {
|
|
103
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Markdown report for a fixture suite. States the four axes, the honest non-claim, and the
|
|
107
|
+
* fixture-based-continuation caveat explicitly — so a reader cannot mistake these metrics for
|
|
108
|
+
* a large-memory / retrieval-correctness claim.
|
|
109
|
+
*/
|
|
110
|
+
export function formatContextStoreEvalMarkdown(aggregate) {
|
|
111
|
+
const lines = [
|
|
112
|
+
"## Context-store eval (rung-1, local-deterministic, fixture-based)",
|
|
113
|
+
"",
|
|
114
|
+
`Fixtures: ${aggregate.fixture_count}`,
|
|
115
|
+
"",
|
|
116
|
+
"| Axis | Mean |",
|
|
117
|
+
"|---|---|",
|
|
118
|
+
`| Recall | ${pct(aggregate.mean_recall)} |`,
|
|
119
|
+
`| Source coverage | ${pct(aggregate.mean_source_coverage)} |`,
|
|
120
|
+
`| Continuation success (fixture-based) | ${pct(aggregate.mean_continuation_success)} |`,
|
|
121
|
+
`| Recoverability | ${pct(aggregate.mean_recoverability)} |`,
|
|
122
|
+
`| Latency (mean, ms) | ${aggregate.mean_latency_ms.toFixed(2)} |`,
|
|
123
|
+
"",
|
|
124
|
+
"### Per fixture",
|
|
125
|
+
"| Fixture | Recall | Source cov. | Continuation | Recoverability | Latency (ms) | Within budget |",
|
|
126
|
+
"|---|---|---|---|---|---|---|",
|
|
127
|
+
...aggregate.results.map((r) => `| ${r.name} | ${pct(r.recall)} | ${pct(r.source_coverage)} | ${pct(r.continuation_success)} | ` +
|
|
128
|
+
`${pct(r.recoverability)} | ${r.latency_ms.toFixed(2)} | ${r.within_budget ? "yes" : "no"} |`),
|
|
129
|
+
"",
|
|
130
|
+
"**What these numbers are NOT:** this harness reports fixture metrics only. It bakes in NO",
|
|
131
|
+
"pass/fail thresholds and makes NO large-memory / 300M / retrieval-correctness claim — the",
|
|
132
|
+
"thresholds that would gate such a claim are an open, gated decision. Continuation success is",
|
|
133
|
+
"FIXTURE-BASED (required markers present in the assembled text), NOT live model re-execution.",
|
|
134
|
+
"Latency is an observed measurement and varies by machine."
|
|
135
|
+
];
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=context-store-eval.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type ContextStoreItem } from "./context-store.js";
|
|
2
|
+
/**
|
|
3
|
+
* Local-file persistence for the V0.4 context store (rung-1), with a simple local SIZE CAP.
|
|
4
|
+
*
|
|
5
|
+
* LOCAL-ONLY: the store is an append-only JSONL file under a `.compaction/`-rooted directory
|
|
6
|
+
* (gitignored). There is NO network, credential, upload, or DB here — plain local file I/O, by
|
|
7
|
+
* design. Store content is LOCAL-ONLY and must be EXCLUDED-BY-DEFAULT from any future share /
|
|
8
|
+
* `feedback` bundle (reuse the existing redaction discipline) — never uploaded.
|
|
9
|
+
*
|
|
10
|
+
* SIZE CAP (accepted decision, docs/design/v0.4-context-store-decisions.md): a simple,
|
|
11
|
+
* deterministic local cap bounds unbounded growth. See `applyContextStoreCap`:
|
|
12
|
+
* - Capped on BOTH item count AND total serialized bytes.
|
|
13
|
+
* - Eviction is DETERMINISTIC and oldest-first (by `created_at`, ties by `content_sha256`).
|
|
14
|
+
* - Eviction removes WHOLE items only; it NEVER mutates a retained item, so every retained
|
|
15
|
+
* item keeps its `source_pointer` + recoverability intact (the provenance invariant holds).
|
|
16
|
+
* - The most recent item is always retained (a single oversized newest item is never silently
|
|
17
|
+
* dropped to an empty store).
|
|
18
|
+
* Normal writes stay append-only; the file is rewritten (atomically) only when the cap evicts.
|
|
19
|
+
*/
|
|
20
|
+
/** Default store filename inside the store directory. */
|
|
21
|
+
export declare const CONTEXT_STORE_FILE = "items.jsonl";
|
|
22
|
+
/** Resolve the JSONL path for a given store directory. */
|
|
23
|
+
export declare function contextStorePath(storeDir: string): string;
|
|
24
|
+
/** A simple local size cap. Either bound may be omitted (treated as unbounded). */
|
|
25
|
+
export interface ContextStoreCap {
|
|
26
|
+
/** Maximum number of items retained. */
|
|
27
|
+
maxItems?: number;
|
|
28
|
+
/** Maximum total serialized (JSONL) bytes retained. */
|
|
29
|
+
maxBytes?: number;
|
|
30
|
+
}
|
|
31
|
+
/** Conservative local defaults — enough headroom for real use, bounded against runaway growth. */
|
|
32
|
+
export declare const DEFAULT_CONTEXT_STORE_CAP: Required<ContextStoreCap>;
|
|
33
|
+
export interface CapResult {
|
|
34
|
+
/** Items retained, in deterministic chronological (oldest-kept → newest) order. */
|
|
35
|
+
kept: ContextStoreItem[];
|
|
36
|
+
/** Items evicted (the oldest), deterministic. */
|
|
37
|
+
evicted: ContextStoreItem[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Apply the size cap. PURE — no I/O. Keeps the NEWEST items that fit within BOTH caps and
|
|
41
|
+
* evicts the OLDEST; the most recent item is always retained. Deterministic and testable.
|
|
42
|
+
* Input is assumed deduped by `content_sha256` (as produced by `loadContextStore`).
|
|
43
|
+
*/
|
|
44
|
+
export declare function applyContextStoreCap(items: ContextStoreItem[], cap?: ContextStoreCap): CapResult;
|
|
45
|
+
/**
|
|
46
|
+
* Read every persisted item from the store directory, deduped by content hash.
|
|
47
|
+
* Returns `[]` if the store does not exist yet (starts empty — no migration).
|
|
48
|
+
* Malformed lines are skipped (a partially-written tail never throws).
|
|
49
|
+
*/
|
|
50
|
+
export declare function loadContextStore(storeDir: string): Promise<ContextStoreItem[]>;
|
|
51
|
+
/** Atomically (write-temp-then-rename) replace the store file with `items`. */
|
|
52
|
+
export declare function writeContextStore(storeDir: string, items: ContextStoreItem[]): Promise<void>;
|
|
53
|
+
export interface AppendResult {
|
|
54
|
+
/** Items newly written this call (i.e. not already present by content hash). */
|
|
55
|
+
appended: ContextStoreItem[];
|
|
56
|
+
/** Items skipped because their content hash already existed in the store. */
|
|
57
|
+
skipped: ContextStoreItem[];
|
|
58
|
+
/** Items evicted by the size cap during this call (the oldest), deterministic. */
|
|
59
|
+
evicted: ContextStoreItem[];
|
|
60
|
+
/** Total distinct items retained in the store after this append (post-cap). */
|
|
61
|
+
total: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Append items to the store, skipping any whose `content_sha256` already exists (idempotent),
|
|
65
|
+
* then enforce the size cap. New writes are append-only; the file is rewritten (atomically)
|
|
66
|
+
* only when the cap evicts something. Returns what was appended / skipped / evicted.
|
|
67
|
+
*/
|
|
68
|
+
export declare function appendContextItems(storeDir: string, items: ContextStoreItem[], cap?: ContextStoreCap): Promise<AppendResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Enforce the size cap on an existing store WITHOUT appending (e.g. after lowering the cap).
|
|
71
|
+
* Rewrites the file only when something is evicted. Returns the kept/evicted split.
|
|
72
|
+
*/
|
|
73
|
+
export declare function enforceContextStoreCap(storeDir: string, cap?: ContextStoreCap): Promise<CapResult>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { dedupeContextItems } from "./context-store.js";
|
|
5
|
+
/**
|
|
6
|
+
* Local-file persistence for the V0.4 context store (rung-1), with a simple local SIZE CAP.
|
|
7
|
+
*
|
|
8
|
+
* LOCAL-ONLY: the store is an append-only JSONL file under a `.compaction/`-rooted directory
|
|
9
|
+
* (gitignored). There is NO network, credential, upload, or DB here — plain local file I/O, by
|
|
10
|
+
* design. Store content is LOCAL-ONLY and must be EXCLUDED-BY-DEFAULT from any future share /
|
|
11
|
+
* `feedback` bundle (reuse the existing redaction discipline) — never uploaded.
|
|
12
|
+
*
|
|
13
|
+
* SIZE CAP (accepted decision, docs/design/v0.4-context-store-decisions.md): a simple,
|
|
14
|
+
* deterministic local cap bounds unbounded growth. See `applyContextStoreCap`:
|
|
15
|
+
* - Capped on BOTH item count AND total serialized bytes.
|
|
16
|
+
* - Eviction is DETERMINISTIC and oldest-first (by `created_at`, ties by `content_sha256`).
|
|
17
|
+
* - Eviction removes WHOLE items only; it NEVER mutates a retained item, so every retained
|
|
18
|
+
* item keeps its `source_pointer` + recoverability intact (the provenance invariant holds).
|
|
19
|
+
* - The most recent item is always retained (a single oversized newest item is never silently
|
|
20
|
+
* dropped to an empty store).
|
|
21
|
+
* Normal writes stay append-only; the file is rewritten (atomically) only when the cap evicts.
|
|
22
|
+
*/
|
|
23
|
+
/** Default store filename inside the store directory. */
|
|
24
|
+
export const CONTEXT_STORE_FILE = "items.jsonl";
|
|
25
|
+
/** Resolve the JSONL path for a given store directory. */
|
|
26
|
+
export function contextStorePath(storeDir) {
|
|
27
|
+
return join(storeDir, CONTEXT_STORE_FILE);
|
|
28
|
+
}
|
|
29
|
+
/** Conservative local defaults — enough headroom for real use, bounded against runaway growth. */
|
|
30
|
+
export const DEFAULT_CONTEXT_STORE_CAP = {
|
|
31
|
+
maxItems: 10_000,
|
|
32
|
+
maxBytes: 5 * 1024 * 1024 // 5 MiB
|
|
33
|
+
};
|
|
34
|
+
/** Serialized on-disk footprint of one item (its JSONL line, including the newline). */
|
|
35
|
+
function serializedBytes(item) {
|
|
36
|
+
return Buffer.byteLength(`${JSON.stringify(item)}\n`, "utf8");
|
|
37
|
+
}
|
|
38
|
+
/** Deterministic oldest-first order: `created_at` ascending, ties broken by `content_sha256`. */
|
|
39
|
+
function oldestFirst(items) {
|
|
40
|
+
return [...items].sort((a, b) => {
|
|
41
|
+
if (a.created_at !== b.created_at)
|
|
42
|
+
return a.created_at < b.created_at ? -1 : 1;
|
|
43
|
+
return a.content_sha256 < b.content_sha256 ? -1 : 1;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Apply the size cap. PURE — no I/O. Keeps the NEWEST items that fit within BOTH caps and
|
|
48
|
+
* evicts the OLDEST; the most recent item is always retained. Deterministic and testable.
|
|
49
|
+
* Input is assumed deduped by `content_sha256` (as produced by `loadContextStore`).
|
|
50
|
+
*/
|
|
51
|
+
export function applyContextStoreCap(items, cap = DEFAULT_CONTEXT_STORE_CAP) {
|
|
52
|
+
const maxItems = cap.maxItems ?? Number.POSITIVE_INFINITY;
|
|
53
|
+
const maxBytes = cap.maxBytes ?? Number.POSITIVE_INFINITY;
|
|
54
|
+
const ordered = oldestFirst(items);
|
|
55
|
+
const keptReversed = [];
|
|
56
|
+
let count = 0;
|
|
57
|
+
let bytes = 0;
|
|
58
|
+
// Walk newest → oldest, keeping while within both caps. The newest item is always kept.
|
|
59
|
+
for (let i = ordered.length - 1; i >= 0; i -= 1) {
|
|
60
|
+
const item = ordered[i];
|
|
61
|
+
const itemBytes = serializedBytes(item);
|
|
62
|
+
const isNewestKept = keptReversed.length === 0;
|
|
63
|
+
if (!isNewestKept && (count + 1 > maxItems || bytes + itemBytes > maxBytes)) {
|
|
64
|
+
break; // every still-older item is evicted (keep the newest contiguous block)
|
|
65
|
+
}
|
|
66
|
+
keptReversed.push(item);
|
|
67
|
+
count += 1;
|
|
68
|
+
bytes += itemBytes;
|
|
69
|
+
}
|
|
70
|
+
const kept = keptReversed.reverse();
|
|
71
|
+
const keptHashes = new Set(kept.map((item) => item.content_sha256));
|
|
72
|
+
const evicted = ordered.filter((item) => !keptHashes.has(item.content_sha256));
|
|
73
|
+
return { kept, evicted };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Read every persisted item from the store directory, deduped by content hash.
|
|
77
|
+
* Returns `[]` if the store does not exist yet (starts empty — no migration).
|
|
78
|
+
* Malformed lines are skipped (a partially-written tail never throws).
|
|
79
|
+
*/
|
|
80
|
+
export async function loadContextStore(storeDir) {
|
|
81
|
+
const path = contextStorePath(storeDir);
|
|
82
|
+
if (!existsSync(path)) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const raw = await readFile(path, "utf8");
|
|
86
|
+
const items = [];
|
|
87
|
+
for (const line of raw.split("\n")) {
|
|
88
|
+
const trimmed = line.trim();
|
|
89
|
+
if (trimmed.length === 0)
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(trimmed);
|
|
93
|
+
if (parsed && typeof parsed.content_sha256 === "string" && typeof parsed.content === "string") {
|
|
94
|
+
items.push(parsed);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Skip a malformed/partial line rather than failing the whole read.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return dedupeContextItems(items);
|
|
102
|
+
}
|
|
103
|
+
/** Atomically (write-temp-then-rename) replace the store file with `items`. */
|
|
104
|
+
export async function writeContextStore(storeDir, items) {
|
|
105
|
+
const path = contextStorePath(storeDir);
|
|
106
|
+
await mkdir(dirname(path), { recursive: true });
|
|
107
|
+
const payload = items.length > 0 ? `${items.map((item) => JSON.stringify(item)).join("\n")}\n` : "";
|
|
108
|
+
const tmp = `${path}.tmp`;
|
|
109
|
+
await writeFile(tmp, payload, "utf8");
|
|
110
|
+
await rename(tmp, path);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Append items to the store, skipping any whose `content_sha256` already exists (idempotent),
|
|
114
|
+
* then enforce the size cap. New writes are append-only; the file is rewritten (atomically)
|
|
115
|
+
* only when the cap evicts something. Returns what was appended / skipped / evicted.
|
|
116
|
+
*/
|
|
117
|
+
export async function appendContextItems(storeDir, items, cap = DEFAULT_CONTEXT_STORE_CAP) {
|
|
118
|
+
const existing = await loadContextStore(storeDir);
|
|
119
|
+
const seen = new Set(existing.map((item) => item.content_sha256));
|
|
120
|
+
const appended = [];
|
|
121
|
+
const skipped = [];
|
|
122
|
+
const fresh = dedupeContextItems(items);
|
|
123
|
+
for (const item of fresh) {
|
|
124
|
+
if (seen.has(item.content_sha256)) {
|
|
125
|
+
skipped.push(item);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
seen.add(item.content_sha256);
|
|
129
|
+
appended.push(item);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (appended.length > 0) {
|
|
133
|
+
const path = contextStorePath(storeDir);
|
|
134
|
+
await mkdir(dirname(path), { recursive: true });
|
|
135
|
+
const payload = `${appended.map((item) => JSON.stringify(item)).join("\n")}\n`;
|
|
136
|
+
await appendFile(path, payload, "utf8");
|
|
137
|
+
}
|
|
138
|
+
// Enforce the size cap (rewrites the file only when something is evicted).
|
|
139
|
+
const { kept, evicted } = applyContextStoreCap([...existing, ...appended], cap);
|
|
140
|
+
if (evicted.length > 0) {
|
|
141
|
+
await writeContextStore(storeDir, kept);
|
|
142
|
+
}
|
|
143
|
+
return { appended, skipped, evicted, total: kept.length };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Enforce the size cap on an existing store WITHOUT appending (e.g. after lowering the cap).
|
|
147
|
+
* Rewrites the file only when something is evicted. Returns the kept/evicted split.
|
|
148
|
+
*/
|
|
149
|
+
export async function enforceContextStoreCap(storeDir, cap = DEFAULT_CONTEXT_STORE_CAP) {
|
|
150
|
+
const existing = await loadContextStore(storeDir);
|
|
151
|
+
const result = applyContextStoreCap(existing, cap);
|
|
152
|
+
if (result.evicted.length > 0) {
|
|
153
|
+
await writeContextStore(storeDir, result.kept);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=context-store-fs.js.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type Clock, type ContextStoreEvalFixture } 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 declare const CONTEXT_STORE_SUFFICIENCY_VERSION = "context-store-sufficiency-v1";
|
|
23
|
+
/** A per-axis gate. `hard` fails the report; `soft` only warns; `report` records (latency). */
|
|
24
|
+
export type AxisGate = {
|
|
25
|
+
kind: "hard";
|
|
26
|
+
min: number;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "soft";
|
|
29
|
+
min: number;
|
|
30
|
+
} | {
|
|
31
|
+
kind: "report";
|
|
32
|
+
};
|
|
33
|
+
export interface SufficiencyThresholds {
|
|
34
|
+
recall: AxisGate;
|
|
35
|
+
source_coverage: AxisGate;
|
|
36
|
+
recoverability: AxisGate;
|
|
37
|
+
continuation_success: AxisGate;
|
|
38
|
+
latency_ms: AxisGate;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Founder-accepted defaults (2026-06-24): hard recall / source coverage / recoverability
|
|
42
|
+
* (= 1.0 on sufficiency fixtures); soft continuation (advisory ≥ 0.9 — it is a marker proxy,
|
|
43
|
+
* not model replay); latency report-only. INTERNAL thresholds, not public claims.
|
|
44
|
+
*/
|
|
45
|
+
export declare const CONTEXT_STORE_SUFFICIENCY_THRESHOLDS: SufficiencyThresholds;
|
|
46
|
+
/** The quality axes a fixture is gated/anchored on (latency is handled separately). */
|
|
47
|
+
declare const QUALITY_AXES: readonly ["recall", "source_coverage", "recoverability", "continuation_success"];
|
|
48
|
+
type QualityAxis = (typeof QUALITY_AXES)[number];
|
|
49
|
+
export interface HardFailure {
|
|
50
|
+
fixture: string;
|
|
51
|
+
axis: QualityAxis;
|
|
52
|
+
measured: number;
|
|
53
|
+
min: number;
|
|
54
|
+
}
|
|
55
|
+
export interface SoftWarning {
|
|
56
|
+
fixture: string;
|
|
57
|
+
axis: QualityAxis;
|
|
58
|
+
measured: number;
|
|
59
|
+
min: number;
|
|
60
|
+
}
|
|
61
|
+
export interface AnchorFailure {
|
|
62
|
+
fixture: string;
|
|
63
|
+
axis: QualityAxis;
|
|
64
|
+
measured: number;
|
|
65
|
+
expected: number;
|
|
66
|
+
}
|
|
67
|
+
export interface LatencyRecord {
|
|
68
|
+
fixture: string;
|
|
69
|
+
latency_ms: number;
|
|
70
|
+
}
|
|
71
|
+
export interface SufficiencyReport {
|
|
72
|
+
version: string;
|
|
73
|
+
/** True iff there are no hard-axis failures and no anchor mismatches. */
|
|
74
|
+
passed: boolean;
|
|
75
|
+
sufficiency_fixtures: number;
|
|
76
|
+
adversarial_fixtures: number;
|
|
77
|
+
/** Hard-axis regressions on sufficiency fixtures — these FAIL the report. */
|
|
78
|
+
hard_failures: HardFailure[];
|
|
79
|
+
/** Soft-axis (continuation) dips on sufficiency fixtures — advisory only, do NOT fail. */
|
|
80
|
+
warnings: SoftWarning[];
|
|
81
|
+
/** Adversarial fixtures whose measured score drifted from their expected anchor — these FAIL. */
|
|
82
|
+
anchor_failures: AnchorFailure[];
|
|
83
|
+
/** Recorded latency per fixture — REPORT-ONLY, never gates. */
|
|
84
|
+
latency: LatencyRecord[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Run every fixture and build the internal sufficiency report. Deterministic apart from the
|
|
88
|
+
* recorded latency (use an injected clock for reproducible reports).
|
|
89
|
+
*/
|
|
90
|
+
export declare function evaluateSufficiency(fixtures: ContextStoreEvalFixture[], thresholds?: SufficiencyThresholds, options?: {
|
|
91
|
+
clock?: Clock;
|
|
92
|
+
}): SufficiencyReport;
|
|
93
|
+
/**
|
|
94
|
+
* Internal markdown rendering of the sufficiency report. For local/CI inspection only —
|
|
95
|
+
* not a user-facing surface and not a claim.
|
|
96
|
+
*/
|
|
97
|
+
export declare function formatSufficiencyReportMarkdown(report: SufficiencyReport): string;
|
|
98
|
+
export {};
|