opencode-dejavu 2.7.0 → 2.27.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/src/validate.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * coerceGateShape + repairGate satisfies the data-model invariants.
6
6
  */
7
7
  import type { Gate } from "./store"
8
- import { canBlock, canRemind, scrubSecrets } from "./patterns"
8
+ import { canBlock, canRemind, looksLikeSuccess, sanitizeForStore, suggestCorrection } from "./patterns"
9
9
 
10
10
  /** sha1 prefix-12, the only key shape patternKey ever emits */
11
11
  const KEY_SHAPE = /^[0-9a-f]{12}$/
@@ -15,6 +15,10 @@ const SNIPPET_MAX = 200
15
15
  const SESSION_STATE_TTL_MS = 24 * 60 * 60 * 1000
16
16
  /** bound per-gate session state so long-lived gates cannot bloat */
17
17
  const SESSION_STATE_CAP = 50
18
+ /** The auto-correction template is a FIXED shape — a correction matching it
19
+ * byte-for-byte around its quoted snippet is machine-generated; anything else
20
+ * is a human/agent edit and must never be re-derived. */
21
+ const AUTO_TEMPLATE_CORRECTION = /^Last error: "(.*)" — address that specific error before retrying this exact call\.$/
18
22
 
19
23
  /**
20
24
  * Structural parse of one persisted gate object. Returns a well-shaped Gate
@@ -34,6 +38,10 @@ export function coerceGateShape(raw: unknown): Gate | null {
34
38
  const strings = (v: unknown): string[] =>
35
39
  Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : []
36
40
  const str = (v: unknown, fallback: string): string => (typeof v === "string" ? v : fallback)
41
+ // Unparseable dates would make a gate immortal (expire() compares
42
+ // Date.parse(...) < cutoff; NaN never is) — reset them instead.
43
+ const dateStr = (v: unknown, fallback: string): string =>
44
+ typeof v === "string" && !Number.isNaN(Date.parse(v)) ? v : fallback
37
45
  const now = new Date().toISOString()
38
46
 
39
47
  const gate: Gate = {
@@ -44,19 +52,39 @@ export function coerceGateShape(raw: unknown): Gate | null {
44
52
  count: num(r.count, 0),
45
53
  sessions: strings(r.sessions),
46
54
  projects: strings(r.projects),
47
- firstSeen: str(r.firstSeen, now),
48
- lastSeen: str(r.lastSeen, now),
55
+ firstSeen: dateStr(r.firstSeen, now),
56
+ lastSeen: dateStr(r.lastSeen, now),
49
57
  snippet: str(r.snippet, ""),
50
58
  remindedCount: num(r.remindedCount, 0),
51
59
  blockedCount: num(r.blockedCount, 0),
52
60
  recurredAfterReminder: num(r.recurredAfterReminder, 0),
53
61
  recurredAfterGate: num(r.recurredAfterGate, 0),
62
+ overrideCount: num(r.overrideCount, 0),
54
63
  }
55
- if (typeof r.succeededAfterGate === "number" && Number.isFinite(r.succeededAfterGate) && r.succeededAfterGate > 0) {
64
+ if (typeof r.succeededAfterGate === "number" && Number.isFinite(r.succeededAfterGate) && r.succeededAfterGate >= 0) {
56
65
  gate.succeededAfterGate = Math.floor(r.succeededAfterGate)
57
66
  }
67
+ if (typeof r.promotionCount === "number" && Number.isFinite(r.promotionCount) && r.promotionCount > 0) {
68
+ gate.promotionCount = Math.floor(r.promotionCount)
69
+ }
58
70
  if (typeof r.correction === "string") gate.correction = r.correction
59
71
  if (r.review === true) gate.review = true
72
+ if (r.feedbackDemoted === true) gate.feedbackDemoted = true
73
+ if (Array.isArray(r.reoffenseSessions)) {
74
+ const sessions = r.reoffenseSessions.filter((x): x is string => typeof x === "string")
75
+ if (sessions.length > 0) gate.reoffenseSessions = sessions.slice(-SESSION_STATE_CAP)
76
+ }
77
+ if (r.feedbackBaseline !== null && typeof r.feedbackBaseline === "object" && !Array.isArray(r.feedbackBaseline)) {
78
+ const b = r.feedbackBaseline as Record<string, unknown>
79
+ const recurred = typeof b.recurred === "number" && Number.isFinite(b.recurred) && b.recurred >= 0 ? Math.floor(b.recurred) : 0
80
+ const overrides = typeof b.overrides === "number" && Number.isFinite(b.overrides) && b.overrides >= 0 ? Math.floor(b.overrides) : 0
81
+ if (recurred > 0 || overrides > 0) gate.feedbackBaseline = { recurred, overrides }
82
+ }
83
+ if (r.retireBaseline !== null && typeof r.retireBaseline === "object" && !Array.isArray(r.retireBaseline)) {
84
+ const b = r.retireBaseline as Record<string, unknown>
85
+ const count = typeof b.count === "number" && Number.isFinite(b.count) && b.count >= 0 ? Math.floor(b.count) : 0
86
+ if (count > 0) gate.retireBaseline = { count }
87
+ }
60
88
  if (r.remindedSessions !== null && typeof r.remindedSessions === "object" && !Array.isArray(r.remindedSessions)) {
61
89
  const sessions: Record<string, number> = {}
62
90
  for (const [session, at] of Object.entries(r.remindedSessions as Record<string, unknown>)) {
@@ -95,22 +123,43 @@ export function repairGate(gate: Gate): boolean {
95
123
  gate.lastSeen = swap
96
124
  changed = true
97
125
  }
126
+ // The retirement baseline anchors a count AT retirement — it can never exceed
127
+ // the lifetime count. Clamp a corrupted overshoot; dropping it instead would
128
+ // re-open the instant re-promotion the baseline exists to damp.
129
+ if (gate.retireBaseline !== undefined && gate.retireBaseline.count > gate.count) {
130
+ gate.retireBaseline.count = gate.count
131
+ changed = true
132
+ }
98
133
  if (gate.snippet.length > SNIPPET_MAX) {
99
134
  gate.snippet = gate.snippet.slice(0, SNIPPET_MAX)
100
135
  changed = true
101
136
  }
102
- const signature = scrubSecrets(gate.signature)
137
+ // A success-shaped snippet is not failure evidence — clear it so the next
138
+ // failure re-captures a real error line (heals legacy data at the boundary).
139
+ if (looksLikeSuccess(gate.snippet)) {
140
+ gate.snippet = ""
141
+ changed = true
142
+ }
143
+ // AUTO_TEMPLATE corrections are machine-made, so re-derive from current evidence on every repair: success-shaped quotes and stale platform advice reach old gates; human edits never match the template byte-for-byte and are untouched.
144
+ if (gate.correction !== undefined && AUTO_TEMPLATE_CORRECTION.test(gate.correction)) {
145
+ const rederived = suggestCorrection(gate.signature, gate.snippet)
146
+ if (rederived !== gate.correction) {
147
+ gate.correction = rederived
148
+ changed = true
149
+ }
150
+ }
151
+ const signature = sanitizeForStore(gate.signature)
103
152
  if (signature !== gate.signature) {
104
153
  gate.signature = signature
105
154
  changed = true
106
155
  }
107
- const snippet = scrubSecrets(gate.snippet)
156
+ const snippet = sanitizeForStore(gate.snippet)
108
157
  if (snippet !== gate.snippet) {
109
158
  gate.snippet = snippet
110
159
  changed = true
111
160
  }
112
161
  if (gate.correction !== undefined) {
113
- let correction = scrubSecrets(gate.correction)
162
+ let correction = sanitizeForStore(gate.correction)
114
163
  if (correction.length > SNIPPET_MAX) {
115
164
  // Unbounded corrections are a context-pollution vector; the companion
116
165
  // skill mandates one actionable line anyway.
@@ -165,6 +214,8 @@ export function repairGate(gate: Gate): boolean {
165
214
  // blocking to reminding if the shape can still remind, else to watching.
166
215
  if (gate.status === "blocking" && !canBlock(gate.tool, gate.signature)) {
167
216
  gate.status = canRemind(gate.tool, gate.signature) ? "reminding" : "watching"
217
+ // reset at the transition: the old tier's counter is stale; reminding accrues fresh
218
+ if (gate.recurredAfterReminder > 0) gate.recurredAfterReminder = 0
168
219
  changed = true
169
220
  }
170
221
  if (gate.status === "reminding" && !canRemind(gate.tool, gate.signature)) {