dsh-my-observability 0.3.0 → 0.3.2

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,45 @@
1
+ /**
2
+ * 原生侧边栏页签注册助手(issue #187 批 1;scripts/build.mjs 拼接片段)。
3
+ *
4
+ * 原生结构:每个面板一个**页签类型**(sidebarRightTabs.register)+ 两个
5
+ * **keyed 席位**(slots.register,body 与 title)。id 全局唯一且是席位的
6
+ * key,一个 key 只能注册一个席位 —— 本插件有两个独立面板,因此每个 kind 用
7
+ * 自己的 id(沿用迁移前 better-sidebar 的 tab id)。
8
+ *
9
+ * 片段无 import/export,共享 client.src.js 的 factory 作用域:本函数的参数
10
+ * 全部由调用方(apply)注入,便于单测与阅读。
11
+ */
12
+ function registerNativeTab(ctx, tabs, slots, spec) {
13
+ ctx.effect(
14
+ () =>
15
+ tabs.register({
16
+ id: spec.id,
17
+ kind: spec.id,
18
+ title: spec.title,
19
+ guide: [{ order: spec.order, title: spec.title }],
20
+ }),
21
+ 'dsh-my-observability: ' + spec.label + ' tab',
22
+ )
23
+ ctx.effect(
24
+ () =>
25
+ slots.inject('sidebar.right.pane.tab', () =>
26
+ slots.register({ name: 'sidebar.right.pane.tab', key: spec.id }, spec.Body),
27
+ ),
28
+ 'dsh-my-observability: ' + spec.label + ' tab body',
29
+ )
30
+ ctx.effect(
31
+ () =>
32
+ slots.inject('sidebar.right.pane.tab.title', () =>
33
+ slots.register({ name: 'sidebar.right.pane.tab.title', key: spec.id }, spec.Title),
34
+ ),
35
+ 'dsh-my-observability: ' + spec.label + ' tab title',
36
+ )
37
+ }
38
+
39
+ /** 原生 tab title 席位:宿主给定标题优先,否则回退面板自带文案。 */
40
+ function nativeTabTitle(fallback) {
41
+ return function TabTitle(props) {
42
+ const info = typeof props.useTabInfo === 'function' ? props.useTabInfo() : undefined
43
+ return createElement('span', null, info?.tab?.title ?? fallback())
44
+ }
45
+ }
@@ -1,149 +1,97 @@
1
1
  /**
2
- * dsh-my-observability — 资源采样监控 + 降级看门狗。
2
+ * dsh-my-observability — 资源采样监控 + 降级看门狗(**dsh-shared 原语的消费方**)。
3
3
  *
4
- * 每 intervalMs(默认 15s)采样本进程 CPU/内存与审计文件大小/写入速率,
5
- * 保留最近 MAX_HISTORY 个样本(ring buffer),并做阈值评估(resource-rules)。
6
- * 用途:让「写放大/资源超限」在运行期当小时可见(9/2 事故复盘结论:
7
- * 15 小时 300GB 写入零监控是事故未被及时发现的主因)。
4
+ * 第三批(issue #198):本文件原本是插件私有的看门狗实现(采样 → 阈值判定 →
5
+ * 连续确认降级/恢复),现已改为 `dsh-shared` 的 `createResourceGuard` 消费方——
6
+ * 抽出来的原语必须有人真的用,否则等于没抽。
8
7
  *
9
- * 降级看门狗(issue #127 资源占用防护):关键阈值(写放大/文件字节)连续
10
- * DEGRADE_CONFIRM_COUNT 次超限 → 触发 onDegrade 回调(宿主降级:停落盘等);
11
- * 连续 RECOVER_CONFIRM_COUNT 次正常 → 触发 onRecover 回调(宿主恢复 + 全量快照)。
12
- * 判定为纯函数(resource-rules.shouldEnterDegrade/shouldExitDegrade),可单测。
8
+ * 保留的插件职责(宿主特有,不进 shared):
9
+ * - **采样源**:`createProcessSampler`(CPU/RSS/审计文件字节/写入速率)
10
+ * + `$DSH_HOME` 目录总字节这个插件特有维度;
11
+ * - **降级动作**:由 index.ts 的 onDegrade/onRecover 决定(停落盘 / 恢复 + 全量快照)。
13
12
  *
14
- * 采样自身开销:15s 一次 process.cpuUsage/memoryUsage + fs.stat(<0.01% CPU、
15
- * 零分配大对象),远低于「监控不能放大被监控对象」的护栏(resource-budget-review)。
13
+ * 行为等价(改造前后逐条对齐,见 test/host-resource-guard.mjs):
14
+ * - 采样间隔默认 15s;历史 ring buffer 60 样本;
15
+ * - 首个样本无窗口 → 不告警、不进历史,仅作后续窗口的 prev;
16
+ * - 关键阈值只有 write-rate / file-size;CPU/内存超限只告警不降级;
17
+ * - 连续 3 次超限 → 降级;连续 3 次正常 → 恢复;降级中不重复触发回调。
18
+ *
19
+ * 资源开销:采样 15s 一次、<0.01% CPU;history 固定 60 条(不随运行时长增长)。
20
+ * 已知存量代价:`homeBytes` 维度每次采样递归扫 $DSH_HOME(O(文件数)),
21
+ * 新接入方不要复制这个维度(见 shared README 的采样边界)。
16
22
  */
