dsh-auto-review 0.4.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/LICENSE +201 -0
  3. package/README.es.md +226 -0
  4. package/README.hi.md +226 -0
  5. package/README.md +226 -0
  6. package/README.pt.md +226 -0
  7. package/README.zh.md +226 -0
  8. package/cordis.patch.yml +27 -0
  9. package/lib/client.js +494 -0
  10. package/lib/client.js.map +1 -0
  11. package/lib/events-CFl6CHZi.js +344 -0
  12. package/lib/index.js +5305 -0
  13. package/lib/invariant.js +223 -0
  14. package/lib/types/client/ReviewPanel.d.ts +29 -0
  15. package/lib/types/client/ReviewPanel.d.ts.map +1 -0
  16. package/lib/types/client/ReviewPanel.js +63 -0
  17. package/lib/types/client/ReviewPanel.js.map +1 -0
  18. package/lib/types/client/index.d.ts +31 -0
  19. package/lib/types/client/index.d.ts.map +1 -0
  20. package/lib/types/client/index.js +51 -0
  21. package/lib/types/client/index.js.map +1 -0
  22. package/lib/types/client/locales.d.ts +49 -0
  23. package/lib/types/client/locales.d.ts.map +1 -0
  24. package/lib/types/client/locales.js +82 -0
  25. package/lib/types/client/locales.js.map +1 -0
  26. package/lib/types/client/present.d.ts +25 -0
  27. package/lib/types/client/present.d.ts.map +1 -0
  28. package/lib/types/client/present.js +36 -0
  29. package/lib/types/client/present.js.map +1 -0
  30. package/lib/types/client/styles.d.ts +9 -0
  31. package/lib/types/client/styles.d.ts.map +1 -0
  32. package/lib/types/client/styles.js +130 -0
  33. package/lib/types/client/styles.js.map +1 -0
  34. package/lib/types/config.d.ts +200 -0
  35. package/lib/types/config.d.ts.map +1 -0
  36. package/lib/types/config.js +154 -0
  37. package/lib/types/config.js.map +1 -0
  38. package/lib/types/events.d.ts +347 -0
  39. package/lib/types/events.d.ts.map +1 -0
  40. package/lib/types/events.js +369 -0
  41. package/lib/types/events.js.map +1 -0
  42. package/lib/types/index.d.ts +26 -0
  43. package/lib/types/index.d.ts.map +1 -0
  44. package/lib/types/index.js +26 -0
  45. package/lib/types/index.js.map +1 -0
  46. package/lib/types/invariant.d.ts +28 -0
  47. package/lib/types/invariant.d.ts.map +1 -0
  48. package/lib/types/invariant.js +306 -0
  49. package/lib/types/invariant.js.map +1 -0
  50. package/lib/types/messages.d.ts +38 -0
  51. package/lib/types/messages.d.ts.map +1 -0
  52. package/lib/types/messages.js +65 -0
  53. package/lib/types/messages.js.map +1 -0
  54. package/lib/types/projection-types.d.ts +61 -0
  55. package/lib/types/projection-types.d.ts.map +1 -0
  56. package/lib/types/projection-types.js +8 -0
  57. package/lib/types/projection-types.js.map +1 -0
  58. package/lib/types/projection.d.ts +93 -0
  59. package/lib/types/projection.d.ts.map +1 -0
  60. package/lib/types/projection.js +197 -0
  61. package/lib/types/projection.js.map +1 -0
  62. package/lib/types/review.d.ts +141 -0
  63. package/lib/types/review.d.ts.map +1 -0
  64. package/lib/types/review.js +389 -0
  65. package/lib/types/review.js.map +1 -0
  66. package/lib/types/runtime.d.ts +160 -0
  67. package/lib/types/runtime.d.ts.map +1 -0
  68. package/lib/types/runtime.js +486 -0
  69. package/lib/types/runtime.js.map +1 -0
  70. package/package.json +132 -0
  71. package/src/client/ReviewPanel.tsx +164 -0
  72. package/src/client/index.ts +72 -0
  73. package/src/client/locales.ts +87 -0
  74. package/src/client/present.ts +58 -0
  75. package/src/client/styles.ts +131 -0
  76. package/src/config.ts +346 -0
  77. package/src/events.ts +553 -0
  78. package/src/index.ts +28 -0
  79. package/src/invariant.ts +326 -0
  80. package/src/messages.ts +91 -0
  81. package/src/projection-types.ts +61 -0
  82. package/src/projection.ts +224 -0
  83. package/src/review.ts +428 -0
  84. package/src/runtime.ts +562 -0
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Package-owned invariant companion for `dsh-auto-review`:
3
+ *
4
+ * 1. Every `autoReview/verdict` audit event references an `approval/asked`
5
+ * event that precedes it in the same session, at most one verdict exists
6
+ * per asked event, and payload fields obey the closed vocabularies
7
+ * (decision/reason/riskLevel/outcome/fallback). The same chain holds for
8
+ * `autoReview/rejection` events (hard-disable rejections).
9
+ * 2. Model-visible ⟺ logged: every `tool/result` carrying the deny marker
10
+ * links to a recorded `deny` verdict with a matching call id, every
11
+ * `tool/result` carrying the fallback marker links to a recorded fallback
12
+ * verdict that was rejected, and every `tool/result` carrying the never
13
+ * marker links to a recorded rejection that settled rejected.
14
+ *
15
+ * @module dsh-auto-review/invariant
16
+ */
17
+
18
+ import type { Context } from '@deepseek-ai/cordis'
19
+ import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
20
+ import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
21
+ import { AUTO_REVIEW_FALLBACKS, CIRCUIT_MARKER_PATTERN, DENY_MARKER_PATTERN, FALLBACK_MARKER_PATTERN, NEVER_MARKER_PATTERN } from './events.ts'
22
+
23
+ const PACKAGE_NAME = 'dsh-auto-review'
24
+
25
+ /** Cordis companion plugin name. */
26
+ export const name = 'auto-review-invariant'
27
+ /** Service required before the companion can reserve package ownership. */
28
+ export const inject = ['invariants']
29
+
30
+ const DECISIONS: readonly string[] = ['allow', 'deny']
31
+ const RISK_LEVELS: readonly string[] = ['low', 'medium', 'high']
32
+ const OUTCOMES: readonly string[] = ['allowed-once', 'rejected', 'cancelled', 'unavailable']
33
+ const TRIP_KINDS: readonly string[] = ['consecutive', 'window']
34
+ const CIRCUIT_ACTIONS: readonly string[] = ['delegate', 'reject', 'abort-turn']
35
+
36
+ interface VerdictRecord {
37
+ readonly callId: string | undefined
38
+ readonly decision: string | undefined
39
+ readonly outcome: string | undefined
40
+ readonly fallback: string | undefined
41
+ readonly escalation: string | undefined
42
+ }
43
+
44
+ /** One recorded hard-disable rejection, for never-marker reconstruction. */
45
+ interface RejectionRecord {
46
+ readonly callId: string | undefined
47
+ readonly outcome: string | undefined
48
+ }
49
+
50
+ /* jscpd:ignore-start -- package companions share replay and dispatch plumbing */
51
+ /** Install verdict-chain and deny-marker validation over loaded logs and newly appended events. */
52
+ const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
53
+ // Install-scoped so a dispose/re-register cycle re-sweeps from a clean slate.
54
+ const askedIds = new WeakMap<Session, Map<string, number>>()
55
+ const verdicts = new WeakMap<Session, Map<string, VerdictRecord>>()
56
+ const verdictByApproval = new WeakMap<Session, Map<string, VerdictRecord>>()
57
+ const circuits = new WeakMap<Session, Map<string, string>>()
58
+ const rejections = new WeakMap<Session, Map<string, RejectionRecord>>()
59
+
60
+ const validateEvent = (session: Session, event: SessionEvent): void => {
61
+ if (event.type === 'approval/asked') {
62
+ let ids = askedIds.get(session)
63
+ if (ids === undefined) {
64
+ ids = new Map<string, number>()
65
+ askedIds.set(session, ids)
66
+ }
67
+ ids.set(event.data.id, (ids.get(event.data.id) ?? 0) + 1)
68
+ return
69
+ }
70
+ if (event.type === 'autoReview/verdict') {
71
+ const data = event.data
72
+ const fallback = data.fallback
73
+ if (fallback === undefined) {
74
+ if (data.decision === undefined || !DECISIONS.includes(data.decision)) {
75
+ fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} has invalid decision ${JSON.stringify(data.decision)}`)
76
+ }
77
+ if (typeof data.reason !== 'string' || data.reason.length === 0) {
78
+ fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} has no reviewer reason`)
79
+ }
80
+ if (data.riskLevel !== undefined && !RISK_LEVELS.includes(data.riskLevel)) {
81
+ fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} has invalid riskLevel ${JSON.stringify(data.riskLevel)}`)
82
+ }
83
+ } else {
84
+ if (!AUTO_REVIEW_FALLBACKS.includes(fallback)) {
85
+ fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} has invalid fallback ${JSON.stringify(fallback)}`)
86
+ }
87
+ if (data.decision !== undefined || data.reason !== undefined || data.riskLevel !== undefined) {
88
+ fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} carries verdict fields alongside fallback ${JSON.stringify(fallback)}`)
89
+ }
90
+ }
91
+ if (data.outcome !== undefined && !OUTCOMES.includes(data.outcome)) {
92
+ fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} has invalid outcome ${JSON.stringify(data.outcome)}`)
93
+ }
94
+ if (data.escalation !== undefined) {
95
+ // The only escalation today: the risk policy overrode an allow. It
96
+ // must reference an allow verdict and can never settle allowed-once.
97
+ if (data.escalation !== 'risk-policy') {
98
+ fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} has invalid escalation ${JSON.stringify(data.escalation)}`)
99
+ }
100
+ if (data.decision !== 'allow') {
101
+ fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} escalates a non-allow verdict`)
102
+ }
103
+ if (data.outcome === 'allowed-once') {
104
+ fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} escalates yet settles allowed-once`)
105
+ }
106
+ }
107
+ if (!Number.isSafeInteger(data.durationMs) || data.durationMs < 0) {
108
+ fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} has invalid durationMs ${String(data.durationMs)}`)
109
+ }
110
+ const count = askedIds.get(session)?.get(data.approvalId) ?? 0
111
+ if (count < 1) {
112
+ fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} references no prior approval/asked ${JSON.stringify(data.approvalId)}`)
113
+ }
114
+ let verdictsForSession = verdicts.get(session)
115
+ if (verdictsForSession === undefined) {
116
+ verdictsForSession = new Map<string, VerdictRecord>()
117
+ verdicts.set(session, verdictsForSession)
118
+ }
119
+ if (verdictsForSession.has(data.reviewId)) {
120
+ fail(`autoReview/verdict repeats reviewId ${JSON.stringify(data.reviewId)}`)
121
+ }
122
+ const record: VerdictRecord = {
123
+ callId: data.callId,
124
+ decision: data.decision,
125
+ outcome: data.outcome,
126
+ fallback: data.fallback,
127
+ escalation: data.escalation,
128
+ }
129
+ verdictsForSession.set(data.reviewId, record)
130
+ let byApproval = verdictByApproval.get(session)
131
+ if (byApproval === undefined) {
132
+ byApproval = new Map<string, VerdictRecord>()
133
+ verdictByApproval.set(session, byApproval)
134
+ }
135
+ if (byApproval.has(data.approvalId)) {
136
+ fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} is the second verdict for approval/asked ${JSON.stringify(data.approvalId)}`)
137
+ }
138
+ byApproval.set(data.approvalId, record)
139
+ return
140
+ }
141
+ if (event.type === 'autoReview/circuit') {
142
+ const data = event.data
143
+ if (!TRIP_KINDS.includes(data.trip.kind)) {
144
+ fail(`autoReview/circuit ${JSON.stringify(data.circuitId)} has invalid trip kind ${JSON.stringify(data.trip.kind)}`)
145
+ }
146
+ if (!CIRCUIT_ACTIONS.includes(data.action)) {
147
+ fail(`autoReview/circuit ${JSON.stringify(data.circuitId)} has invalid action ${JSON.stringify(data.action)}`)
148
+ }
149
+ if (!Number.isSafeInteger(data.trip.count) || data.trip.count < 1) {
150
+ fail(`autoReview/circuit ${JSON.stringify(data.circuitId)} has invalid trip count ${String(data.trip.count)}`)
151
+ }
152
+ let circuitIds = circuits.get(session)
153
+ if (circuitIds === undefined) {
154
+ circuitIds = new Map<string, string>()
155
+ circuits.set(session, circuitIds)
156
+ }
157
+ if (circuitIds.has(data.circuitId)) {
158
+ fail(`autoReview/circuit repeats circuitId ${JSON.stringify(data.circuitId)}`)
159
+ }
160
+ circuitIds.set(data.circuitId, data.action)
161
+ return
162
+ }
163
+ if (event.type === 'autoReview/override') {
164
+ const verdict = verdicts.get(session)?.get(event.data.reviewId)
165
+ if (verdict === undefined || (verdict.decision !== 'deny' && verdict.escalation !== 'risk-policy')) {
166
+ fail(`autoReview/override references reviewId ${JSON.stringify(event.data.reviewId)} without a prior denial verdict`)
167
+ }
168
+ return
169
+ }
170
+ if (event.type === 'autoReview/rejection') {
171
+ const data = event.data
172
+ if (typeof data.reason !== 'string' || data.reason.length === 0) {
173
+ fail(`autoReview/rejection ${JSON.stringify(data.rejectionId)} has no hard-disable reason`)
174
+ }
175
+ if (data.outcome !== 'rejected') {
176
+ fail(`autoReview/rejection ${JSON.stringify(data.rejectionId)} has invalid outcome ${JSON.stringify(data.outcome)}`)
177
+ }
178
+ if (data.approvalId !== undefined) {
179
+ const count = askedIds.get(session)?.get(data.approvalId) ?? 0
180
+ if (count < 1) {
181
+ fail(`autoReview/rejection ${JSON.stringify(data.rejectionId)} references no prior approval/asked ${JSON.stringify(data.approvalId)}`)
182
+ }
183
+ let byApproval = verdictByApproval.get(session)
184
+ if (byApproval === undefined) {
185
+ byApproval = new Map<string, VerdictRecord>()
186
+ verdictByApproval.set(session, byApproval)
187
+ }
188
+ if (byApproval.has(data.approvalId)) {
189
+ fail(`autoReview/rejection ${JSON.stringify(data.rejectionId)} is the second decision for approval/asked ${JSON.stringify(data.approvalId)}`)
190
+ }
191
+ byApproval.set(data.approvalId, {
192
+ callId: data.callId,
193
+ decision: undefined,
194
+ outcome: data.outcome,
195
+ fallback: undefined,
196
+ escalation: undefined,
197
+ })
198
+ }
199
+ let rejectionIds = rejections.get(session)
200
+ if (rejectionIds === undefined) {
201
+ rejectionIds = new Map<string, RejectionRecord>()
202
+ rejections.set(session, rejectionIds)
203
+ }
204
+ if (rejectionIds.has(data.rejectionId)) {
205
+ fail(`autoReview/rejection repeats rejectionId ${JSON.stringify(data.rejectionId)}`)
206
+ }
207
+ rejectionIds.set(data.rejectionId, { callId: data.callId, outcome: data.outcome })
208
+ return
209
+ }
210
+ if (event.type === 'approval/decided') {
211
+ // The answerer claims a request by appending its verdict BEFORE the
212
+ // service settles the decided pair; a claimed verdict must therefore
213
+ // agree with the recorded outcome (a `delegate` fallback omits its
214
+ // outcome because the downstream answerer owns the decision).
215
+ const claimed = verdictByApproval.get(session)?.get(event.data.id)
216
+ if (claimed?.outcome !== undefined && claimed.outcome !== event.data.outcome) {
217
+ fail(`approval/decided ${JSON.stringify(event.data.id)} outcome ${JSON.stringify(event.data.outcome)} contradicts the autoReview/verdict outcome ${JSON.stringify(claimed.outcome)}`)
218
+ }
219
+ return
220
+ }
221
+ if (event.type === 'tool/result') {
222
+ const block = event.data.message.content[0]
223
+ if (block === undefined || block.type !== 'tool-result') {
224
+ // Not a tool-result projection this invariant owns; skip.
225
+ return
226
+ }
227
+ const text = block.content
228
+ .filter(item => item.type === 'text')
229
+ .map(item => (item as { text: string }).text)
230
+ .join('\n')
231
+ const denyMatch = DENY_MARKER_PATTERN.exec(text)
232
+ if (denyMatch !== null) {
233
+ const reviewId = denyMatch[1]
234
+ if (reviewId === undefined) {
235
+ fail(`tool/result carries an unparseable deny marker: ${JSON.stringify(denyMatch[0])}`)
236
+ return
237
+ }
238
+ const verdict = verdicts.get(session)?.get(reviewId)
239
+ if (verdict === undefined) {
240
+ fail(`tool/result deny marker references unknown reviewId ${JSON.stringify(reviewId)}`)
241
+ return
242
+ }
243
+ if (verdict.decision !== 'deny' && !(verdict.escalation === 'risk-policy' && verdict.outcome === 'rejected')) {
244
+ fail(`tool/result deny marker references a non-deny verdict ${JSON.stringify(reviewId)}`)
245
+ }
246
+ if (verdict.callId !== undefined && verdict.callId !== block.toolCallId) {
247
+ fail(`tool/result deny marker for review ${JSON.stringify(reviewId)} has call id ${JSON.stringify(block.toolCallId)}, expected ${JSON.stringify(verdict.callId)}`)
248
+ }
249
+ return
250
+ }
251
+ const fallbackMatch = FALLBACK_MARKER_PATTERN.exec(text)
252
+ if (fallbackMatch !== null) {
253
+ const reviewId = fallbackMatch[1]
254
+ if (reviewId === undefined) {
255
+ fail(`tool/result carries an unparseable fallback marker: ${JSON.stringify(fallbackMatch[0])}`)
256
+ return
257
+ }
258
+ const verdict = verdicts.get(session)?.get(reviewId)
259
+ if (verdict === undefined) {
260
+ fail(`tool/result fallback marker references unknown reviewId ${JSON.stringify(reviewId)}`)
261
+ return
262
+ }
263
+ if (verdict.fallback === undefined || verdict.outcome !== 'rejected') {
264
+ fail(`tool/result fallback marker references a verdict that was not rejected by fallback ${JSON.stringify(reviewId)}`)
265
+ }
266
+ if (verdict.callId !== undefined && verdict.callId !== block.toolCallId) {
267
+ fail(`tool/result fallback marker for review ${JSON.stringify(reviewId)} has call id ${JSON.stringify(block.toolCallId)}, expected ${JSON.stringify(verdict.callId)}`)
268
+ }
269
+ return
270
+ }
271
+ const circuitMatch = CIRCUIT_MARKER_PATTERN.exec(text)
272
+ if (circuitMatch !== null) {
273
+ const circuitId = circuitMatch[1]
274
+ const toolName = circuitMatch[2]
275
+ if (circuitId === undefined || toolName === undefined) {
276
+ fail(`tool/result carries an unparseable circuit marker: ${JSON.stringify(circuitMatch[0])}`)
277
+ return
278
+ }
279
+ const action = circuits.get(session)?.get(circuitId)
280
+ if (action === undefined) {
281
+ fail(`tool/result circuit marker references unknown circuitId ${JSON.stringify(circuitId)}`)
282
+ return
283
+ }
284
+ if (action === 'delegate') {
285
+ fail(`tool/result circuit marker references a delegate-action circuit ${JSON.stringify(circuitId)}`)
286
+ }
287
+ return
288
+ }
289
+ const neverMatch = NEVER_MARKER_PATTERN.exec(text)
290
+ if (neverMatch === null) return
291
+ const rejectionId = neverMatch[1]
292
+ if (rejectionId === undefined) {
293
+ fail(`tool/result carries an unparseable never marker: ${JSON.stringify(neverMatch[0])}`)
294
+ return
295
+ }
296
+ const rejection = rejections.get(session)?.get(rejectionId)
297
+ if (rejection === undefined) {
298
+ fail(`tool/result never marker references unknown rejectionId ${JSON.stringify(rejectionId)}`)
299
+ return
300
+ }
301
+ if (rejection.outcome !== 'rejected') {
302
+ fail(`tool/result never marker references a rejection that did not settle rejected ${JSON.stringify(rejectionId)}`)
303
+ }
304
+ if (rejection.callId !== undefined && rejection.callId !== block.toolCallId) {
305
+ fail(`tool/result never marker for rejection ${JSON.stringify(rejectionId)} has call id ${JSON.stringify(block.toolCallId)}, expected ${JSON.stringify(rejection.callId)}`)
306
+ }
307
+ }
308
+ }
309
+ for (const session of ctx.sessions.list()) {
310
+ for (const event of session.events) validateEvent(session, event)
311
+ }
312
+ ctx.on('internal/dispatch', (_mode, eventName, args) => {
313
+ if (eventName !== 'session/event') return
314
+ const [session, event] = args as [Session, SessionEvent]
315
+ validateEvent(session, event)
316
+ }, { global: true })
317
+ }, { inject: ['sessions'] })
318
+ /* jscpd:ignore-end */
319
+
320
+ /**
321
+ * Register this package's invariant companion.
322
+ * @param ctx - Cordis context carrying the invariant service.
323
+ * @returns the installed registration's disposer after setup succeeds.
324
+ */
325
+ export const apply = (ctx: Context): Promise<() => void> =>
326
+ Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
@@ -0,0 +1,91 @@
1
+ /**
2
+ * User-facing command strings for `dsh-auto-review` in the supported
3
+ * languages. The table is a pure function of the resolved `language` config;
4
+ * the English strings are the source of truth, the Chinese twin mirrors
5
+ * them. Reviewer-prompt content is NOT here (it is config-provided
6
+ * guidance/policy text, not command UI).
7
+ * @module dsh-auto-review/messages
8
+ */
9
+
10
+ /** Supported UI languages for the `/auto-review` command output. */
11
+ export type UiLanguage = 'en' | 'zh'
12
+
13
+ /** All command strings of one language. */
14
+ export interface Messages {
15
+ readonly usage: string
16
+ readonly description: string
17
+ readonly statusLine: (enabled: boolean) => string
18
+ readonly verdictsLine: (used: number, max: number) => string
19
+ readonly failuresLine: (used: number, max: number) => string
20
+ readonly circuitLine: (kind: string, count: number, action: string) => string
21
+ readonly allTimeLine: (allows: number, denies: number, fallbacks: number, rejections: number, avg: number) => string
22
+ readonly recentLine: (labels: string) => string
23
+ readonly unknownArg: (arg: string) => string
24
+ readonly already: (state: string) => string
25
+ readonly switchedNotice: (enabled: boolean) => string
26
+ readonly switchedResult: (state: string) => string
27
+ readonly approveResult: (toolName: string, reviewId: string, minutes: number) => string
28
+ readonly approveNone: (index: number, count: number) => string
29
+ readonly approveInvalid: (arg: string) => string
30
+ readonly circuitNotice: (kind: string, count: number) => string
31
+ }
32
+
33
+ const EN: Messages = {
34
+ usage: 'Usage: /auto-review on|off|status|approve [n]',
35
+ description: 'enable or disable second-model AI auto-review for this session, inspect verdicts, or approve one retry',
36
+ statusLine: enabled => `Auto-review is ${enabled ? 'ON' : 'OFF'} for this session.`,
37
+ verdictsLine: (used, max) => `AI verdicts this turn: ${used}/${max}`,
38
+ failuresLine: (used, max) => `Reviewer failures this turn: ${used}/${max}`,
39
+ circuitLine: (kind, count, action) => `Rejection circuit breaker tripped (${kind}: ${count} denials); later requests in this turn follow "${action}".`,
40
+ allTimeLine: (allows, denies, fallbacks, rejections, avg) => `All-time: ${allows} allows, ${denies} denies, ${fallbacks} fallbacks, ${rejections} never rejects (avg ${avg} ms per verdict).`,
41
+ recentLine: labels => `Recent verdicts: ${labels}`,
42
+ unknownArg: arg => `Unknown /auto-review argument "${arg}". Usage: /auto-review on|off|status|approve [n]`,
43
+ already: state => `Auto-review is already ${state} for this session.`,
44
+ switchedNotice: enabled => `AI auto-review was switched ${enabled ? 'ON' : 'OFF'} for this session (changed by the user).`,
45
+ switchedResult: state => `Auto-review ${state} for this session.`,
46
+ approveResult: (toolName, reviewId, minutes) => [
47
+ `Authorized ONE retry of the denied ${toolName} action (review ${reviewId}).`,
48
+ `The next ${toolName} review within ${minutes} minutes carries this authorization as reviewer context — the reviewer still decides.`,
49
+ ].join('\n'),
50
+ approveNone: (index, count) => `No recent denial #${index} in this session (${count} recorded).`,
51
+ approveInvalid: arg => `Invalid /auto-review approve index "${arg}". Usage: /auto-review approve [n]`,
52
+ circuitNotice: (kind, count) => `The AI auto-review rejection circuit breaker tripped (${kind}: ${count} denials). `
53
+ + 'This turn is aborted because the agent kept proposing blocked actions. '
54
+ + 'Adjust the sandbox boundary or the review policy, then continue.',
55
+ }
56
+
57
+ const ZH: Messages = {
58
+ usage: '用法:/auto-review on|off|status|approve [n]',
59
+ description: '为本会话启用或禁用第二模型 AI 自动审查、查看裁决统计,或批准一次重试',
60
+ statusLine: enabled => `本会话的自动审查已${enabled ? '开启' : '关闭'}。`,
61
+ verdictsLine: (used, max) => `本回合 AI 裁决数:${used}/${max}`,
62
+ failuresLine: (used, max) => `本回合审查失败数:${used}/${max}`,
63
+ circuitLine: (kind, count, action) => `拒绝熔断器已触发(${kind}:${count} 次拒绝);本回合后续请求按 "${action}" 处理。`,
64
+ allTimeLine: (allows, denies, fallbacks, rejections, avg) => `累计:${allows} 次放行,${denies} 次拒绝,${fallbacks} 次失败,${rejections} 次硬禁用拒绝(每次裁决平均 ${avg} ms)。`,
65
+ recentLine: labels => `最近裁决:${labels}`,
66
+ unknownArg: arg => `未知的 /auto-review 参数 "${arg}"。用法:/auto-review on|off|status|approve [n]`,
67
+ already: state => `本会话的自动审查已处于 ${state} 状态。`,
68
+ switchedNotice: enabled => `AI 自动审查已为本会话${enabled ? '开启' : '关闭'}(由用户更改)。`,
69
+ switchedResult: state => `本会话的自动审查已${state}。`,
70
+ approveResult: (toolName, reviewId, minutes) => [
71
+ `已批准对 ${toolName} 被拒操作的一次重试(审查 ${reviewId})。`,
72
+ `接下来 ${minutes} 分钟内对 ${toolName} 的下一次审查将携带该授权作为审查上下文——最终仍由审查代理裁决。`,
73
+ ].join('\n'),
74
+ approveNone: (index, count) => `本会话没有第 ${index} 条近期拒绝记录(共 ${count} 条)。`,
75
+ approveInvalid: arg => `无效的 /auto-review approve 序号 "${arg}"。用法:/auto-review approve [n]`,
76
+ circuitNotice: (kind, count) => `AI 自动审查拒绝熔断器触发(${kind}:${count} 次拒绝)。`
77
+ + '由于代理持续提出被阻止的操作,本回合已中止。'
78
+ + '请调整沙箱边界或审查策略后继续。',
79
+ }
80
+
81
+ /** The message tables, keyed by {@link UiLanguage}. */
82
+ export const MESSAGES: Readonly<Record<UiLanguage, Messages>> = { en: EN, zh: ZH }
83
+
84
+ /**
85
+ * Pick one language's message table.
86
+ * @param language - the resolved UI language.
87
+ * @returns the table (English and Chinese are the shipped languages).
88
+ */
89
+ export function messages(language: UiLanguage): Messages {
90
+ return MESSAGES[language]
91
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Pure-type outlet of the `autoReview` session projection: the wire value
3
+ * the host folds and the browser review panel renders. Zero value imports —
4
+ * client programs pull these types without dragging the host chain.
5
+ * @module dsh-auto-review/projection-types
6
+ */
7
+
8
+ /** One verdict, presented for the panel. */
9
+ export interface AutoReviewVerdictView {
10
+ readonly reviewId: string
11
+ readonly toolName: string
12
+ readonly at: number
13
+ readonly durationMs: number
14
+ readonly decision?: 'allow' | 'deny'
15
+ readonly riskLevel?: 'low' | 'medium' | 'high'
16
+ readonly escalation?: 'risk-policy'
17
+ readonly fallback?: string
18
+ readonly outcome?: string
19
+ readonly reason?: string
20
+ }
21
+
22
+ /** One recent denial offered for a one-shot approve (newest first). */
23
+ export interface AutoReviewDenyView {
24
+ readonly reviewId: string
25
+ readonly toolName: string
26
+ readonly reason?: string
27
+ }
28
+
29
+ /** The whole wire value folded by the `autoReview` projection unit. */
30
+ export interface AutoReviewProjection {
31
+ /** The session's switch — the last `autoReview/state` event, or the `enableByDefault` default. */
32
+ readonly enabled: boolean
33
+ /** Real AI verdicts in the current open turn. */
34
+ readonly verdictsUsed: number
35
+ /** Reviewer failures in the current open turn. */
36
+ readonly failuresUsed: number
37
+ /** Cumulative counts over the whole session. */
38
+ readonly allows: number
39
+ readonly denies: number
40
+ readonly fallbacks: number
41
+ /** Hard-disable (`never` policy) rejections — no reviewer ran for these. */
42
+ readonly neverRejects: number
43
+ /** Mean duration of decision-carrying verdicts, rounded; 0 without any. */
44
+ readonly avgDurationMs: number
45
+ /** The current turn's circuit trip, or null. */
46
+ readonly circuit: {
47
+ readonly action: 'delegate' | 'reject' | 'abort-turn'
48
+ readonly trip: { readonly kind: 'consecutive' | 'window'; readonly count: number }
49
+ readonly toolName: string
50
+ } | null
51
+ /** The last 8 verdicts, newest first. */
52
+ readonly recent: readonly AutoReviewVerdictView[]
53
+ /** The last 5 denials, newest first (approve candidates; index 0 = approve 1). */
54
+ readonly recentDenies: readonly AutoReviewDenyView[]
55
+ }
56
+
57
+ declare module '@deepseek-ai/dsh-session-projection/types' {
58
+ interface SessionProjectionMap {
59
+ autoReview: AutoReviewProjection
60
+ }
61
+ }