pi-mega-compact 0.20.6 → 0.20.7

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,199 @@
1
+ /**
2
+ * dedup-audit.ts — durable audit trail for dedup tier decisions
3
+ * (external-audit item #2).
4
+ *
5
+ * Before this module a tier decision existed only as the in-process `onTier`
6
+ * callback that paints the live UI; nothing survived the process, so an
7
+ * operator could not answer "which layer collapsed this region, onto what, at
8
+ * what similarity?" — the inputs needed to tune the thresholds in
9
+ * config/dedup.ts. Here each decision is appended to the repo's events.log as
10
+ * one structured JSON line (see `DedupAuditEvent` below).
11
+ *
12
+ * The event type and its append helper live HERE rather than in monitoring.ts:
13
+ * monitoring.ts already owns three concerns (decision events, the dashboard.json
14
+ * metrics snapshot, FP alerting) and sits close to its 300-line soft limit, so
15
+ * co-locating the shape with the only recorder that produces it keeps both files
16
+ * under the headroom gate. monitoring.ts re-exports both for callers (and the
17
+ * dashboard SSE tail) that treat it as the events.log barrel.
18
+ *
19
+ * Design constraints:
20
+ * - PURE INSTRUMENTATION. Nothing in this file may influence a dedup outcome.
21
+ * - Best-effort/non-fatal: `logDedupAudit` swallows IO errors, and the emitter
22
+ * itself is wrapped so a malformed field can never break add().
23
+ * - Honest fields only: a value is emitted only where the caller actually
24
+ * computed it. L0/L1 are hash/verify tiers and pass no `similarity`.
25
+ * - Signal, not chatter: callers emit on DECISIONS (a match, a scored
26
+ * candidate, the final outcome), never on every "scanning" transition.
27
+ * - Flag-gated by cfg.DEDUP_AUDIT (default ON; OFF writes nothing at all).
28
+ *
29
+ * PREVENT-PI-004: local filesystem append only, no network.
30
+ */
31
+
32
+ import { appendFileSync, mkdirSync } from "node:fs";
33
+ import { dirname } from "node:path";
34
+ import { defaultEventsPath } from "../monitoring.js";
35
+
36
+ /**
37
+ * One dedup decision, as persisted to events.log.
38
+ *
39
+ * `ts` is an ISO-8601 string (not the epoch millis DedupDecisionEvent uses):
40
+ * the dashboard SSE tail only forwards lines carrying a `type` discriminator,
41
+ * and its consumers parse the timestamp as a date. Optional fields are omitted
42
+ * rather than nulled — a missing `similarity` means the tier computed no score,
43
+ * which is itself the honest signal (L0/L1 are hash/verify tiers).
44
+ */
45
+ export interface DedupAuditEvent {
46
+ /** SSE discriminator — the dashboard streams only typed lines. */
47
+ type: "dedup_audit";
48
+ /** ISO-8601 decision timestamp. */
49
+ ts: string;
50
+ /** Normalized session the region belongs to. */
51
+ sessionId: string;
52
+ /** Which layer produced the decision ("new" = nothing collapsed). */
53
+ tier: "L0" | "L1" | "L2" | "new";
54
+ /** What the layer decided. */
55
+ status: "deduped" | "passed" | "stored";
56
+ /** Checkpoint the region collapsed onto, or the nearest one scored. */
57
+ matchedEntry?: string;
58
+ /** Checkpoint created, when the outcome was a new write. */
59
+ storedEntry?: string;
60
+ /** Cosine similarity — present only where a tier actually scored. */
61
+ similarity?: number;
62
+ /** Why the decision went the way it did (e.g. "contentHash", "mark_only"). */
63
+ dedupReason?: string;
64
+ /** Tokens the original region occupied before compaction. */
65
+ originalTokenEstimate?: number;
66
+ /** Tokens the stored summary occupies. */
67
+ tokenEstimate?: number;
68
+ }
69
+
70
+ /**
71
+ * Append one audit event to events.log (best-effort, never throws).
72
+ *
73
+ * Same append-one-JSON-line contract as monitoring.ts's logDecision — an
74
+ * unwritable path is swallowed so instrumentation can never break add().
75
+ */
76
+ export function logDedupAudit(path: string, ev: DedupAuditEvent): void {
77
+ try {
78
+ mkdirSync(dirname(path), { recursive: true });
79
+ appendFileSync(path, `${JSON.stringify(ev)}\n`, "utf-8");
80
+ } catch {
81
+ /* best-effort — never break the extension on a log failure */
82
+ }
83
+ }
84
+
85
+ /** The slice of VectorStore the audit emitter reads. */
86
+ export interface DedupAuditContext {
87
+ /** Per-repo state dir — resolves the events.log the dashboard tails. */
88
+ readonly stateDir: string;
89
+ /** Explicit events.log override (tests / the Sprint-14 monitoring opt-in). */
90
+ readonly eventsPath?: string;
91
+ /** Whether the audit trail is enabled (cfg.DEDUP_AUDIT). */
92
+ readonly auditEnabled: boolean;
93
+ }
94
+
95
+ /** The decision payload a call site supplies; ts/type are filled in here. */
96
+ export type DedupAuditInput = Omit<DedupAuditEvent, "type" | "ts">;
97
+
98
+ /**
99
+ * The per-add() facts every decision in one cascade shares: which session, and
100
+ * the token accounting already computed by the caller.
101
+ */
102
+ export interface DedupAuditScope {
103
+ sessionId: string;
104
+ /** Tokens the original region occupied (VectorStore.add's `origTokens`). */
105
+ originalTokenEstimate?: number;
106
+ /** Tokens the stored summary occupies (AddInput.tokenEstimate). */
107
+ tokenEstimate?: number;
108
+ }
109
+
110
+ /**
111
+ * A decision recorder bound to one add() cascade.
112
+ *
113
+ * `deduped` / `passed` / `stored` are the three shapes the cascade actually
114
+ * produces; binding them here keeps VectorStore.add's call sites to one line
115
+ * each (delegate-shell pattern) and keeps the field-honesty rules — which tier
116
+ * may carry a similarity, matched vs. created id — in a single place.
117
+ */
118
+ export interface DedupAuditRecorder {
119
+ /** A tier collapsed the region onto `matchedEntry`. */
120
+ deduped(
121
+ tier: "L0" | "L1" | "L2",
122
+ matchedEntry: string,
123
+ dedupReason: string,
124
+ similarity?: number,
125
+ ): void;
126
+ /** A tier scored a candidate but did not collapse (threshold near-miss). */
127
+ passed(
128
+ tier: "L0" | "L1" | "L2",
129
+ matchedEntry: string,
130
+ similarity: number,
131
+ ): void;
132
+ /** Final outcome: nothing collapsed, a new checkpoint was written. */
133
+ stored(storedEntry: string, dedupReason: string, tokenEstimate: number): void;
134
+ }
135
+
136
+ /** Build a recorder bound to one add() cascade. */
137
+ export function dedupAuditRecorder(
138
+ ctx: DedupAuditContext,
139
+ scope: DedupAuditScope,
140
+ ): DedupAuditRecorder {
141
+ const base = {
142
+ sessionId: scope.sessionId,
143
+ originalTokenEstimate: scope.originalTokenEstimate,
144
+ tokenEstimate: scope.tokenEstimate,
145
+ };
146
+ return {
147
+ deduped: (tier, matchedEntry, dedupReason, similarity) =>
148
+ emitDedupAudit(ctx, {
149
+ ...base,
150
+ tier,
151
+ status: "deduped",
152
+ matchedEntry,
153
+ dedupReason,
154
+ ...(similarity === undefined ? {} : { similarity }),
155
+ }),
156
+ passed: (tier, matchedEntry, similarity) =>
157
+ emitDedupAudit(ctx, {
158
+ ...base,
159
+ tier,
160
+ status: "passed",
161
+ matchedEntry,
162
+ similarity,
163
+ }),
164
+ stored: (storedEntry, dedupReason, tokenEstimate) =>
165
+ emitDedupAudit(ctx, {
166
+ ...base,
167
+ tier: "new",
168
+ status: "stored",
169
+ storedEntry,
170
+ dedupReason,
171
+ tokenEstimate,
172
+ }),
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Append one dedup decision to events.log.
178
+ *
179
+ * Resolves the target path from the explicit `eventsPath` when a caller opted
180
+ * in (Sprint 14 monitoring / tests), otherwise from the store's own per-repo
181
+ * state dir — production never passes `eventsPath`, so defaulting is what makes
182
+ * the audit trail actually exist on a real device.
183
+ */
184
+ export function emitDedupAudit(
185
+ ctx: DedupAuditContext,
186
+ input: DedupAuditInput,
187
+ ): void {
188
+ if (!ctx.auditEnabled) return;
189
+ try {
190
+ const path = ctx.eventsPath ?? defaultEventsPath(ctx.stateDir);
191
+ logDedupAudit(path, {
192
+ type: "dedup_audit",
193
+ ts: new Date().toISOString(),
194
+ ...input,
195
+ });
196
+ } catch {
197
+ /* instrumentation must never break the add() path */
198
+ }
199
+ }
@@ -8,6 +8,7 @@
8
8
  * "./vectorStore.js" are unchanged.
9
9
  */
10
10
  export { VectorStore } from "./vectorStore/class.js";
11
+ export { addCheckpoint } from "./vectorStore/add.js";
11
12
  export { computeRegionHash } from "./vectorStore/hash.js";
12
13
  export {
13
14
  L2_ENABLED,