17
- import { statSync, readdirSync } from 'node:fs'
18
- import { join } from 'node:path'
19
- import { homedir } from 'node:os'
20
- import { evaluateResourceAlerts, shouldEnterDegrade, shouldExitDegrade, DEFAULT_LIMITS } from './resource-rules.js'
21
- import { jsonlFile } from './store-persist.js'
22
-
23
- const DEFAULT_INTERVAL_MS = 15000
24
- const MAX_HISTORY = 60
25
-
26
- /** 创建资源监控器:{ sample, start, stop }。options: intervalMs / limits / onDegrade / onRecover。 */
23
+ import { readdirSync, statSync } from 'node:fs';
24
+ import { join } from 'node:path';
25
+ import { homedir } from 'node:os';
26
+ import { createProcessSampler, createResourceGuard } from 'dsh-shared';
27
+ import { jsonlFile } from './store-persist.js';
28
+ const DEFAULT_INTERVAL_MS = 15000;
29
+ /** 创建资源监控器:{ sample, start, stop, isDegraded }(薄适配 dsh-shared 看门狗)。 */
27
30
  export function createResourceMonitor(ctx, options = {}) {
28
- const intervalMs =
29
- Number.isFinite(options.intervalMs) && options.intervalMs > 0 ? options.intervalMs : DEFAULT_INTERVAL_MS
30
- const limits = { ...DEFAULT_LIMITS, ...(options.limits ?? {}) }
31
- const onDegrade = typeof options.onDegrade === 'function' ? options.onDegrade : null
32
- const onRecover = typeof options.onRecover === 'function' ? options.onRecover : null
33
- const state = {
34
- timer: null,
35
- file: jsonlFile(),
36
- dshHome: process.env.DSH_HOME || join(homedir(), '.dsh'),
37
- lastSample: null,
38
- lastCpu: process.cpuUsage(),
39
- history: [],
40
- degraded: false,
41
- }
42
- const monitor = {
43
- sample: () => sample(state, limits, onDegrade, onRecover),
44
- start: () => startMonitor(state, intervalMs, monitor),
45
- stop: () => stopMonitor(state),
46
- isDegraded: () => state.degraded,
47
- }
48
- return monitor
49
- }
50
-
51
- /** 采样一次:CPU 使用率(窗口内 user+sys)/RSS/审计文件字节/写入速率 + 告警 + 降级判定。 */
52
- function sample(state, limits, onDegrade, onRecover) {
53
- const now = Date.now()
54
- const cpu = process.cpuUsage()
55
- const cpuDelta = cpu.user - state.lastCpu.user + (cpu.system - state.lastCpu.system) // µs
56
- state.lastCpu = cpu
57
- const mem = process.memoryUsage()
58
- const memoryBytes = mem.rss
59
- let fileBytes = 0
60
- try {
61
- fileBytes = statSync(state.file).size
62
- } catch {
63
- // 审计文件尚未创建:字节为 0
64
- }
65
- let homeBytes = 0
66
- try {
67
- homeBytes = dshHomeSize(state.dshHome)
68
- } catch {
69
- // $DSH_HOME 不可达
70
- }
71
- const prev = state.lastSample
72
- if (prev !== null) {
73
- const deltaMs = Math.max(now - prev.time, 1)
74
- // CPU 单核折算:cpuDelta(µs) / deltaMs(ms) / 1000 → 百分比(×100)
75
- const cpuPercent = (cpuDelta / 1000 / deltaMs) * 100
76
- const byteDelta = fileBytes - prev.fileBytes
77
- const writeRateBytesPerHour = byteDelta > 0 ? (byteDelta / deltaMs) * 3600 * 1000 : 0
78
- const sample = { time: now, cpuPercent, memoryBytes, fileBytes, writeRateBytesPerHour, homeBytes }
79
- state.history.push(sample)
80
- if (state.history.length > MAX_HISTORY) state.history.splice(0, state.history.length - MAX_HISTORY)
81
- state.lastSample = sample
82
- updateDegradeState(state, limits, onDegrade, onRecover)
31
+ const home = process.env.DSH_HOME || join(homedir(), '.dsh');
32
+ const collect = options.collect ?? defaultSampler(home);
33
+ // 配置容错保持与改造前一致:非法 intervalMs 回退默认(而不是 fail-fast 让插件起不来)
34
+ const intervalMs = Number.isFinite(options.intervalMs) && options.intervalMs > 0
35
+ ? options.intervalMs
36
+ : DEFAULT_INTERVAL_MS;
37
+ const guard = createResourceGuard({
38
+ collect,
39
+ limits: options.limits,
40
+ intervalMs,
41
+ historySize: options.historySize,
42
+ enterConfirmCount: options.enterConfirmCount,
43
+ exitConfirmCount: options.exitConfirmCount,
44
+ now: options.now,
45
+ onDegrade: (snapshot) => options.onDegrade?.(snapshot),
46
+ onRecover: (snapshot) => options.onRecover?.(snapshot),
47
+ logger: ctxLogger(ctx),
48
+ prefix: '[dsh-my-observability]',
49
+ });
83
50
  return {
84
- ...sample,
85
- history: [...state.history],
86
- alerts: evaluateResourceAlerts(sample, limits),
87
- degraded: state.degraded,
88
- }
89
- }
90
- state.lastSample = { time: now, fileBytes, memoryBytes, cpuPercent: 0, writeRateBytesPerHour: 0, homeBytes }
91
- return { ...state.lastSample, history: [...state.history], alerts: [], degraded: state.degraded }
92
- }
93
-
94
- /**
95
- * 降级状态机:未降级且连续超限 → 进入降级(回调);已降级且连续正常 → 退出降级(回调)。
96
- * 判定纯函数见 resource-rules.js;本函数只持有状态并触发宿主回调。
97
- */
98
- function updateDegradeState(state, limits, onDegrade, onRecover) {
99
- if (!state.degraded) {
100
- if (shouldEnterDegrade(state.history, limits)) {
101
- state.degraded = true
102
- onDegrade?.()
103
- }
104
- } else if (shouldExitDegrade(state.history, limits)) {
105
- state.degraded = false
106
- onRecover?.()
107
- }
51
+ sample: () => guard.sample(),
52
+ start: () => guard.start(),
53
+ stop: () => guard.stop(),
54
+ isDegraded: () => guard.isDegraded(),
55
+ history: () => guard.history(),
56
+ stats: () => guard.stats(),
57
+ guard: () => guard,
58
+ };
108
59
  }
