Commit Graph

3 Commits

Author SHA1 Message Date
c01f4d6350 test: 两个零测试包补黑箱判据 —— files(沙箱)与 tfidf
覆盖率分析显示这两个包**零测试文件**,而它们各自守着一条不能错的边界:

    internal/plugins/files   581 行  含沙箱逃逸防护
    internal/tfidf           306 行  记忆检索的 fallback 路径

## files:沙箱边界

581 行、0 判据。它是文件读写插件,`pathWithinSandbox` 写错一个字符就是
任意文件读写。没有判据意味着没人知道它现在是否还挡得住。

6 条判据全部走**真实装配**(调 `Start()` 而不是手搓 Plugin 结构),
通过注入的 registrar 捕获 handler,然后以「输入 {path, content} → 输出」断言:

    TestToolsAreRegistered           四个工具都在
    TestReadWriteRoundTrip           写进去能原样读回
    TestSandboxEscapeIsRejected      ★ 沙箱外路径必须被拒(3 种逃逸形态)
    TestSandboxAllowsInside          反向:沙箱内必须能读
    TestEmptyPathIsRejected          read/write 拒绝空 path
    TestLsEmptyPathMeansCurrentDir   ls 把空 path 当 "."
    TestLsListsDirectory             ls 列出目录内容

**沙箱那条的判据设计**:沙箱外造一个**真实存在**的文件。若只拦不存在的
路径,判据会假绿;文件真实存在而仍被拒,才证明是沙箱在起作用。

变异(`pathWithinSandbox` 加 `if true { return true }`)⇒ 实测红:

    ★ 沙箱外路径 ".../secret.txt" 被接受了,返回 {"content":"SECRET",...}
      这是任意文件读取。沙箱根: ...

## tfidf:检索判别力

306 行、0 判据。它是没有神经网络 embedder 时的文本相似度来源 ——
判据错 = 检索错,而检索错是静默的(不报错,只是答非所问)。

判据**只断言契约,不断言数值**:TF-IDF 的权重取决于语料统计,写死数值
等于把训练集也当成契约。6 条钉的是换了实现也必须成立的性质:

    TestVectorizeIsDeterministic            同输入同输出(map 遍历浮点累加
                                            会产生末位差异,让缓存/去重失效)
    TestVectorizeHasFixedDimAndNonNegative  维度恒定、非 NaN/Inf、非负
    TestEmptyTextGivesZeroVector            空文本不 panic 且给零向量
    TestSearchRanksRelatedAboveUnrelated    ★ 核心质量判据:只排序,不看数值
    TestAddRemoveChangesResults             增删后检索结果随之变化
    TestFingerprintAndDim                   元信息非空

`TestSearchRanksRelatedAboveUnrelated` 直接对治 engineering-lessons §1.1
记的那个坑:16 条判据全绿而检索结果完全无关(分数全挤在 0.75~0.79)。
所以它只问"billing invoice 的查询里,auth 文档有没有排到相关文档前面"。

变异(`Search` 提前 `return nil`)⇒ 实测红两条。

## 一处判据写错了(已按实际契约修正)

第一版把 `files_ls` 也纳入"空 path 必须拒绝",实测红:

    ★ files_ls 接受了空 path,返回 {"content":"(empty directory)"}

查代码(plugin.go:488)发现那是**明确设计**:`if path == "" { path = "." }`。
不是缺陷,是我把三条工具的输入契约当成同一条了。改成两条判据分开钉:

    TestEmptyPathIsRejected          read/write 拒绝空 path
    TestLsEmptyPathMeansCurrentDir   ls 把空 path 当 "."

把差异显式写下来,以后有人想"统一都拒绝空 path"时能看到这是有意的。

## 覆盖率

    internal/plugins/files   0.0% → 54.1%
    internal/tfidf           0.0% → 有判据(契约全覆盖)

## 验证

    go build ./...            PASS
    go vet ./...              PASS
    gofmt -l                  干净
    go test ./... -count=1    67 包全过,0 FAIL(新增 2 个 test 包)
2026-11-04 20:41:43 +08:00
38d6e2d580 style: gofmt 两处对齐(graphmedia_test.go 注释、tfidf.go 结构体字段)
这两处是本次特性分支带入的格式回归(origin/main 上不脏)。
纯空白/对齐调整,无语义变化;不整体重排文件。
2026-09-12 08:23:30 +08:00
2bcd3e94ee refactor(memory): 拆除描述式媒体索引,媒体成为一等块并按原生向量融合
背景:此前媒体是靠「生成的描述文本」将就进记忆的——写 marker 进正文、
再由正则反解成 media_refs 与图库里的 type=Media 实体。这条链路有三个
致命缺陷:描述由异步模型生成(未生成前媒体等于不存在)、语义检索实质上
只搜描述文字、图库里的「媒体节点」是描述文本的投影而不是媒体本身。

本提交把这条链路整体拆除,媒体改为按自己的原生向量参与记忆:

一、描述链彻底删除(无残留、无兼容分支)
- media.Item 去掉 Description/DescribedBy 与对应列;
- 删除 Store.Describe / Store.Search / Store.Pending;
- 删除 Agent.mediaDescribeLoop / describePendingMedia 与配置项
  core.memory.media.describe_on_ingest;
- SDK 侧 MediaAttachment 去掉 Description(见 SDK 仓独立提交)。

二、marker 机制删除,媒体归属改为结构化块边
- 删除 mediaMarkerLine/parseMediaMarkers/mediaEntityName/mediaTriplesFromText/
  extractMediaDigests/sentenceWithMediaMarkers/docMediaContext;
- memory.Triple 新增 MediaDigests 结构化字段;句子文本保持原样,
  不再被 marker 污染;
- 块以 sentence --contains--> block / document --contains--> block 结构边
  挂到承载节点(新增 documents 表与 document 节点种类);
- 模型未给原句时用「主谓宾。」拼一句自然语言作落点,不造 marker 文本。

三、旧数据迁移(幂等)
- 新增 GraphDB.MigrateLegacyMediaEntities:把 type=Media 的旧实体按短 digest
  还原成原生块、挂回原句子、删除旧实体与描述关系;Agent 启动时执行;
- CleanupOrphanedSentences 同时看关系引用与块边,避免把只靠块存活的句子
  连同块边一起删掉。

四、向量融合:媒体按图本身被召回
- 新增 vector.FuseVectors(逐维求和 + L2 归一化);
- Doc.DenseVec = 文本向量 ⊕ 文档块的媒体向量(同 fingerprint 才融合),
  新增 Doc.DenseFP,指纹变化触发重算;
- ContextEvent.DenseVec 同理融合事件块;事件新增 DenseFP,Prune 只在
  同一统一空间内比稠密余弦;
- 跨模态视觉路只召回「仍被某层记忆块持有」的媒体,CAS 全库字节不再
  直接充当记忆检索结果。

五、同时纳入本分支既有的嵌入基础改造(此前工作区未提交,缺它 HEAD 不可构建)
- internal/tfidf 懒回退包、千问三段式多模态 ONNX 空间的 Go 侧
  (qwen/embedder.go、image.go、model_input.go)、CLIP 移除、
  sdk.NewStore 分词器签名与调用点、embed 侧车 systemd 单元。

验证:go build ./... 、go vet ./...(含 -tags medialive)均通过;
在 HEAD 的独立 worktree 上重放本次暂存集后 go test -short ./internal/...
全部通过(端口冲突类用例在隔离环境中亦通过)。未提交工作区中与本改造
无关的改动(HarmonyOS、waiter、devicebridge、plan.md 等)。
2026-09-11 11:45:24 +08:00