@compaction/cli 0.1.2 → 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 -0
- 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/init.js +184 -157
- package/dist/cli/commands/run.js +13 -1
- package/dist/cli/index.js +4 -1
- package/dist/cli/onboarding/InitTui.d.ts +52 -0
- package/dist/cli/onboarding/InitTui.js +153 -0
- package/dist/cli/onboarding/model.d.ts +39 -0
- package/dist/cli/onboarding/model.js +77 -0
- package/dist/cli/onboarding/should-use-tui.d.ts +25 -0
- package/dist/cli/onboarding/should-use-tui.js +35 -0
- package/dist/cli/onboarding/wordmark.d.ts +28 -0
- package/dist/cli/onboarding/wordmark.js +72 -0
- package/dist/core/aggregate-format.d.ts +3 -1
- package/dist/core/aggregate-format.js +18 -0
- package/dist/core/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 +32 -1
- package/dist/core/report-generator.js +67 -0
- package/dist/core/run-aggregator.d.ts +64 -0
- package/dist/core/run-aggregator.js +132 -3
- package/dist/core/session-aggregate.d.ts +43 -0
- package/dist/core/session-aggregate.js +89 -7
- package/dist/core/trace-adapters.d.ts +8 -0
- package/dist/core/trace-adapters.js +4 -0
- package/dist/core/types.d.ts +55 -0
- package/package.json +5 -1
|
@@ -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 {};
|
|
@@ -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;
|