109
-
110
- /** 启动周期采样(幂等)。 */
111
- function startMonitor(state, intervalMs, monitor) {
112
- if (state.timer === null) {
113
- state.timer = setInterval(() => {
114
- void monitor.sample()
115
- }, intervalMs)
116
- if (state.timer.unref) state.timer.unref()
117
- }
118
- return state.timer
60
+ /** 取宿主的 logger(回调异常 warn 用);不可用则返回 undefined(看门狗不因此失效)。 */
61
+ function ctxLogger(ctx) {
62
+ const logger = ctx?.logger;
63
+ if (typeof logger?.warn !== 'function')
64
+ return undefined;
65
+ return { warn: (message) => logger.warn?.(message) };
119
66
  }
120
-
121
- /** 停止采样(幂等)。 */
122
- function stopMonitor(state) {
123
- if (state.timer !== null) {
124
- clearInterval(state.timer)
125
- state.timer = null
126
- }
67
+ /** 默认采样源:进程维度 + 审计文件字节/写入速率 + $DSH_HOME 总字节。 */
68
+ function defaultSampler(home) {
69
+ return createProcessSampler({
70
+ file: jsonlFile(),
71
+ extra: () => ({ homeBytes: dshHomeSize(home) }),
72
+ });
127
73
  }
128
-
129
74
  /** 递归计算目录总字节(best-effort,跳过不可达文件)。 */
130
75
  function dshHomeSize(dir) {
131
- let total = 0
132
- try {
133
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
134
- const full = join(dir, entry.name)
135
- if (entry.isDirectory()) {
136
- total += dshHomeSize(full)
137
- } else if (entry.isFile()) {
138
- try {
139
- total += statSync(full).size
140
- } catch {
141
- // 文件不可达
76
+ let total = 0;
77
+ try {
78
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
79
+ const full = join(dir, entry.name);
80
+ if (entry.isDirectory()) {
81
+ total += dshHomeSize(full);
82
+ }
83
+ else if (entry.isFile()) {
84
+ try {
85
+ total += statSync(full).size;
86
+ }
87
+ catch {
88
+ // 文件不可达
89
+ }
90
+ }
142
91
  }
143
- }
144
92
  }
145
- } catch {
146
- // 目录不可达
147
- }
148
- return total
93
+ catch {
94
+ // 目录不可达
95
+ }
96
+ return total;
149
97
  }
@@ -1,102 +1,12 @@
1
1
  /**
2
- * dsh-my-observability — 资源阈值判定(纯函数)。
2
+ * dsh-my-observability — 资源阈值判定(**dsh-shared 原语的消费方**,issue #198 第三批)。
3
3
  *
4
- * 阈值口径与 skill `resource-budget-review` 一致(设计/开发/运行三遍校验
5
- * 的运行期部分):写放大/CPU/内存超限在采样数据上提前暴露,告警可查询。
6
- * 纯函数便于单测与配置覆盖(config.resourceLimits 浅合并)。
7
- */
8
-
9
- /** 默认阈值(DSH 插件场景,来源 resource-budget-review 五维表)。 */
10
- export const DEFAULT_LIMITS = {
11
- /** 审计文件写入速率上限:50 MB/小时。 */
12
- writeRateBytesPerHour: 50 * 1024 * 1024,
13
- /** 审计文件大小上限:50 MB(循环 compact 后应远小于此)。 */
14
- fileBytes: 50 * 1024 * 1024,
15
- /** 本进程 CPU 均值上限:10%(单核折算,采样窗口内 user+sys / 时长)。 */
16
- cpuPercent: 10,
17
- /** 本进程 RSS 上限:500 MB。 */
18
- memoryBytes: 500 * 1024 * 1024,
19
- }
20
-
21
- /** 采样数据 → 告警列表([{ rule, level, message, value, limit }])。 */
22
- export function evaluateResourceAlerts(sample, limits = DEFAULT_LIMITS) {
23
- const alerts = []
24
- if (typeof sample.writeRateBytesPerHour === 'number' && sample.writeRateBytesPerHour > limits.writeRateBytesPerHour) {
25
- alerts.push({
26
- rule: 'write-rate',
27
- level: 'error',
28
- message: `审计写入速率 ${fmtMB(sample.writeRateBytesPerHour)}/h 超过上限 ${fmtMB(limits.writeRateBytesPerHour)}/h(写放大风险)`,
29
- value: sample.writeRateBytesPerHour,
30
- limit: limits.writeRateBytesPerHour,
31
- })
32
- }
33
- if (typeof sample.fileBytes === 'number' && sample.fileBytes > limits.fileBytes) {
34
- alerts.push({
35
- rule: 'file-size',
36
- level: 'error',
37
- message: `审计文件 ${fmtMB(sample.fileBytes)} 超过上限 ${fmtMB(limits.fileBytes)}(请检查 compact/轮转)`,
38
- value: sample.fileBytes,
39
- limit: limits.fileBytes,
40
- })
41
- }
42
- if (typeof sample.cpuPercent === 'number' && sample.cpuPercent > limits.cpuPercent) {
43
- alerts.push({
44
- rule: 'cpu',
45
- level: 'warn',
46
- message: `本进程 CPU ${Math.round(sample.cpuPercent)}% 超过上限 ${limits.cpuPercent}%`,
47
- value: sample.cpuPercent,
48
- limit: limits.cpuPercent,
49
- })
50
- }
51
- if (typeof sample.memoryBytes === 'number' && sample.memoryBytes > limits.memoryBytes) {
52
- alerts.push({
53
- rule: 'memory',
54
- level: 'warn',
55
- message: `本进程内存 ${fmtMB(sample.memoryBytes)} 超过上限 ${fmtMB(limits.memoryBytes)}`,
56
- value: sample.memoryBytes,
57
- limit: limits.memoryBytes,
58
- })
59
- }
60
- return alerts
61
- }
62
-
63
- /** 连续多少次采样超限(关键阈值)判定进入降级(模块内默认值)。 */
64
- const DEGRADE_CONFIRM_COUNT = 3
65
-
66
- /** 连续多少次采样正常判定退出降级(模块内默认值)。 */
67
- const RECOVER_CONFIRM_COUNT = 3
68
-
69
- /**
70
- * 关键阈值判定:磁盘写放大风险主导(write-rate / file-size 任一超限即记超限)。
71
- * CPU/内存超限告警但不触发落盘降级(避免误伤正常大请求峰值)。
72
- */
73
- function isCriticalOverLimit(sample, limits = DEFAULT_LIMITS) {
74
- return (
75
- (sample.writeRateBytesPerHour ?? 0) > limits.writeRateBytesPerHour || (sample.fileBytes ?? 0) > limits.fileBytes
76
- )
77
- }
78
-
79
- /**
80
- * 降级判定(纯函数):最近 confirmCount 个样本**全部**关键阈值超限 → 进入降级。
81
- * 历史样本不足 confirmCount 时不降级(冷启动保护)。
82
- * 返回布尔(shouldEnterDegrade)。
83
- */
84
- export function shouldEnterDegrade(history, limits = DEFAULT_LIMITS, confirmCount = DEGRADE_CONFIRM_COUNT) {
85
- const recent = history.slice(-confirmCount)
86
- if (recent.length < confirmCount) return false
87
- return recent.every((sample) => isCriticalOverLimit(sample, limits))
88
- }
89
-
90
- /**
91
- * 恢复判定(纯函数):最近 confirmCount 个样本**全部**关键阈值正常 → 退出降级。
92
- * 历史样本不足 confirmCount 时不恢复(避免刚降级立即恢复抖动)。
4
+ * 第三批把阈值判定与看门狗状态机抽到 `dsh-shared` 的 `resource-rules` /
5
+ * `resource-guard`,本文件只做**别名 re-export**:
6
+ * - 保留插件内既有的导入路径与名称(`DEFAULT_LIMITS` 等),路由/测试/配置契约不变;
7
+ * - 判定口径(50MB/h 写入速率、50MB 文件、10% CPU、500MB RSS;相等不算超限;
8
+ * 连续 3 次确认)由 shared 单点维护,插件不再各存一份。
9
+ *
10
+ * 原语边界与反例见 plugins/dsh-shared/README.md 与 docs/共享工具包/概述.md。
93
11
  */
94
- export function shouldExitDegrade(history, limits = DEFAULT_LIMITS, confirmCount = RECOVER_CONFIRM_COUNT) {
95
- const recent = history.slice(-confirmCount)
96
- if (recent.length < confirmCount) return false
97
- return recent.every((sample) => !isCriticalOverLimit(sample, limits))
98
- }
99
-
100
- function fmtMB(bytes) {
101
- return `${(bytes / 1024 / 1024).toFixed(1)} MB`
102
- }
12
+ export { DEFAULT_RESOURCE_LIMITS as DEFAULT_LIMITS, evaluateResourceAlerts, shouldEnterDegrade, shouldExitDegrade, } from 'dsh-shared';
package/lib/review.js CHANGED
@@ -13,118 +13,109 @@
13
13
  *
14
14
  * AI 增强审查见 lib/ai.js(可选,agents 服务可用时追加 LLM 结论)。
15
15
  */
16
- import { isTestFile, isSourceFile } from './diff.js'
17
-
16
+ import { isTestFile, isSourceFile } from './diff.js';
18
17
  /** 单文件变更行数阈值(large-diff 规则)。 */
19
- export const LARGE_DIFF_LINES = 300
20
-
18
+ export const LARGE_DIFF_LINES = 300;
21
19
  /** 对已解析 diff 运行规则引擎,返回 { summary, issues }。 */
22
20
  export function reviewRules(parsed) {
23
- const issues = []
24
- for (const file of parsed.files) {
25
- for (const added of file.addedLines) {
26
- pushIf(issues, debugIssue(file, added))
27
- pushIf(issues, secretIssue(file, added))
28
- pushIf(issues, conflictIssue(file, added))
29
- pushIf(issues, todoIssue(file, added))
30
- pushIf(issues, trailingIssue(file, added))
21
+ const issues = [];
22
+ for (const file of parsed.files) {
23
+ for (const added of file.addedLines) {
24
+ pushIf(issues, debugIssue(file, added));
25
+ pushIf(issues, secretIssue(file, added));
26
+ pushIf(issues, conflictIssue(file, added));
27
+ pushIf(issues, todoIssue(file, added));
28
+ pushIf(issues, trailingIssue(file, added));
29
+ }
30
+ pushIf(issues, largeDiffIssue(file));
31
+ pushIf(issues, binaryIssue(file));
31
32
  }
32
- pushIf(issues, largeDiffIssue(file))
33
- pushIf(issues, binaryIssue(file))
34
- }
35
- pushIf(issues, noTestIssue(parsed))
36
- return summarize(parsed, issues)
33
+ pushIf(issues, noTestIssue(parsed));
34
+ return summarize(parsed, issues);
37
35
  }
38
-
39
36
  /** 汇总:变更统计 + 问题分级计数。 */
40
37
  function summarize(parsed, issues) {
41
- const files = parsed.files
42
- const insertions = files.reduce((sum, file) => sum + file.insertions, 0)
43
- const deletions = files.reduce((sum, file) => sum + file.deletions, 0)
44
- return {
45
- issues,
46
- summary: {
47
- files: files.length,
48
- insertions,
49
- deletions,
50
- issues: issues.length,
51
- errors: countBy(issues, 'error'),
52
- warnings: countBy(issues, 'warning'),
53
- infos: countBy(issues, 'info'),
54
- binary: parsed.binary,
55
- },
56
- }
38
+ const files = parsed.files;
39
+ const insertions = files.reduce((sum, file) => sum + file.insertions, 0);
40
+ const deletions = files.reduce((sum, file) => sum + file.deletions, 0);
41
+ return {
42
+ issues,
43
+ summary: {
44
+ files: files.length,
45
+ insertions,
46
+ deletions,
47
+ issues: issues.length,
48
+ errors: countBy(issues, 'error'),
49
+ warnings: countBy(issues, 'warning'),
50
+ infos: countBy(issues, 'info'),
51
+ binary: parsed.binary,
52
+ },
53
+ };
57
54
  }
58
-
59
55
  function countBy(issues, severity) {
60
- return issues.filter((issue) => issue.severity === severity).length
56
+ return issues.filter((issue) => issue.severity === severity).length;
61
57
  }
62
-
63
58
  function pushIf(issues, issue) {
64
- if (issue !== undefined) issues.push(issue)
59
+ if (issue !== undefined)
60
+ issues.push(issue);
65
61
  }
66
-
67
62
  /** 调试残留(console.* / debugger / print 族)。 */
68
63
  function debugIssue(file, added) {
69
- if (!/console\.(log|debug|warn|info|error)|debugger\b|\bprint\(|println\(|System\.out\.print/.test(added.text))
70
- return undefined
71
- return issue('warning', 'debug-statement', file, added, '调试残留语句(console/print/debugger)')
64
+ if (!/console\.(log|debug|warn|info|error)|debugger\b|\bprint\(|println\(|System\.out\.print/.test(added.text))
65
+ return undefined;
66
+ return issue('warning', 'debug-statement', file, added, '调试残留语句(console/print/debugger)');
72
67
  }
73
-
74
68
  /** 密钥/凭据硬编码。 */
75
69
  function secretIssue(file, added) {
76
- if (
77
- !/(password|passwd|api[_-]?key|secret|access[_-]?token|auth[_-]?token)\s*[:=]\s*['"][^'"]{4,}['"]/i.test(added.text)
78
- )
79
- return undefined
80
- return issue('error', 'secret-leak', file, added, '疑似硬编码密钥/凭据')
70
+ if (!/(password|passwd|api[_-]?key|secret|access[_-]?token|auth[_-]?token)\s*[:=]\s*['"][^'"]{4,}['"]/i.test(added.text))
71
+ return undefined;
72
+ return issue('error', 'secret-leak', file, added, '疑似硬编码密钥/凭据');
81
73
  }
82
-
83
74
  /** 合并冲突标记残留。 */
84
75
  function conflictIssue(file, added) {
85
- if (!/^(<<<<<<<|=======|>>>>>>>)/.test(added.text)) return undefined
86
- return issue('error', 'conflict-marker', file, added, '合并冲突标记残留')
76
+ if (!/^(<<<<<<<|=======|>>>>>>>)/.test(added.text))
77
+ return undefined;
78
+ return issue('error', 'conflict-marker', file, added, '合并冲突标记残留');
87
79
  }
88
-
89
80
  /** TODO/FIXME 标记。 */
90
81
  function todoIssue(file, added) {
91
- if (!/\b(TODO|FIXME|HACK|XXX)\b/.test(added.text)) return undefined
92
- return issue('info', 'todo-marker', file, added, 'TODO/FIXME 标记')
82
+ if (!/\b(TODO|FIXME|HACK|XXX)\b/.test(added.text))
83
+ return undefined;
84
+ return issue('info', 'todo-marker', file, added, 'TODO/FIXME 标记');
93
85
  }
94
-
95
86
  /** 尾随空格。 */
96
87
  function trailingIssue(file, added) {
97
- if (!/\s+$/.test(added.text)) return undefined
98
- return issue('info', 'trailing-space', file, added, '行尾多余空格')
88
+ if (!/\s+$/.test(added.text))
89
+ return undefined;
90
+ return issue('info', 'trailing-space', file, added, '行尾多余空格');
99
91
  }
100
-
101
92
  /** 单文件变更超阈值。 */
102
93
  function largeDiffIssue(file) {
103
- const changed = file.insertions + file.deletions
104
- if (changed <= LARGE_DIFF_LINES || file.binary) return undefined
105
- return issue('warning', 'large-diff', file, null, `单文件变更 ${changed} 行(>${LARGE_DIFF_LINES}),建议拆分提交`)
94
+ const changed = file.insertions + file.deletions;
95
+ if (changed <= LARGE_DIFF_LINES || file.binary)
96
+ return undefined;
97
+ return issue('warning', 'large-diff', file, null, `单文件变更 ${changed} 行(>${LARGE_DIFF_LINES}),建议拆分提交`);
106
98
  }
107
-
108
99
  /** 二进制文件变更。 */
109
100
  function binaryIssue(file) {
110
- if (!file.binary) return undefined
111
- return issue('warning', 'binary-file', file, null, '二进制文件变更')
101
+ if (!file.binary)
102
+ return undefined;
103
+ return issue('warning', 'binary-file', file, null, '二进制文件变更');
112
104
  }
113
-
114
105
  /** 有源码变更但无测试变更。 */
115
106
  function noTestIssue(parsed) {
116
- const sourceFiles = parsed.files.filter((file) => isSourceFile(file.path))
117
- const testFiles = parsed.files.filter((file) => isTestFile(file.path))
118
- if (sourceFiles.length === 0 || testFiles.length > 0) return undefined
119
- return issue('info', 'no-test-change', null, null, '有源码变更但没有测试文件变更')
107
+ const sourceFiles = parsed.files.filter((file) => isSourceFile(file.path));
108
+ const testFiles = parsed.files.filter((file) => isTestFile(file.path));
109
+ if (sourceFiles.length === 0 || testFiles.length > 0)
110
+ return undefined;
111
+ return issue('info', 'no-test-change', null, null, '有源码变更但没有测试文件变更');
120
112
  }
121
-
122
113
  function issue(severity, rule, file, added, message) {
123
- return {
124
- severity,
125
- rule,
126
- file: file?.path ?? '',
127
- line: added?.line ?? 0,
128
- message,
129
- }
114
+ return {
115
+ severity,
116
+ rule,
117
+ file: file?.path ?? '',
118
+ line: added?.line ?? 0,
119
+ message,
120
+ };
130
121
  }