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/CHANGELOG.md +361 -0
- package/README.md +25 -14
- package/command/dejavu.md +26 -0
- package/index.ts +415 -39
- package/package.json +6 -2
- package/scripts/analyze.ts +59 -0
- package/scripts/doctor.ts +505 -0
- package/scripts/githooks/commit-msg +46 -0
- package/scripts/migrate.ts +49 -0
- package/skills/dejavu/SKILL.md +48 -0
- package/src/AGENTS.md +56 -8
- package/src/patterns.ts +680 -36
- package/src/store.ts +794 -181
- package/src/validate.ts +58 -7
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,
|
|
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:
|
|
48
|
-
lastSeen:
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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)) {
|