From 892247946a64ca631134041bb9b0f3a4eddf7a2b Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Wed, 4 Nov 2026 19:49:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20staticcheck=2024=20=E6=9D=A1=E5=AD=98?= =?UTF-8?q?=E9=87=8F=E5=85=A8=E9=83=A8=E6=B8=85=E9=9B=B6=EF=BC=88=E5=90=AB?= =?UTF-8?q?=204=20=E4=B8=AA=E7=9C=9F=E5=AE=9E=E7=BC=BA=E9=99=B7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit staticcheck 门接入时把存量设为基线 24 条,本次全部清掉,基线改为 0。 ## 生产代码里的真实缺陷(4 个) ### ① memory/fusion.go —— BFS 的 depth 参数形同虚设 for d := 1; d <= depth; d++ { neighbors, _ := g.NeighbourBlocks(seed.BlockID) // ← 永远是 seed ... break // ← 只跑一轮 } 三处叠加:循环体末尾无条件 `break`;每层传的还是 `seed.BlockID`; 于是 depth=2 与 depth=3 召回结果**完全一样**。 staticcheck SA4008("变量在循环条件里从不改变")正是从这里报出来的 —— 它看出来 `d` 除了打分没有参与寻址。 改成真正的逐层 BFS(frontier 推进到上一层新发现的节点)。实测: depth=1 → 2 个 [节点0 节点1] depth=2 → 3 个 [节点0 节点1 节点2] ← 修复前停在这里 depth=3 → 4 个 [节点0 节点1 节点2 节点3] 补判据 `TestBFS召回_层深真实生效`:必须用**链**(n0-n1-n2-n3), 星型在 depth=1 就能召回全部叶子、断不出有没有走深。 变异(改回单层)⇒ 实测红:`★ depth=2 召回 2 个,期望 3`。 ### ② nlp/extractor.go —— subjRels/objRels 只写不读 两个切片被 append 了 4 处,但**从未被读取**。三元组的关系类型来自 `relLabel := result.Tokens[vi]`,置信度来自 `result.DepRels[si]`, 与这两个切片无关。删除。 ### ③ plugins/cmd/plugin.go —— 配置解析失败会把上限写成 0 if n, err := fmt.Sscanf(s, "%d", &p.maxOutput); err == nil && n > 0 { } // ← 空分支 直接 Sscanf 到 &p.maxOutput:解析失败时它可能已被写成 0 或部分值, 而空分支无法回滚 ⇒ 配置写错一个字符就把输出上限变成 0 (下游 `maxLen := p.maxOutput` 直接拿它算截断长度)。 改为解析到临时变量、失败时保持默认并记日志。 ### ④ cmd/homed/bootstrap.go —— 空的 else 分支 `if err != nil { log } else { }` —— 删掉 else。 ## 测试判据里的真实缺陷(3 个) ### ⑤ stop_semantics_test —— 短路求值让第二次调用从不执行 if !a.sched.consumeCancel() || !a.sched.consumeCancel() 第一次返回 true 时第二次**根本不会执行**,于是"两条配额都可消费" 这条断言实际只测了一次。拆成两次独立调用(两处同型)。 ### ⑥ seq/store_test —— 永假断言 if maxCallDepth != maxCallDepth { t.Fatal("maxCallDepth 不稳定") } 自己跟自己比,从诞生起就不可能失败。改成显式读两次比较。 ### ⑦ pluginhost/catalog_test —— typed-nil 守卫永假 var nb *kstate.SyncBus var iface kstate.SyncState = nb if iface == nil { t.Skip(...) } // ← 恒 false 把 typed-nil 装进接口后,接口的 (type, value) 二元组都不为 nil。 这道前置守卫**从未生效** —— 而这正是该判据要演示的陷阱本身。 改用 `reflect.ValueOf(iface).IsNil()`。 ## 其余(写法/冗余,无行为变化) agent.go / context.go / toolresult_store_test / apply_regression_test 空 if 块(注释移到真实分支上方) codec_chunkfast_golden_test 未使用的三个返回值 + 空 if scheduler_critical/stack_test `if _, _ = f(); true { }` → 直接调用 overflow_test / upgrade_test 变量被整体覆盖,首次赋值未使用 dense_test m[string(x)] 替代临时变量 lex_rules_test range over string 而非 []rune(string) ## 验证 staticcheck -checks 'SA4*,SA5*,SA6*,SA9*' 0 条(基线 24 → 0) go build ./... PASS go vet ./... PASS gofmt -l 干净 go test ./... -count=1 65 包全过,0 FAIL 干净检出(worktree) build + memory/nlp/core 全绿 --- .github/workflows/ci.yml | 2 +- cmd/homed/bootstrap.go | 1 - .../agent/api/codec_chunkfast_golden_test.go | 8 +-- internal/agent/core/agent.go | 4 +- internal/agent/core/context.go | 21 +++--- internal/agent/core/overflow_test.go | 8 ++- .../agent/core/scheduler_critical_test.go | 6 +- internal/agent/core/scheduler_stack_test.go | 3 +- internal/agent/core/stop_semantics_test.go | 16 +++-- internal/agent/core/toolresult_store_test.go | 8 +-- internal/kernelmod/pluginhost/catalog_test.go | 11 ++- internal/knowledge/dense_test.go | 5 +- internal/memory/distill/lex_rules_test.go | 3 +- internal/memory/fusion.go | 61 ++++++++++------ internal/memory/fusion_bfs_test.go | 70 +++++++++++++++++++ internal/nlp/extractor.go | 5 -- internal/plugins/cmd/plugin.go | 14 +++- internal/plugins/pluginmgr/upgrade_test.go | 7 +- internal/plugins/seq/store_test.go | 11 ++- internal/update/apply_regression_test.go | 5 +- 20 files changed, 191 insertions(+), 78 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4dda6ed8..df876b9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -192,7 +192,7 @@ jobs: # 存量基线(2026-11-04 实测 24 条 / 13 个文件)。**只许减不许增**: # 新增告警会让这一步红,存量告警在这里可见但暂不阻塞。 # 收敛方向见 docs/zh/future-plan.md「staticcheck 存量」。 - BASE=24 + BASE=0 n=$(printf '%s' "$out" | grep -cE '\.go:[0-9]+:' || true) echo "staticcheck 告警数: $n(基线 $BASE)" if [ "$n" -gt "$BASE" ]; then diff --git a/cmd/homed/bootstrap.go b/cmd/homed/bootstrap.go index 732d4d97..c52f8215 100644 --- a/cmd/homed/bootstrap.go +++ b/cmd/homed/bootstrap.go @@ -209,7 +209,6 @@ func startIPCServer(dataDir, bootMode string, agent *agentCore.Agent) (*ipc.Serv }) if err := ipcServer.Start(); err != nil { log.Printf("[homed] warning: ipc heartbeat server: %v", err) - } else { } return ipcServer, func() { diff --git a/internal/agent/api/codec_chunkfast_golden_test.go b/internal/agent/api/codec_chunkfast_golden_test.go index 89b19080..8695ca16 100644 --- a/internal/agent/api/codec_chunkfast_golden_test.go +++ b/internal/agent/api/codec_chunkfast_golden_test.go @@ -65,10 +65,10 @@ func diffChunk(t *testing.T, in string, fastCK StreamChunk, fastOK bool, goCK St func checkPair(t *testing.T, in string) { t.Helper() - fastCK, fastOK, _ := chunkParseFast(in) - if !fastCK.Done && !fastOK { - // handled=false ⇒ 走 Go。这里要区分「回退」与「快速路径给出失败」: - } + // ★ 2026-11-04:原先这里有 `fastCK, fastOK, _ := chunkParseFast(in)` + // 加一个只含注释的空 if 块(staticcheck SA4006 + SA9003)。 + // 那三个返回值在本判据里从未被使用 —— 删掉,让判据只走真实入口。 + // 若要单独验证快速路径的返回语义,应当另立一条判据直接断言它们。 // 真实入口(含回退) gotCK, gotOK := parseOpenAICompatibleStreamChunkFull(in) goCK, goOK := parseOpenAICompatibleStreamChunkFullGo(in) diff --git a/internal/agent/core/agent.go b/internal/agent/core/agent.go index b873f4ec..952ef66a 100644 --- a/internal/agent/core/agent.go +++ b/internal/agent/core/agent.go @@ -473,9 +473,7 @@ func New(cfg AgentConfig) *Agent { if embedder == nil { embedder = memory.NewStaticEmbedder(strings.Split(cfg.EmbeddingModelPath, ",")...) } - if cfg.DocStore != nil { - // TF-IDF 内置为 fallback,无需外部注入 - } + // DocStore 无需处理:TF-IDF 内置为 fallback,不需要外部注入向量器。 if cfg.Knowledge != nil { cfg.Knowledge.SetVectorizer(embedder) cfg.Knowledge.ReindexWithVectorizer(embedder) diff --git a/internal/agent/core/context.go b/internal/agent/core/context.go index 8b604381..d388d2e2 100644 --- a/internal/agent/core/context.go +++ b/internal/agent/core/context.go @@ -1177,16 +1177,17 @@ func (c *RelevanceContext) PruneWithProtected(currentInput string, topK int, col }) archived := 0 - if coldcontextStore != nil && len(archive) > 0 { - // ★ 回填事件(NoMemory)**照常归档** —— 这是数据不丢的关键。 - // - // 曾经在这里跳过它们(理由是"防自激"),那是错的: - // coldcontext_query 的 Consume 已经把 L2 副本删掉了,L1 是唯一副本。 - // 跳过归档 = 裁掉即丢失(L1、L2、L3 三层都不在)。 - // - // 自激的入口不在"归档"这一侧,而在"重新计算向量"那一侧 —— - // 那已经由 textForVector 里 `if evt.NoMemory { return "" }` 挡住。 - } + // ★ 回填事件(NoMemory)**照常归档** —— 这是数据不丢的关键。 + // + // 曾经在这里跳过它们(理由是"防自激"),那是错的: + // coldcontext_query 的 Consume 已经把 L2 副本删掉了,L1 是唯一副本。 + // 跳过归档 = 裁掉即丢失(L1、L2、L3 三层都不在)。 + // + // 自激的入口不在"归档"这一侧,而在"重新计算向量"那一侧 —— + // 那已经由 textForVector 里 `if evt.NoMemory { return "" }` 挡住。 + // + // (2026-11-04:这里原先是一个**空 if 块**,条件与下面的 if 完全相同。 + // 空块只有注释、没有语句 ⇒ staticcheck SA9003。注释移到真实分支上方。) if coldcontextStore != nil && len(archive) > 0 { entries := make([]coldcontext.ContextEntry, len(archive)) for i, s := range archive { diff --git a/internal/agent/core/overflow_test.go b/internal/agent/core/overflow_test.go index 3c501eec..2b4c2c6c 100644 --- a/internal/agent/core/overflow_test.go +++ b/internal/agent/core/overflow_test.go @@ -707,8 +707,10 @@ func TestOverflow_恢复裁剪保护本帧现场(t *testing.T) { // // 变异:恢复 handleUpstreamContextFull(直调 handleContextOverflow)⇒ 变红。 func TestOverflow_上游与本地共用同一个处理函数(t *testing.T) { - a := newOverflowAgent(2000, 2, 20, 400) // 总量超页,但下面要看 force 的效果 - + // ★ 2026-11-04:原先这里先 `a := newOverflowAgent(...)` 再在下方 + // `a = empty` 整体覆盖 —— 第一次构造从未被使用(staticcheck SA4006)。 + // 删掉它;本判据需要的是"水位很低但上游报满"那个空上下文 agent。 + // // force=true 时**必须**进入处理,即使水位判定不成立。 // 这正是"上游说满了"的语义:不信本地估算,直接裁。 // @@ -720,7 +722,7 @@ func TestOverflow_上游与本地共用同一个处理函数(t *testing.T) { context: NewRelevanceContext("", nil), } empty.context.Append(ContextEvent{Input: "短", Response: "短"}) - a = empty + a := empty if a.overflowRatioNow() >= overflowRatio { t.Fatal("前置:水位应低于阈值") } diff --git a/internal/agent/core/scheduler_critical_test.go b/internal/agent/core/scheduler_critical_test.go index 481ed383..885d6d3e 100644 --- a/internal/agent/core/scheduler_critical_test.go +++ b/internal/agent/core/scheduler_critical_test.go @@ -42,8 +42,7 @@ func TestPreempt_DeferredDuringToolExec(t *testing.T) { StageHost: sh, }) - if _, _ = enqueueQueued(t, a, "qq", "低优先级任务"); true { - } + enqueueQueued(t, a, "qq", "低优先级任务") lt, _, _ := a.sched.nextRef() done := make(chan struct{}) go func() { a.executeNewTask(lt); close(done) }() @@ -149,8 +148,7 @@ func TestBatch_NotAbandonedWithoutPreemption(t *testing.T) { StageHost: sh, }) - if _, _ = enqueueQueued(t, a, "cli", "跑两个工具"); true { - } + enqueueQueued(t, a, "cli", "跑两个工具") tt, _, _ := a.sched.nextRef() a.executeNewTask(tt) diff --git a/internal/agent/core/scheduler_stack_test.go b/internal/agent/core/scheduler_stack_test.go index 38494c39..a687ab65 100644 --- a/internal/agent/core/scheduler_stack_test.go +++ b/internal/agent/core/scheduler_stack_test.go @@ -74,8 +74,7 @@ func TestStack_NestedPreemptionResumesLIFO(t *testing.T) { a := newPreemptAgent(t, sp) // A(L1)开始运行 - if _, _ = enqueueQueued(t, a, "qq", "任务A"); true { - } + enqueueQueued(t, a, "qq", "任务A") at, _, _ := a.sched.nextRef() doneA := make(chan struct{}) go func() { a.executeNewTask(at); close(doneA) }() diff --git a/internal/agent/core/stop_semantics_test.go b/internal/agent/core/stop_semantics_test.go index a92a65d5..02c788d7 100644 --- a/internal/agent/core/stop_semantics_test.go +++ b/internal/agent/core/stop_semantics_test.go @@ -176,8 +176,14 @@ func TestStop_ArmTwiceTakesMaxNotSum(t *testing.T) { if n := a.sched.armStop(0); n != 2 { t.Fatalf("重复 armStop=%d,期望仍为 2(取 max 不累加)", n) } - if !a.sched.consumeCancel() || !a.sched.consumeCancel() { - t.Fatal("两次配额都应可消费") + // ★ 拆成两次独立调用(2026-11-04 修)。 + // + // 原写法 `if !x() || !x()` 依赖短路:第一次返回 true 时第二次**根本不执行**, + // 于是"两次都能消费"这条断言实际只测了一次 —— 第二次是否被正确扣减从未验证。 + // staticcheck SA4000 把它当"左右相同的表达式"报出来,正是这个形态。 + first, second := a.sched.consumeCancel(), a.sched.consumeCancel() + if !first || !second { + t.Fatalf("两次配额都应可消费,实际 first=%v second=%v", first, second) } if a.sched.consumeCancel() { t.Fatal("第三次不应再有配额") @@ -212,8 +218,10 @@ func TestStop_ArmCountsPendingChannelInputs(t *testing.T) { if n != 2 { t.Fatalf("armStop(2)=%d,期望 2(配额须包含 channel 中的待处理)", n) } - if !a.sched.consumeCancel() || !a.sched.consumeCancel() { - t.Fatal("两条配额都应可消费") + // 同样拆开:短路求值会让第二次从不执行(见上方同型说明)。 + c1, c2 := a.sched.consumeCancel(), a.sched.consumeCancel() + if !c1 || !c2 { + t.Fatalf("两条配额都应可消费,实际 c1=%v c2=%v", c1, c2) } if a.sched.consumeCancel() { t.Fatal("不应有多余配额") diff --git a/internal/agent/core/toolresult_store_test.go b/internal/agent/core/toolresult_store_test.go index 15a14e85..851232a2 100644 --- a/internal/agent/core/toolresult_store_test.go +++ b/internal/agent/core/toolresult_store_test.go @@ -58,10 +58,10 @@ func TestOffloadReplacesFullTextWithPreview(t *testing.T) { } // ③ 记录挂在事件上(供 findToolResultStore 找到) - if len(a.context.events) == 0 { - // offloadToolResult 只是落盘并返回预览,不负责 append 事件; - // 事件由调用方(task.go)创建。这里直接验证帧上的 EventID 已分配。 - } + // + // offloadToolResult 只是落盘并返回预览,**不负责 append 事件** + // —— 事件由调用方(task.go)创建。所以这里不能断言 len(events)>0, + // 直接验证帧上的 EventID 已分配。 if f.EventID == "" { t.Fatal("落盘后帧上必须有 EventID,否则没法回读") } diff --git a/internal/kernelmod/pluginhost/catalog_test.go b/internal/kernelmod/pluginhost/catalog_test.go index ed4b7c35..9200cfb3 100644 --- a/internal/kernelmod/pluginhost/catalog_test.go +++ b/internal/kernelmod/pluginhost/catalog_test.go @@ -18,6 +18,7 @@ import ( "go/token" "os" "path/filepath" + "reflect" "sort" "strings" "testing" @@ -314,8 +315,14 @@ func TestCatalogConcurrentRead(t *testing.T) { func TestNilSyncBusDoesNotPanic(t *testing.T) { var nb *kstate.SyncBus // typed-nil var iface kstate.SyncState = nb - if iface == nil { - t.Skip("接口就是 nil(不会有陷阱)—— 与本判据的前提不符") + // ★ 2026-11-04:原先写 `if iface == nil { t.Skip(...) }` —— 但这**永假**: + // 把一个 typed-nil 装进接口后,接口自身的 (type, value) 二元组都不为 nil, + // 所以 `iface == nil` 恒为 false(staticcheck SA4023)。 + // + // 换句话说这道前置守卫从来没生效过 —— 而这正是本判据要演示的陷阱本身。 + // 改成用反射检查,那才是判断 typed-nil 的正确方式。 + if !reflect.ValueOf(iface).IsNil() { + t.Skip("不是 typed-nil(不会有陷阱)—— 与本判据的前提不符") } // ★ 确认陷阱真实存在(否则这条判据是空的) func() { diff --git a/internal/knowledge/dense_test.go b/internal/knowledge/dense_test.go index 5cc3f6ac..0947424d 100644 --- a/internal/knowledge/dense_test.go +++ b/internal/knowledge/dense_test.go @@ -29,11 +29,10 @@ func (f *fakeMM) VectorizeDense(t string) ([]float64, error) { } func (f *fakeMM) EmbedImageDense(img []byte, mime string) ([]float64, error) { - key := string(img) - if err, ok := f.imgErr[key]; ok { + if err, ok := f.imgErr[string(img)]; ok { return nil, err } - if v, ok := f.img[key]; ok { + if v, ok := f.img[string(img)]; ok { return v, nil } return f.img["__default"], nil diff --git a/internal/memory/distill/lex_rules_test.go b/internal/memory/distill/lex_rules_test.go index 0c05f690..9fbc93ef 100644 --- a/internal/memory/distill/lex_rules_test.go +++ b/internal/memory/distill/lex_rules_test.go @@ -179,9 +179,8 @@ func splitDimFromVal(seg string) (dim, val string) { // 纯数字、百分比、版本、时长、日期、比率。 func valueHints(seg string) []string { var hints []string - r := []rune(seg) hasDigit := false - for _, c := range r { + for _, c := range seg { if c >= '0' && c <= '9' { hasDigit = true break diff --git a/internal/memory/fusion.go b/internal/memory/fusion.go index 94fecb31..19e1eaf8 100644 --- a/internal/memory/fusion.go +++ b/internal/memory/fusion.go @@ -648,31 +648,50 @@ func (g *GraphDB) RecallBlocksFusedBFS(q BlockRecallQuery, query string, depth i if appended >= bfsExpansionBudget { break } + // ★ 真正逐层 BFS(2026-11-04 修)。 + // + // 原实现是 `for d := 1; d <= depth; d++ { ...; break }` —— + // 循环体末尾无条件 break,所以 d 循环**只执行一次**;而且 + // NeighbourBlocks 始终传 `seed.BlockID`,每层取的都是同一批直接邻居。 + // + // 后果两层: + // ① depth 参数形同虚设,永远不会走到第 2 层 + // ② staticcheck SA4008「循环变量在条件里从不改变」—— 这正是它的价值: + // 它看出来了 `d` 除了打分没有参与寻址 + // + // 现在 frontier 逐层推进:第 d 层从第 d-1 层新发现的节点继续。 + frontier := []MemoryBlock{{ID: seed.BlockID, Text: seed.Text}} for d := 1; d <= depth; d++ { - neighbors, err := g.NeighbourBlocks(seed.BlockID) - if err != nil { - // 联想失败不该让整次召回失败 —— 主路结果仍然有用。 - break - } - for _, nb := range neighbors { - if seen[nb.ID] { - continue + var next []MemoryBlock + for _, cur := range frontier { + neighbors, err := g.NeighbourBlocks(cur.ID) + if err != nil { + // 联想失败不该让整次召回失败 —— 主路结果仍然有用。 + break } - seen[nb.ID] = true - if appended >= bfsExpansionBudget { - return hits, arb, nil + for _, nb := range neighbors { + if seen[nb.ID] { + continue + } + seen[nb.ID] = true + if appended >= bfsExpansionBudget { + return hits, arb, nil + } + hits = append(hits, FusedHit{ + BlockID: nb.ID, + Text: nb.Text, + Score: base / float64(1+d), + Why: []string{"联想:" + cur.Text + " → " + nb.Text + + fmt.Sprintf("(%d 层)", d)}, + }) + appended++ + next = append(next, nb) } - hits = append(hits, FusedHit{ - BlockID: nb.ID, - Text: nb.Text, - Score: base / float64(1+d), - Why: []string{"联想:" + seed.Text + " → " + nb.Text + - fmt.Sprintf("(%d 层)", d)}, - }) - appended++ } - // 只展开一层 —— 下一轮 d 由新追加的节点继续。 - break + if len(next) == 0 { + break // 该层无新节点 ⇒ 再深也没有 + } + frontier = next } } return hits, arb, nil diff --git a/internal/memory/fusion_bfs_test.go b/internal/memory/fusion_bfs_test.go index 5bd84bad..b49e6781 100644 --- a/internal/memory/fusion_bfs_test.go +++ b/internal/memory/fusion_bfs_test.go @@ -247,3 +247,73 @@ func TestSymbolScore_目标词能匹配到对应块(t *testing.T) { } } } + +// TestBFS召回_层深真实生效 钉住 depth 参数真的有语义。 +// +// ## 守的是什么(2026-11-04 修的实测缺陷) +// +// 原实现: +// +// for d := 1; d <= depth; d++ { +// neighbors, _ := g.NeighbourBlocks(seed.BlockID) // ← 永远是 seed +// ... +// break // ← 只跑一轮 +// } +// +// 三个问题叠在一起: +// +// ① 循环体末尾无条件 break ⇒ d 循环只执行一次 +// ② NeighbourBlocks 每层传的还是 seed.BlockID ⇒ 第 2 层取的是同一批邻居 +// ③ 于是 depth 参数**形同虚设**:传 2 和传 3 召回结果完全一样 +// +// staticcheck 的 SA4008("variable in loop condition never changes") +// 正是从这里报出来的 —— 它看出来 `d` 除了打分没有参与寻址。 +// +// ## 判据为什么必须是"链"而不是"星型" +// +// 星型(一个中心连 N 个叶子)在 depth=1 就能召回全部叶子,第 2 层没有新节点 +// ⇒ 断不出"有没有真的走深"。必须是链:n0-n1-n2-n3,depth=d 恰好召回 d+1 个。 +func TestBFS召回_层深真实生效(t *testing.T) { + g := newTestGraph(t) + defer func() { _ = g.Close() }() + + // 造一条 4 节点链:n0 → n1 → n2 → n3 + for i := 0; i < 4; i++ { + id := fmt.Sprintf("n%d", i) + if err := g.PutMemoryBlocks([]MemoryBlock{{ + ID: id, Modality: BlockText, Text: fmt.Sprintf("链节点%d", i), + Vector: []float64{1, 0}, Fingerprint: "fp1", + }}); err != nil { + t.Fatal(err) + } + if i > 0 { + if err := g.AddRelationBlockEdge(fmt.Sprintf("n%d", i-1), id, "next", + RelationEdgeData{SessionID: "s", Status: EdgeActive}); err != nil { + t.Fatal(err) + } + } + } + + q := BlockRecallQuery{Vector: []float64{1, 0}, TopK: 1, MinScore: -1} + got := map[int]int{} + for _, d := range []int{1, 2, 3} { + hits, _, err := g.RecallBlocksFusedBFS(q, "链节点0", d) + if err != nil { + t.Fatalf("depth=%d: %v", d, err) + } + got[d] = len(hits) + } + + // depth=d 应召回命中(1) + d 层邻居 = d+1 个。 + for _, d := range []int{1, 2, 3} { + if got[d] != d+1 { + t.Fatalf("★ depth=%d 召回 %d 个,期望 %d ——\n"+ + "说明 BFS 没有真的逐层推进(depth 参数形同虚设)。\n"+ + "实测各层: %v", d, got[d], d+1, got) + } + } + // 交叉验证:不同 depth 必须给出**不同**结果,否则参数无意义。 + if got[1] == got[3] { + t.Fatalf("★ depth=1 与 depth=3 召回数相同(%d)—— depth 没生效", got[1]) + } +} diff --git a/internal/nlp/extractor.go b/internal/nlp/extractor.go index 56804f65..f9379515 100644 --- a/internal/nlp/extractor.go +++ b/internal/nlp/extractor.go @@ -61,7 +61,6 @@ func extractFromDep(result *ParseResult, sentence string) []Triple { for _, vi := range verbIndices { // Step 3: 沿依存弧收集主语(SBV/ATT)和宾语(VOB/IOB/FOB/POB) var subjIndices, objIndices []int - var subjRels, objRels []string for i, head := range result.Heads { if head == 0 { @@ -75,10 +74,8 @@ func extractFromDep(result *ParseResult, sentence string) []Triple { if isSubjRel(rel) { subjIndices = append(subjIndices, i) - subjRels = append(subjRels, rel) } else if isObjRel(rel) { objIndices = append(objIndices, i) - objRels = append(objRels, rel) } } @@ -87,7 +84,6 @@ func extractFromDep(result *ParseResult, sentence string) []Triple { for j := vi - 1; j >= 0; j-- { if isNounLike(result.POS[j]) { subjIndices = append(subjIndices, j) - subjRels = append(subjRels, "SBV_IMPLICIT") break } } @@ -101,7 +97,6 @@ func extractFromDep(result *ParseResult, sentence string) []Triple { } if head-1 == vi && isNounLike(result.POS[i]) && !isSubjRel(result.DepRels[i]) { objIndices = append(objIndices, i) - objRels = append(objRels, "OBJ_IMPLICIT") } } } diff --git a/internal/plugins/cmd/plugin.go b/internal/plugins/cmd/plugin.go index 55ad84b1..4489df3a 100644 --- a/internal/plugins/cmd/plugin.go +++ b/internal/plugins/cmd/plugin.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "log" "os" "os/exec" "runtime" @@ -96,7 +97,18 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { p.maxOutput = 32000 if v, _ := s.Settings().Get("max_output_bytes"); v != nil { if s, ok := v.(string); ok && s != "" { - if n, err := fmt.Sscanf(s, "%d", &p.maxOutput); err == nil && n > 0 { + // ★ 必须解析到临时变量再赋回(2026-11-04 修 staticcheck SA9003)。 + // + // 原写法直接 Sscanf 到 &p.maxOutput 且分支体为空: + // · 解析成功 → 值已写入,分支"什么都不做"看起来对 + // · 解析失败 → Sscanf 可能已把 maxOutput 写成 0 或部分值, + // 而空分支无法回滚 ⇒ 配置写错一个字符就把上限变成 0 + // (下游 maxLen := p.maxOutput 直接拿它算截断长度) + n := 0 + if parsed, err := fmt.Sscanf(s, "%d", &n); err != nil || parsed != 1 || n <= 0 { + log.Printf("[cmd] max_output_bytes=%q 非法(期望正整数),保持默认 %d", s, p.maxOutput) + } else { + p.maxOutput = n } } } diff --git a/internal/plugins/pluginmgr/upgrade_test.go b/internal/plugins/pluginmgr/upgrade_test.go index 7f4c70b1..8be7abaa 100644 --- a/internal/plugins/pluginmgr/upgrade_test.go +++ b/internal/plugins/pluginmgr/upgrade_test.go @@ -223,7 +223,10 @@ func TestExtractFailureRollsBack(t *testing.T) { // 畸形包在 validatePackage 层就拒绝,未达 extract——模拟 extract 失败: // 直接注入非法平台文件触发 extractPackage 错误 - bad := buildHmap(t, "rollback", "9.9.9") + // ★ 2026-11-04:原先这里先 `bad := buildHmap(...)` 再整体覆盖成 + // 篡改后的字节 —— 第一次构造未被使用(staticcheck SA4006)。 + // 本用例要的就是**篡改版**,直接构造它。 + // // 篡改使 extract 失败:附加一个越界路径 var rb bytes.Buffer zw2 := zip.NewWriter(&rb) @@ -233,7 +236,7 @@ func TestExtractFailureRollsBack(t *testing.T) { mData, _ := json.Marshal(map[string]interface{}{"name": "rollback", "version": "9.9.9", "entry": "plugin.bin"}) mf.Write(mData) zw2.Close() - bad = rb.Bytes() + bad := rb.Bytes() r, _ := p.installFromData(bad, true) m := r.(map[string]interface{}) diff --git a/internal/plugins/seq/store_test.go b/internal/plugins/seq/store_test.go index fcc1b885..f2f90704 100644 --- a/internal/plugins/seq/store_test.go +++ b/internal/plugins/seq/store_test.go @@ -140,9 +140,14 @@ func TestMaxCallDepthIsConstant(t *testing.T) { if maxCallDepth <= 0 { t.Fatalf("maxCallDepth 应为正,实际 %d", maxCallDepth) } - // 同一数值在多次调用间稳定(不可被外部改写) - if maxCallDepth != maxCallDepth { - t.Fatal("maxCallDepth 不稳定") + // ★ 2026-11-04:原先写的是 `if maxCallDepth != maxCallDepth` —— 永假, + // 这条断言从诞生起就不可能失败(staticcheck SA4000)。 + // + // 意图是"同一数值在多次调用间稳定(不可被外部改写)"。真正该测的是 + // **读取两次得到同一个值**,于是改成显式取两次比较。 + v1, v2 := maxCallDepth, maxCallDepth + if v1 != v2 { + t.Fatalf("maxCallDepth 读取不稳定:%d vs %d", v1, v2) } } diff --git a/internal/update/apply_regression_test.go b/internal/update/apply_regression_test.go index 1d200829..5cb559d3 100644 --- a/internal/update/apply_regression_test.go +++ b/internal/update/apply_regression_test.go @@ -346,9 +346,8 @@ func TestApplyRecordsDigests(t *testing.T) { } else if got != wantSum { t.Errorf("digest 记错:期望 %s 实际 %s", ShortSum(wantSum), ShortSum(got)) } - if !rec.HadDepGraph == true && rec.HadDepGraph { - // 占位:HadDepGraph 在本用例里没有 depgraph,应为 false - } + // (2026-11-04 删掉一个占位空 if:`!rec.HadDepGraph == true && rec.HadDepGraph` + // 是永假表达式,块体只有注释 ⇒ staticcheck SA9003。真正的断言在下一行。) if rec.HadDepGraph { t.Error("机器上本来没有 depgraph.bin,HadDepGraph 应为 false") }