@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,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 {};