pi-continual-harness 0.10.0 → 0.11.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 +2 -1
- package/package.json +1 -1
- package/src/corpus.ts +310 -0
- package/src/harness.ts +40 -0
- package/src/index.ts +13 -0
- package/src/refine.ts +7 -1
package/README.md
CHANGED
|
@@ -63,6 +63,7 @@ Durable I/O (round-trip with pi-reflect):
|
|
|
63
63
|
```
|
|
64
64
|
/harness status # counts + durable layer presence/mtime
|
|
65
65
|
/harness export [path] # layered export (or full snapshot to a path)
|
|
66
|
+
/harness export-corpus [path] # calibration corpus JSONL for pi-reflex (local-only; dedupe pairs + lifecycle)
|
|
66
67
|
/harness import [--prune] [path] # layered import (or single file from a path)
|
|
67
68
|
/harness prune [--decay <days>] # drop items below the importance floor
|
|
68
69
|
/harness keep <id> # nudge importance up (+0.1)
|
|
@@ -315,7 +316,7 @@ deltas directly). The propose stage is pluggable via a registry
|
|
|
315
316
|
|
|
316
317
|
The dedupe planner's `similarity` seam (`DedupeOptions.similarity`, re-exported
|
|
317
318
|
from the package entry) is how a companion package upgrades the comparison —
|
|
318
|
-
e.g. embedding cosine similarity, or a local decision engine such as pi-
|
|
319
|
+
e.g. embedding cosine similarity, or a local decision engine such as pi-reflex
|
|
319
320
|
(a local-only project for now). A comparator may return
|
|
320
321
|
`{ score, abstain }` instead of a plain number: **an abstaining pair is
|
|
321
322
|
treated as not duplicates** — both items are kept — so an engine with
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-continual-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Online self-improvement layer for pi. A unified, in-trajectory harness-state store (prompt notes, memory, skill descriptions, sub-agent specs) with a manual /refine that proposes evidence-backed structured CRUD deltas. Composes with pi-reflect (offline) and pi-mem (storage); owns only the online optimizer layer. Inspired by Continual Harness (arXiv 2605.09998) and ACE (arXiv 2510.04618).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/corpus.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// Calibration corpus exporter — the S1 half of the pi-reflex consumer contract
|
|
2
|
+
// (formerly pi-jev; CONTRACT-harness.md §4 in that project; issue #12 here).
|
|
3
|
+
//
|
|
4
|
+
// Turns the session's OWN audit trail — `harness-state` snapshots and
|
|
5
|
+
// `harness-refinement` entries — into two JSONL corpora for calibrating and
|
|
6
|
+
// evaluating a semantic decision engine (pi-reflex) against real harness
|
|
7
|
+
// decisions:
|
|
8
|
+
//
|
|
9
|
+
// dedupe-pairs.jsonl one record per compared item pair
|
|
10
|
+
// - label "dup" parsed from dedupe delete reasons (merged / near-
|
|
11
|
+
// duplicate); the run's ground truth
|
|
12
|
+
// - label "not_dup" same-key-field pairs with token overlap in
|
|
13
|
+
// [NOT_DUP_FLOOR, threshold) that the planner did NOT
|
|
14
|
+
// merge; heuristic negatives, needs_review until a human
|
|
15
|
+
// confirms (the planner's threshold, not a label, decided)
|
|
16
|
+
// lifecycle.jsonl one record per item lifecycle event, classified from
|
|
17
|
+
// consecutive snapshot diffs:
|
|
18
|
+
// created (id appears) · kept (+0.1) · dropped (−0.1) · cited (small
|
|
19
|
+
// positive bump, the outcome loop's default +0.03) · pruned (removed with
|
|
20
|
+
// last importance < IMPORTANCE_FLOOR) · deleted (removed otherwise)
|
|
21
|
+
//
|
|
22
|
+
// Pure: takes an entry iterable (e.g. ctx.sessionManager.getBranch()), returns
|
|
23
|
+
// records; no I/O here. The `/harness export-corpus` subcommand is thin glue.
|
|
24
|
+
//
|
|
25
|
+
// CONTRACT INVARIANT (local-only): records describe item CONTENTS. Export
|
|
26
|
+
// writes local files and nothing else — no network, no telemetry. The corpus
|
|
27
|
+
// leaves the machine only if the user pushes it.
|
|
28
|
+
//
|
|
29
|
+
// Determinism: same entries in, same records out (stable order, hashes of
|
|
30
|
+
// content only). Overlaps are rounded to 4 decimals for stable JSON.
|
|
31
|
+
|
|
32
|
+
import { createHash } from "node:crypto";
|
|
33
|
+
import { IMPORTANCE_FLOOR, REFINE_ENTRY, STATE_ENTRY } from "./store.js";
|
|
34
|
+
import { DEDUPE_THRESHOLD, tokenOverlap } from "./proposer.js";
|
|
35
|
+
import { DEFAULT_REF_BUMP } from "./config.js";
|
|
36
|
+
import type { ComponentKind, HarnessItem, HarnessState } from "./types.js";
|
|
37
|
+
|
|
38
|
+
// ---- record types (contract §4; `v` follows the contract's major.minor) -----
|
|
39
|
+
|
|
40
|
+
export type LifecycleEvent = "created" | "cited" | "kept" | "dropped" | "pruned" | "deleted";
|
|
41
|
+
|
|
42
|
+
export interface LifecycleRecord {
|
|
43
|
+
v: 1;
|
|
44
|
+
kind: "lifecycle";
|
|
45
|
+
item_kind: ComponentKind;
|
|
46
|
+
/** sha256 of the item's content, prefixed — contents are not carried here. */
|
|
47
|
+
content_hash: string;
|
|
48
|
+
event: LifecycleEvent;
|
|
49
|
+
/** Post-event importance. */
|
|
50
|
+
importance: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface DedupePairRecord {
|
|
54
|
+
v: 1;
|
|
55
|
+
kind: "dedupe_pair";
|
|
56
|
+
/** Keeper (dup label) / first-by-store-order (not_dup) content. */
|
|
57
|
+
a: string;
|
|
58
|
+
/** Duplicate (dup label) / second-by-store-order (not_dup) content. */
|
|
59
|
+
b: string;
|
|
60
|
+
label: "dup" | "not_dup";
|
|
61
|
+
similarity: number;
|
|
62
|
+
source: "tokenOverlap";
|
|
63
|
+
/** Present on "not_dup" records: the label is inferred from non-merge, not
|
|
64
|
+
* human-confirmed. */
|
|
65
|
+
needs_review?: true;
|
|
66
|
+
meta: { kinds: ComponentKind[]; owners: string[] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CorpusResult {
|
|
70
|
+
pairs: DedupePairRecord[];
|
|
71
|
+
lifecycle: LifecycleRecord[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Options for buildCorpus. */
|
|
75
|
+
export interface CorpusOptions {
|
|
76
|
+
/** The CONFIGURED outcome-loop citation bump (harness.json
|
|
77
|
+
* `outcomeImportance.bump`; default 0.03 = DEFAULT_REF_BUMP). Classification
|
|
78
|
+
* is config-aware: keep/drop (±0.1) are checked FIRST, so a configured
|
|
79
|
+
* bump of exactly 0.1 collides and keep/drop win — documented behavior. */
|
|
80
|
+
citeBump?: number | undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- tuning knobs -----------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/** Overlap floor for "not_dup" candidates: below it a pair is uninformative. */
|
|
86
|
+
export const NOT_DUP_FLOOR = 0.3;
|
|
87
|
+
/** keep/drop bump magnitude (harness.ts handleBump). */
|
|
88
|
+
const KEEP_DROP_BUMP = 0.1;
|
|
89
|
+
/** Classification tolerance around each bump (float safety, clamped bumps). */
|
|
90
|
+
const BUMP_TOL = 0.02;
|
|
91
|
+
/** Smallest importance delta worth classifying at all. */
|
|
92
|
+
const EPSILON = 0.005;
|
|
93
|
+
|
|
94
|
+
// ---- helpers ----------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/** Loose entry shape, kept decoupled from pi's internal session types.
|
|
97
|
+
* `applied` is a legacy COUNT; the full delta list lives in `appliedDeltas`
|
|
98
|
+
* (0.11.0+ audit entries — see refine.ts). */
|
|
99
|
+
type AnyEntry = {
|
|
100
|
+
type?: string;
|
|
101
|
+
customType?: string;
|
|
102
|
+
data?: {
|
|
103
|
+
state?: HarnessState;
|
|
104
|
+
proposer?: string;
|
|
105
|
+
applied?: unknown;
|
|
106
|
+
appliedDeltas?: unknown[];
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Parse a dedupe delete reason ("merged into h_x (overlap 0.71)" or the
|
|
111
|
+
* delete-only "near-duplicate of h_x (overlap 0.71)"). */
|
|
112
|
+
export function parseDedupeDelete(
|
|
113
|
+
reason: string,
|
|
114
|
+
): { keeperId: string; overlap: number } | undefined {
|
|
115
|
+
const m = /^(?:merged into|near-duplicate of) (h_[A-Za-z0-9_]+) \(overlap (0?\.\d+|1(?:\.0+)?)\)$/.exec(
|
|
116
|
+
reason,
|
|
117
|
+
);
|
|
118
|
+
if (!m) return undefined;
|
|
119
|
+
return { keeperId: m[1]!, overlap: Number(m[2]) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function round4(x: number): number {
|
|
123
|
+
return Math.round(x * 1e4) / 1e4;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hash(content: string): string {
|
|
127
|
+
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Durable placement, mirroring planDedupe's private key: only items in the
|
|
131
|
+
* same layer were ever compared by the planner. */
|
|
132
|
+
function durableLayer(i: HarnessItem): string {
|
|
133
|
+
return i.scope === "project" ? `project:${i.project ?? ""}` : "global";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---- lifecycle ---------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
/** Classify one snapshot → next-snapshot transition into lifecycle records.
|
|
139
|
+
* `citeBump` is the CONFIGURED outcome-loop bump (see CorpusOptions). */
|
|
140
|
+
function diffSnapshots(
|
|
141
|
+
prev: HarnessState,
|
|
142
|
+
next: HarnessState,
|
|
143
|
+
citeBump: number,
|
|
144
|
+
out: LifecycleRecord[],
|
|
145
|
+
): void {
|
|
146
|
+
const prevById = new Map(prev.items.map((i) => [i.id, i]));
|
|
147
|
+
const nextById = new Map(next.items.map((i) => [i.id, i]));
|
|
148
|
+
|
|
149
|
+
for (const item of next.items) {
|
|
150
|
+
const before = prevById.get(item.id);
|
|
151
|
+
if (!before) {
|
|
152
|
+
out.push(lifecycle(item, "created", item.importance));
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const d = item.importance - before.importance;
|
|
156
|
+
if (Math.abs(d) < EPSILON) continue;
|
|
157
|
+
// keep/drop FIRST: a configured citation bump of exactly ±0.1 collides and
|
|
158
|
+
// keep/drop win (documented in CorpusOptions).
|
|
159
|
+
if (Math.abs(d - KEEP_DROP_BUMP) <= BUMP_TOL) out.push(lifecycle(item, "kept", item.importance));
|
|
160
|
+
else if (Math.abs(d + KEEP_DROP_BUMP) <= BUMP_TOL)
|
|
161
|
+
out.push(lifecycle(item, "dropped", item.importance));
|
|
162
|
+
else if (citeBump > 0 && Math.abs(d - citeBump) <= BUMP_TOL)
|
|
163
|
+
out.push(lifecycle(item, "cited", item.importance));
|
|
164
|
+
// else: explicit harness_mutate importance set — not a lifecycle signal.
|
|
165
|
+
}
|
|
166
|
+
for (const item of prev.items) {
|
|
167
|
+
if (nextById.has(item.id)) continue;
|
|
168
|
+
const event = item.importance < IMPORTANCE_FLOOR ? "pruned" : "deleted";
|
|
169
|
+
out.push(lifecycle(item, event, item.importance));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function lifecycle(item: HarnessItem, event: LifecycleEvent, importance: number): LifecycleRecord {
|
|
174
|
+
return { v: 1, kind: "lifecycle", item_kind: item.kind, content_hash: hash(item.content), event, importance };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---- dedupe pairs --------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
/** Emit pair records for one dedupe refinement run.
|
|
180
|
+
*
|
|
181
|
+
* `current` is the store state AT AUDIT TIME — a run that applied nothing
|
|
182
|
+
* writes NO harness-state snapshot, so "last snapshot" is correct whether or
|
|
183
|
+
* not this run wrote one (no-merge runs: current IS the pre-run state).
|
|
184
|
+
* `pre` (previous snapshot, only meaningful when the run applied deletes)
|
|
185
|
+
* supplies DELETED items' contents; keepers are read from `current`.
|
|
186
|
+
*
|
|
187
|
+
* `deltas` comes from the audit entry's `appliedDeltas` (0.11.0+). Legacy
|
|
188
|
+
* entries carry only a numeric count — pass [] there: dup reconstruction is
|
|
189
|
+
* impossible without delete reasons, but not_dup candidates still emit. */
|
|
190
|
+
function collectDedupePairs(
|
|
191
|
+
current: HarnessState,
|
|
192
|
+
pre: HarnessState,
|
|
193
|
+
deltas: unknown[],
|
|
194
|
+
seen: Set<string>,
|
|
195
|
+
out: DedupePairRecord[],
|
|
196
|
+
): void {
|
|
197
|
+
const currentById = new Map(current.items.map((i) => [i.id, i]));
|
|
198
|
+
const preById = new Map(pre.items.map((i) => [i.id, i]));
|
|
199
|
+
|
|
200
|
+
const deletes = (deltas as Array<{ op?: string; id?: string; reason?: string }>).filter(
|
|
201
|
+
(d) => d && d.op === "delete" && d.id && typeof d.reason === "string",
|
|
202
|
+
);
|
|
203
|
+
const deletedIds = new Set(deletes.map((d) => d.id!));
|
|
204
|
+
|
|
205
|
+
for (const d of deletes) {
|
|
206
|
+
const parsed = parseDedupeDelete(d.reason!);
|
|
207
|
+
if (!parsed) continue;
|
|
208
|
+
const keeper = currentById.get(parsed.keeperId) ?? preById.get(parsed.keeperId);
|
|
209
|
+
const dup = preById.get(d.id!);
|
|
210
|
+
if (!keeper || !dup) continue;
|
|
211
|
+
pushPair(
|
|
212
|
+
out,
|
|
213
|
+
seen,
|
|
214
|
+
keeper.content,
|
|
215
|
+
dup.content,
|
|
216
|
+
"dup",
|
|
217
|
+
round4(parsed.overlap),
|
|
218
|
+
{ kinds: [keeper.kind], owners: [keeper.ownerModel, dup.ownerModel] },
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// not_dup candidates: everything the planner COULD have compared (same key
|
|
223
|
+
// fields, active, not deleted this run) but did not merge, with an
|
|
224
|
+
// informative overlap. Always relative to the CURRENT state.
|
|
225
|
+
const candidates = current.items.filter((i) => i.active && !deletedIds.has(i.id));
|
|
226
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
227
|
+
for (let j = i + 1; j < candidates.length; j++) {
|
|
228
|
+
const a = candidates[i]!;
|
|
229
|
+
const b = candidates[j]!;
|
|
230
|
+
if (a.kind !== b.kind) continue;
|
|
231
|
+
if (a.ownerModel !== b.ownerModel) continue;
|
|
232
|
+
if (durableLayer(a) !== durableLayer(b)) continue;
|
|
233
|
+
const sim = tokenOverlap(a.content, b.content);
|
|
234
|
+
if (sim < NOT_DUP_FLOOR || sim >= DEDUPE_THRESHOLD) continue;
|
|
235
|
+
pushPair(
|
|
236
|
+
out,
|
|
237
|
+
seen,
|
|
238
|
+
a.content,
|
|
239
|
+
b.content,
|
|
240
|
+
"not_dup",
|
|
241
|
+
round4(sim),
|
|
242
|
+
{ kinds: [a.kind], owners: [a.ownerModel] },
|
|
243
|
+
true,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function pushPair(
|
|
250
|
+
out: DedupePairRecord[],
|
|
251
|
+
seen: Set<string>,
|
|
252
|
+
a: string,
|
|
253
|
+
b: string,
|
|
254
|
+
label: DedupePairRecord["label"],
|
|
255
|
+
similarity: number,
|
|
256
|
+
meta: DedupePairRecord["meta"],
|
|
257
|
+
needsReview = false,
|
|
258
|
+
): void {
|
|
259
|
+
const key = `${label}\x00${a}\x00${b}`;
|
|
260
|
+
if (seen.has(key)) return; // one record per unique pair across runs
|
|
261
|
+
seen.add(key);
|
|
262
|
+
const rec: DedupePairRecord = { v: 1, kind: "dedupe_pair", a, b, label, similarity, source: "tokenOverlap", meta };
|
|
263
|
+
if (needsReview) rec.needs_review = true;
|
|
264
|
+
out.push(rec);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---- entry-point ---------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Build the calibration corpora from an ordered session-branch entry iterable
|
|
271
|
+
* (e.g. `ctx.sessionManager.getBranch()`). Pure and deterministic.
|
|
272
|
+
*
|
|
273
|
+
* Walk notes:
|
|
274
|
+
* - A dedupe run that APPLIED deltas appends its post-run `harness-state`
|
|
275
|
+
* snapshot immediately before its `harness-refinement` audit entry, so at
|
|
276
|
+
* an audit entry the last snapshot is the CURRENT state either way.
|
|
277
|
+
* - A no-merge run writes NO snapshot: the last snapshot is an older state
|
|
278
|
+
* that is still the current store (candidates emit from it).
|
|
279
|
+
* - Legacy audit entries (pre-0.11.0) carry `applied` as a COUNT with no
|
|
280
|
+
* delta details: dup reconstruction is skipped, candidates still emit.
|
|
281
|
+
*/
|
|
282
|
+
export function buildCorpus(entries: Iterable<unknown>, opts: CorpusOptions = {}): CorpusResult {
|
|
283
|
+
const citeBump = opts.citeBump ?? DEFAULT_REF_BUMP;
|
|
284
|
+
const pairs: DedupePairRecord[] = [];
|
|
285
|
+
const lifecycle: LifecycleRecord[] = [];
|
|
286
|
+
const seen = new Set<string>();
|
|
287
|
+
const snapshots: HarnessState[] = [];
|
|
288
|
+
for (const raw of entries) {
|
|
289
|
+
const entry = raw as AnyEntry;
|
|
290
|
+
if (entry.type === "custom" && entry.customType === STATE_ENTRY && entry.data?.state) {
|
|
291
|
+
const next = entry.data.state;
|
|
292
|
+
const prev = snapshots.at(-1);
|
|
293
|
+
if (prev) diffSnapshots(prev, next, citeBump, lifecycle);
|
|
294
|
+
snapshots.push(next);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (
|
|
298
|
+
entry.type === "custom" &&
|
|
299
|
+
entry.customType === REFINE_ENTRY &&
|
|
300
|
+
entry.data?.proposer === "dedupe"
|
|
301
|
+
) {
|
|
302
|
+
const current = snapshots.at(-1);
|
|
303
|
+
if (!current) continue; // no state seen yet — nothing to compare
|
|
304
|
+
const deltas = Array.isArray(entry.data.appliedDeltas) ? entry.data.appliedDeltas : [];
|
|
305
|
+
const pre = snapshots.at(-2) ?? { items: [] };
|
|
306
|
+
collectDedupePairs(current, pre, deltas, seen, pairs);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { pairs, lifecycle };
|
|
310
|
+
}
|
package/src/harness.ts
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
|
|
26
26
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
27
27
|
import { stat } from "node:fs/promises";
|
|
28
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
29
|
+
import { join } from "node:path";
|
|
28
30
|
import {
|
|
29
31
|
applyDeltas,
|
|
30
32
|
bumpImportance,
|
|
@@ -41,11 +43,13 @@ import {
|
|
|
41
43
|
import {
|
|
42
44
|
defaultScopeForPath,
|
|
43
45
|
layerFilesFor,
|
|
46
|
+
loadConfig,
|
|
44
47
|
projectDurablePath,
|
|
45
48
|
projectSlug,
|
|
46
49
|
} from "./config.js";
|
|
47
50
|
import { KIND_LABEL } from "./types.js";
|
|
48
51
|
import type { ComponentKind, Delta, HarnessItem } from "./types.js";
|
|
52
|
+
import { buildCorpus } from "./corpus.js";
|
|
49
53
|
|
|
50
54
|
/** One row of the /harness subcommand menu (label = name, value = name). */
|
|
51
55
|
interface CompletionEntry {
|
|
@@ -56,6 +60,7 @@ interface CompletionEntry {
|
|
|
56
60
|
const SUBCOMMANDS: CompletionEntry[] = [
|
|
57
61
|
{ name: "import", description: "Import durable state, layered (--prune to prune stale items)" },
|
|
58
62
|
{ name: "export", description: "Export active items to durable layers (or one file with a path)" },
|
|
63
|
+
{ name: "export-corpus", description: "Export calibration corpus JSONL for pi-reflex (dedupe pairs + lifecycle)" },
|
|
59
64
|
{ name: "status", description: "Show harness status (active/total, per-kind counts, durable layers)" },
|
|
60
65
|
{ name: "prune", description: "Decay & prune inactive items (--decay <days>)" },
|
|
61
66
|
{ name: "keep", description: "Bump item importance (+0.1)" },
|
|
@@ -218,6 +223,9 @@ export function registerHarness(pi: ExtensionAPI): void {
|
|
|
218
223
|
case "export":
|
|
219
224
|
await handleExport(ctx, rest);
|
|
220
225
|
return;
|
|
226
|
+
case "export-corpus":
|
|
227
|
+
await handleExportCorpus(ctx, rest);
|
|
228
|
+
return;
|
|
221
229
|
case "prune":
|
|
222
230
|
await handlePrune(pi, ctx, rest);
|
|
223
231
|
return;
|
|
@@ -320,6 +328,38 @@ async function handleExport(ctx: ExtensionCommandContext, rest: string[]): Promi
|
|
|
320
328
|
ctx.ui.setStatus("harness", undefined);
|
|
321
329
|
}
|
|
322
330
|
|
|
331
|
+
function toJsonl(rows: unknown[]): string {
|
|
332
|
+
return rows.length > 0 ? `${rows.map((r) => JSON.stringify(r)).join("\n")}\n` : "";
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** `/harness export-corpus [path]` — write the calibration corpora (contract
|
|
336
|
+
* §4) from THIS session branch's audit trail. Default dir:
|
|
337
|
+
* ./harness-corpus/<yyyy-mm-dd>/. Local file output only — contract invariant:
|
|
338
|
+
* nothing leaves the machine. */
|
|
339
|
+
async function handleExportCorpus(ctx: ExtensionCommandContext, rest: string[]): Promise<void> {
|
|
340
|
+
const explicit = explicitPath(rest);
|
|
341
|
+
const dir = explicit ?? join("harness-corpus", new Date().toISOString().slice(0, 10));
|
|
342
|
+
ctx.ui.setStatus("harness", "Exporting calibration corpus…");
|
|
343
|
+
try {
|
|
344
|
+
// Config-aware citation classification: the outcome loop's bump is
|
|
345
|
+
// configurable (default 0.03), so the classifier must not hardcode it.
|
|
346
|
+
const { outcomeImportance } = await loadConfig();
|
|
347
|
+
const corpus = buildCorpus(ctx.sessionManager.getBranch() as Iterable<unknown>, {
|
|
348
|
+
citeBump: outcomeImportance?.bump,
|
|
349
|
+
});
|
|
350
|
+
await mkdir(dir, { recursive: true });
|
|
351
|
+
await writeFile(join(dir, "dedupe-pairs.jsonl"), toJsonl(corpus.pairs));
|
|
352
|
+
await writeFile(join(dir, "lifecycle.jsonl"), toJsonl(corpus.lifecycle));
|
|
353
|
+
ctx.ui.notify(
|
|
354
|
+
`Calibration corpus → ${dir}: ${corpus.pairs.length} dedupe pair(s), ${corpus.lifecycle.length} lifecycle event(s). Local files only — nothing leaves the machine.`,
|
|
355
|
+
"info",
|
|
356
|
+
);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
ctx.ui.notify(`/harness export-corpus failed: ${(err as Error).message}`, "error");
|
|
359
|
+
}
|
|
360
|
+
ctx.ui.setStatus("harness", undefined);
|
|
361
|
+
}
|
|
362
|
+
|
|
323
363
|
async function handlePrune(
|
|
324
364
|
pi: ExtensionAPI,
|
|
325
365
|
ctx: ExtensionCommandContext,
|
package/src/index.ts
CHANGED
|
@@ -92,6 +92,19 @@ export {
|
|
|
92
92
|
type SimilarityResult,
|
|
93
93
|
} from "./proposer.js";
|
|
94
94
|
|
|
95
|
+
// Calibration corpus exporter (pi-reflex contract §4, formerly pi-jev). Re-exported so companion
|
|
96
|
+
// packages can build corpora from exported session branches without reaching
|
|
97
|
+
// into internal paths.
|
|
98
|
+
export {
|
|
99
|
+
buildCorpus,
|
|
100
|
+
NOT_DUP_FLOOR,
|
|
101
|
+
parseDedupeDelete,
|
|
102
|
+
type CorpusResult,
|
|
103
|
+
type DedupePairRecord,
|
|
104
|
+
type LifecycleEvent,
|
|
105
|
+
type LifecycleRecord,
|
|
106
|
+
} from "./corpus.js";
|
|
107
|
+
|
|
95
108
|
// Injection selection policy (on by default). Re-exported so companion packages
|
|
96
109
|
// and tests can reuse the pure selection/normalization without reaching into
|
|
97
110
|
// internal paths.
|
package/src/refine.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { Context, Model, TextContent } from "@earendil-works/pi-ai";
|
|
|
22
22
|
import { applyDeltas, DEFAULT_DURABLE_PATH, exportDurableLayers, modelKey, PROJECT_DURABLE_DIR, REFINE_ENTRY, snapshotState } from "./store.js";
|
|
23
23
|
import { getProposer, DEFAULT_DEDUPE } from "./proposer.js";
|
|
24
24
|
import type { CompleteOptions, CompleteResult } from "./proposer.js";
|
|
25
|
+
import type { AppliedDelta } from "./types.js";
|
|
25
26
|
import { loadConfig, projectSlug } from "./config.js";
|
|
26
27
|
|
|
27
28
|
const DEFAULT_EVIDENCE_BYTES = 16000;
|
|
@@ -125,6 +126,7 @@ export async function runRefine(
|
|
|
125
126
|
const ownerKey = modelKey(ctx.model);
|
|
126
127
|
|
|
127
128
|
let applied = 0;
|
|
129
|
+
let appliedDeltas: AppliedDelta[] = [];
|
|
128
130
|
let applyError: string | undefined;
|
|
129
131
|
if (proposedDeltas.length > 0) {
|
|
130
132
|
// Direct-apply path (rule-based / model proposers): persist each batch the
|
|
@@ -134,7 +136,7 @@ export async function runRefine(
|
|
|
134
136
|
// race during the await above. It rolls back in-memory state before throwing,
|
|
135
137
|
// so we surface the failure as an audited no-op rather than crashing /refine.
|
|
136
138
|
try {
|
|
137
|
-
|
|
139
|
+
appliedDeltas = applyDeltas(
|
|
138
140
|
proposedDeltas.map((d) => {
|
|
139
141
|
const delta = d.delta;
|
|
140
142
|
return ownerKey !== undefined && delta.op === "create"
|
|
@@ -164,6 +166,10 @@ export async function runRefine(
|
|
|
164
166
|
source,
|
|
165
167
|
proposer: proposer.name,
|
|
166
168
|
applied,
|
|
169
|
+
// Full applied-delta list (incl. delete reasons) — richer than the count:
|
|
170
|
+
// the calibration-corpus exporter (corpus.ts) reconstructs dedupe pairs
|
|
171
|
+
// from it. Legacy readers only consuming `applied` are unaffected.
|
|
172
|
+
appliedDeltas,
|
|
167
173
|
rationales: proposedDeltas.map((d) => d.rationale),
|
|
168
174
|
// Hidden model spend made visible: dedicated-model proposers report what the
|
|
169
175
|
// call cost (model, tokens, latency, ok/error). Absent for rule-based/steering.
|