dsh-my-observability 0.1.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/lib/store.js ADDED
@@ -0,0 +1,231 @@
1
+ /**
2
+ * dsh-my-observability — audit event store.
3
+ *
4
+ * 事件审计日志的内存态 + 持久化:
5
+ * - 按会话隔离(bySession 分桶),查询/追加都限定在单个会话内;
6
+ * - 每会话事件上限(MAX_EVENTS_PER_SESSION,FIFO 淘汰),防无限膨胀;
7
+ * - 持久化 $DSH_HOME/observability/audit.json(防抖 500ms + 原子写
8
+ * tmp+rename + teardown flush),启动时异步加载(加载完成前的事件
9
+ * 缓冲在 pending,加载后回放),重启后完整恢复;
10
+ * - 全局事件上限(MAX_TOTAL_EVENTS),超限按会话轮转淘汰最旧会话事件。
11
+ */
12
+ import { rename, writeFile, readFile, mkdir } from 'node:fs/promises'
13
+ import { homedir } from 'node:os'
14
+ import { join, dirname } from 'node:path'
15
+
16
+ export const MAX_EVENTS_PER_SESSION = 2000
17
+ export const MAX_TOTAL_EVENTS = 20000
18
+
19
+ /** 审计数据文件:$DSH_HOME/observability/audit.json(fallback ~/.dsh/…)。 */
20
+ export function stateFile() {
21
+ const home = process.env.DSH_HOME
22
+ const base = typeof home === 'string' && home !== '' ? home : homedir()
23
+ return join(base, 'observability', 'audit.json')
24
+ }
25
+
26
+ /** 初始空状态。 */
27
+ export function createState() {
28
+ return { version: 1, bySession: {} }
29
+ }
30
+
31
+ /**
32
+ * 创建审计存储:{ state, record, events, sessions, count, dispose }。
33
+ * record 在状态加载完成前缓冲(不丢事件);dispose 冲刷未落盘数据。
34
+ */
35
+ export function createStore(ctx) {
36
+ const store = { state: createState() }
37
+ const handle = {
38
+ ctx,
39
+ file: stateFile(),
40
+ store,
41
+ pending: [],
42
+ ready: false,
43
+ persistTimer: null,
44
+ dirtyChain: Promise.resolve(),
45
+ }
46
+ store.record = (event) => record(handle, event)
47
+ store.events = (sessionId, type, limit) => eventsOf(handle, sessionId, type, limit)
48
+ store.sessions = () => sessionsOf(handle)
49
+ store.count = () => countOf(handle)
50
+ store.dispose = () => dispose(handle)
51
+ void readFile(handle.file, 'utf8')
52
+ .then((text) => onLoaded(handle, text))
53
+ .catch(() => onLoaded(handle, ''))
54
+ return store
55
+ }
56
+
57
+ /** 追加一条审计事件(自动分配 id/时间戳);未就绪时缓冲。 */
58
+ function record(handle, event) {
59
+ const item = { id: nextId(handle), time: Date.now(), ...event }
60
+ if (!handle.ready) {
61
+ handle.pending.push(item)
62
+ return item
63
+ }
64
+ appendEvent(handle, item)
65
+ persistSoon(handle)
66
+ return item
67
+ }
68
+
69
+ /** 查询某会话的事件(正序时间轴;type 可选过滤;limit 限制条数)。 */
70
+ function eventsOf(handle, sessionId, type, limit) {
71
+ const bucket = handle.store.state.bySession[sessionId]?.events
72
+ if (!Array.isArray(bucket)) return []
73
+ let list = bucket
74
+ if (type !== undefined && type !== null && type !== '') {
75
+ list = list.filter((event) => event.type === type)
76
+ }
77
+ const capped = typeof limit === 'number' && limit > 0 ? list.slice(-limit) : list
78
+ return capped.map((event) => ({ ...event }))
79
+ }
80
+
81
+ /** 有审计事件的会话列表(按最后活动时间倒序,含事件数)。 */
82
+ function sessionsOf(handle) {
83
+ const entries = Object.entries(handle.store.state.bySession)
84
+ const list = entries
85
+ .map(([sessionId, bucket]) => ({
86
+ sessionId,
87
+ count: bucket.events.length,
88
+ lastTime: bucket.events.length > 0 ? bucket.events[bucket.events.length - 1].time : 0,
89
+ }))
90
+ .filter((entry) => entry.count > 0)
91
+ list.sort((a, b) => b.lastTime - a.lastTime)
92
+ return list
93
+ }
94
+
95
+ /** 全部会话事件总数(供状态展示/测试断言)。 */
96
+ function countOf(handle) {
97
+ let total = 0
98
+ for (const bucket of Object.values(handle.store.state.bySession)) total += bucket.events.length
99
+ return total
100
+ }
101
+
102
+ /** 事件自增 id(跨会话单调)。 */
103
+ function nextId(handle) {
104
+ handle.seq = (handle.seq ?? 0) + 1
105
+ return handle.seq
106
+ }
107
+
108
+ /** 追加事件到会话桶:FIFO 淘汰 + 全局上限轮转淘汰。 */
109
+ function appendEvent(handle, event) {
110
+ const state = handle.store.state
111
+ const bucket = state.bySession[event.sessionId] ?? (state.bySession[event.sessionId] = { events: [] })
112
+ bucket.events.push(event)
113
+ if (bucket.events.length > MAX_EVENTS_PER_SESSION) {
114
+ bucket.events.splice(0, bucket.events.length - MAX_EVENTS_PER_SESSION)
115
+ }
116
+ if (countOf(handle) > MAX_TOTAL_EVENTS) evictOldest(handle)
117
+ }
118
+
119
+ /** 全局超限:从最早活动的会话整桶淘汰,直到回到上限内。 */
120
+ function evictOldest(handle) {
121
+ const state = handle.store.state
122
+ const sessions = Object.entries(state.bySession)
123
+ .filter(([, bucket]) => bucket.events.length > 0)
124
+ .sort((a, b) => firstTimeOf(a[1]) - firstTimeOf(b[1]))
125
+ for (const [sessionId, bucket] of sessions) {
126
+ if (countOf(handle) <= MAX_TOTAL_EVENTS) break
127
+ delete state.bySession[sessionId]
128
+ void bucket
129
+ }
130
+ }
131
+
132
+ function firstTimeOf(bucket) {
133
+ return bucket.events.length > 0 ? bucket.events[0].time : 0
134
+ }
135
+
136
+ /** 状态加载完成:解析/规整 + 合并本进程已产生的事件 + 回放缓冲 + 落盘。 */
137
+ function onLoaded(handle, text) {
138
+ const parsed = parseLoaded(text)
139
+ if (parsed !== undefined) {
140
+ mergeCurrent(handle.store.state, parsed)
141
+ handle.store.state = parsed
142
+ }
143
+ handle.ready = true
144
+ const pending = handle.pending.splice(0)
145
+ for (const item of pending) appendEvent(handle, item)
146
+ if (pending.length > 0 || parsed !== undefined) persistSoon(handle)
147
+ }
148
+
149
+ /** 把当前 state 中已产生的事件合并进磁盘状态(防 dispose 回放后覆盖丢失)。 */
150
+ function mergeCurrent(current, parsed) {
151
+ for (const [sessionId, bucket] of Object.entries(current.bySession)) {
152
+ if (!Array.isArray(bucket.events) || bucket.events.length === 0) continue
153
+ const target = parsed.bySession[sessionId] ?? (parsed.bySession[sessionId] = { events: [] })
154
+ target.events.push(...bucket.events)
155
+ }
156
+ }
157
+
158
+ /** 解析已持久化的状态(结构不合法时回退空状态)。 */
159
+ function parseLoaded(text) {
160
+ if (text === undefined || text === null || text === '') return undefined
161
+ try {
162
+ const parsed = JSON.parse(text)
163
+ if (!isValidRoot(parsed)) return undefined
164
+ const state = createState()
165
+ for (const [sessionId, bucket] of Object.entries(parsed.bySession)) {
166
+ const events = validEventsOf(bucket)
167
+ if (events.length > 0) state.bySession[sessionId] = { events }
168
+ }
169
+ return state
170
+ } catch {
171
+ return undefined
172
+ }
173
+ }
174
+
175
+ /** 持久化根结构校验(bySession 必须为对象)。 */
176
+ function isValidRoot(parsed) {
177
+ return parsed !== null && typeof parsed === 'object' && typeof parsed.bySession === 'object'
178
+ }
179
+
180
+ /** 会话桶事件规整:过滤非法事件 + 截断到每会话上限。 */
181
+ function validEventsOf(bucket) {
182
+ return Array.isArray(bucket?.events)
183
+ ? bucket.events.filter(isValidEvent).slice(-MAX_EVENTS_PER_SESSION)
184
+ : []
185
+ }
186
+
187
+ /** 事件结构校验(时间/会话/类型为字符串与数字的合理形态)。 */
188
+ function isValidEvent(event) {
189
+ return event !== null && typeof event === 'object'
190
+ && typeof event.time === 'number'
191
+ && typeof event.sessionId === 'string'
192
+ && typeof event.type === 'string'
193
+ }
194
+
195
+ /** 原子写当前状态(经 dirtyChain 串行化;自动建目录)。 */
196
+ function persistNow(handle) {
197
+ const snapshot = JSON.stringify(handle.store.state)
198
+ const tmp = `${handle.file}.tmp-${process.pid}`
199
+ handle.dirtyChain = handle.dirtyChain.then(async () => {
200
+ try {
201
+ await mkdir(dirname(handle.file), { recursive: true })
202
+ await writeFile(tmp, snapshot, 'utf8')
203
+ await rename(tmp, handle.file)
204
+ } catch (error) {
205
+ handle.ctx.logger?.warn(`[dsh-my-observability] persist failed: ${error instanceof Error ? error.message : String(error)}`)
206
+ }
207
+ }).catch(() => {})
208
+ }
209
+
210
+ /** 防抖(500ms)调度持久化。 */
211
+ function persistSoon(handle) {
212
+ if (handle.persistTimer !== null) return
213
+ handle.persistTimer = setTimeout(() => {
214
+ handle.persistTimer = null
215
+ persistNow(handle)
216
+ }, 500)
217
+ }
218
+
219
+ /** 卸载冲刷:清定时器 + 回放未就绪缓冲 + 立即落盘。 */
220
+ function dispose(handle) {
221
+ if (handle.persistTimer !== null) {
222
+ clearTimeout(handle.persistTimer)
223
+ handle.persistTimer = null
224
+ }
225
+ if (!handle.ready) {
226
+ const pending = handle.pending.splice(0)
227
+ for (const item of pending) appendEvent(handle, item)
228
+ }
229
+ persistNow(handle)
230
+ void handle.dirtyChain
231
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "dsh-my-observability",
3
+ "version": "0.1.0",
4
+ "description": "DSH 可观测性 + Git 工程工具插件:会话轨迹回放(时间轴)、事件审计(agent 行为记录)、结构化 Git 提交(Conventional Commits)、增量 diff 审查(提交前审查)。DSH web plugin: trajectory replay timeline, event audit log, structured git commits, pre-commit incremental diff review.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/baosfeng/my-dsh-plugins"
10
+ },
11
+ "engines": {
12
+ "node": ">=22"
13
+ },
14
+ "scripts": {
15
+ "build": "node scripts/build.mjs",
16
+ "test": "npx vitest run --coverage && cucumber-js test/features --import 'test/features/steps/*.mjs'"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "default": "./lib/index.js"
21
+ },
22
+ "./client": {
23
+ "default": "./lib/client.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "lib",
29
+ "cordis.patch.yml",
30
+ "README.md",
31
+ "CHANGELOG.md",
32
+ "LICENSE",
33
+ "assets"
34
+ ],
35
+ "dsh": {
36
+ "bundle": {
37
+ "patch": "./cordis.patch.yml"
38
+ },
39
+ "client": {
40
+ "platform": "web",
41
+ "inject": [
42
+ "@deepseek-ai/dsh-client-runtime"
43
+ ]
44
+ }
45
+ },
46
+ "peerDependencies": {
47
+ "cordis": "^4.0.0-rc.8",
48
+ "react": "^18.2.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "cordis": {
52
+ "optional": true
53
+ },
54
+ "react": {
55
+ "optional": true
56
+ }
57
+ },
58
+ "license": "MIT"
59
+ }