搜索语料与覆盖面盘点:先证明你究竟查了什么
故障复盘会上最危险的一句话不是“我没搜到”,而是“整个代码库都搜过了”。开发者可能只查了当前工作树,平台搜索可能只索引默认分支,子模块可能没有初始化,稀疏检出可能隐藏了一半目录,生成代码可能被 .gitignore 排除,受限账号也可能看不到真正的消费者。没有搜索面清单,“零结果”只是一个无法审计的观察。
搜索面盘点要把代码资产、文件可见性、解析条件、身份权限和时间状态放进同一份证据。它不追求把所有内容无差别塞进索引,而是让每次查询能够回答:哪些对象被纳入、哪些被排除、为什么排除、结果对应哪个提交与哪个身份、何时需要补查另一条路径。
这份证据也是搜索工具之间的共同接口。无论后面使用本地命令、语言服务器还是集中索引平台,都先消费同一份仓库与文件边界;工具切换时只替换查询能力,不重新发明资产事实。这样才能比较两次结果差异究竟来自代码变化、权限变化、索引延迟还是查询语义变化。
一条零结果背后的八种状态
搜索命令没有输出,可能是目标不存在,也可能是根目录错了、文件被忽略、隐藏目录被跳过、内容被判定为二进制、编码无法解码、分支不在本地、仓库不在当前身份权限内或集中索引尚未完成。只有第一种状态支持“没有引用”的结论,其他状态都要求改变语料或工具。
架构评审应把查询结果分成三层:工具执行状态、语料覆盖状态和业务解释。工具执行状态包含命令、版本、退出码和诊断;语料覆盖状态包含仓库、提交、分支、文件分类与权限;业务解释才是“可以删除旧接口”或“仍需兼容”。把三层压成一条截图,后续无法复现,也无法判断结论何时过期。
查询证据 = 工具与参数
+ 身份与授权
+ 仓库、分支、提交
+ 文件纳入和排除策略
+ 索引或工作树时间
+ 退出码、诊断和结果摘要
+ 缺口与补查动作建立仓库、分支与工作树清单
先从代码宿主导出仓库清单,记录活跃、归档、Fork、镜像和迁移中状态。归档仓库仍可能被运行系统引用,Fork 可能保留未合并补丁,镜像可能存在同步延迟。对每个仓库记录默认分支、仍受支持的发布分支、最近提交、owner 和构建入口。只查默认分支适合快速发现,不足以支持兼容性删除。
本地工作树必须固定提交。未提交修改会让另一位工程师无法复现结果;浅克隆缺少历史,单分支克隆没有维护分支,未初始化子模块缺少嵌套源码,稀疏检出只呈现选定路径,Git LFS 指针也不等于实际大文件内容。盘点不能只问“当前分支叫什么”,还要证明代码宿主有哪些远端分支、当前提交与上游是否分叉、对象库是否浅化,以及每个子模块与 LFS 对象是否真的落盘。下面的命令只读当前仓库,并为会失败的探针单独保存退出码。
$evidence = Join-Path $env:TEMP "search-surface-evidence"
New-Item -ItemType Directory -Force $evidence | Out-Null
git rev-parse --show-toplevel | Set-Content "$evidence/root.txt"
git rev-parse HEAD | Set-Content "$evidence/commit.txt"
git status --short | Set-Content "$evidence/worktree.txt"
git branch --show-current | Set-Content "$evidence/branch.txt"
git remote -v | Set-Content "$evidence/remotes.txt"
git for-each-ref refs/remotes --format='%(refname:short) %(objectname)' |
Set-Content "$evidence/remote-branches.txt"
git rev-list --left-right --count 'HEAD...@{upstream}' 2>&1 |
Set-Content "$evidence/upstream-divergence.txt"
"upstream_exit=$LASTEXITCODE" | Set-Content "$evidence/upstream-exit.txt"
git rev-parse --is-shallow-repository |
Set-Content "$evidence/shallow.txt"
git submodule status --recursive | Set-Content "$evidence/submodules.txt"
"submodule_exit=$LASTEXITCODE" | Set-Content "$evidence/submodule-exit.txt"
git sparse-checkout list 2>&1 | Set-Content "$evidence/sparse-checkout.txt"
"sparse_exit=$LASTEXITCODE" | Set-Content "$evidence/sparse-exit.txt"
git lfs ls-files --all --long 2>&1 | Set-Content "$evidence/lfs-files.txt"
"lfs_list_exit=$LASTEXITCODE" | Set-Content "$evidence/lfs-list-exit.txt"
git lfs fsck 2>&1 | Set-Content "$evidence/lfs-fsck.txt"
"lfs_fsck_exit=$LASTEXITCODE" | Set-Content "$evidence/lfs-fsck-exit.txt"
git count-objects -vH | Set-Content "$evidence/object-size.txt"预期证据中 commit.txt 是完整提交标识,worktree.txt 为空或明确列出未提交差异,shallow.txt 为 false。upstream-divergence.txt 的两个数字分别表示只在本地与只在上游的提交数;非零不一定错误,却意味着查询快照不能冒充远端最新状态。子模块状态行首的 - 表示未初始化,+ 表示检出提交与父仓记录不一致,U 表示冲突;这些状态都要阻断“完整搜索”结论。
sparse-checkout list 非零可能表示没有启用稀疏检出,也可能表示 Git 版本或工作树状态异常,所以必须连同诊断解释;启用时还要把选中的目录与仓库总跟踪集合比较。git lfs ls-files --all 证明哪些路径受 LFS 管理,git lfs fsck 才验证当前对象是否存在且一致;若 Git LFS 未安装,退出码本身就是搜索面缺口。文本指针能被 rg 命中,只证明查到了 OID 和大小声明,不证明真实对象内容被检索。
维护分支不能靠切换当前工作树逐个碰碰运气。对需要证明兼容性的分支,先从代码宿主的受支持分支清单生成隔离 worktree 或独立只读镜像,为每个分支记录完整提交号并分别查询;同一目录反复 checkout 会让证据被后一次操作覆盖。标签只在发布产物确实以标签为权威入口时纳入,已归档分支则保留 owner 的退役判断,避免把所有历史分支无上限复制到热索引。
逐层解释文件为什么可见或不可见
Git 跟踪集合、文件系统集合和搜索工具默认集合并不相同。git ls-files 反映被跟踪及可选的未跟踪文件;rg --files 会应用自己的隐藏、二进制和忽略策略;IDE、Ctags、语言服务器与集中索引器又有各自排除配置。清单要列出每一层的来源和优先级,而不是只复制一份 .gitignore。
git ls-files -co --exclude-standard | Sort-Object | Set-Content "$evidence/git-visible.txt"
rg --files | Sort-Object | Set-Content "$evidence/rg-default.txt"
rg --files --hidden --no-ignore | Sort-Object | Set-Content "$evidence/rg-all.txt"
Compare-Object `
(Get-Content "$evidence/rg-default.txt") `
(Get-Content "$evidence/rg-all.txt") |
Set-Content "$evidence/rg-boundary.diff.txt"正向结果是 rg-default.txt 包含日常需要检索的源码,差异文件能够解释缓存、构建产物、隐藏配置和依赖目录为何被排除。反向实验可以在临时目录增加 .gitignore、.ignore 和 .rgignore,故意制造覆盖差异,再用 rg --debug 查看规则来自哪里。不要为了追求“全”而长期使用 --no-ignore;这会把 node_modules、密钥、缓存和大型产物带入结果,既降低信噪比,也扩大敏感信息暴露。
生成代码和 Vendored 代码需要单独分类。生成文件可能是发布工件、也可能只是可重建缓存;Vendor 目录可能需要漏洞排查,但通常不应参与业务重命名。每类文件要记录权威来源、是否允许直接修改、生成命令和验证方式。否则批量替换会改动一个下次构建即被覆盖的副本,或错误地修改第三方代码。
“权威来源”不能凭目录名猜测。仓库里同时存在 src、generated 和发布包时,应沿构建任务追踪谁生成谁:若 OpenAPI 描述生成客户端,描述文件是结构权威,客户端只是验证生成链的派生物;若数据库工具反向生成快照,运行数据库或迁移历史可能才是权威。搜索清单应给每组派生关系分配 owner,并记录修改入口。重构命中派生物时,正确动作通常是回到上游修改并重新生成,然后比较生成差异,而不是把所有命中机械替换。
同一文件也可能在不同任务里承担不同角色。漏洞响应需要扫描 Vendor 目录中的版本与危险调用,业务 API 重命名却应把它排除;许可证审计需要看到压缩包清单,普通导航不需要解包每个归档。于是纳入策略不能只有永久的 include/exclude,而要绑定查询目的。一个可维护的清单至少区分日常导航、兼容性迁移、安全审计和合规取证四类 profile,并为临时放宽边界记录到期时间。
差异文件本身也需要解释。默认集合少于扩展集合是正常现象,但新增差异突然翻倍,可能表示构建目录改名、忽略规则失效或依赖被错误提交;默认集合突然变小,则可能是根级 .gitignore 误匹配、稀疏检出改变或生成任务没有运行。团队应对文件数和总字节设置变化告警,但不把固定数量当质量目标,最终判断仍回到分类与权威关系。
识别二进制、编码、大文件和动态资产
搜索器通常通过 NUL 字节或启发式规则判断二进制内容,集中索引也可能跳过超大文件、非 UTF-8 文本和生成文件。数据库迁移脚本、旧系统源码、压缩归档、Notebook、Protobuf 生成物和 LFS 对象都可能处在边界上。清单至少记录扩展名、MIME 或检测结果、编码、大小分布、是否索引以及替代查询方式。
Get-ChildItem -File -Recurse | ForEach-Object {
[pscustomobject]@{
Path = $_.FullName
Bytes = $_.Length
Extension = $_.Extension
}
} | Sort-Object Bytes -Descending |
Select-Object -First 100 |
Export-Csv "$evidence/largest-files.csv" -NoTypeInformation -Encoding utf8
rg --stats --hidden --glob '!node_modules/**' --glob '!dist/**' 'API_V1' . 2>&1 |
Set-Content "$evidence/query-stats.txt"若某些文本必须用指定编码解码,应在专用脚本中明确转换并保留原始文件哈希,不能让搜索工具静默跳过后仍声称全量覆盖。大文件应先判断权威性和检索价值,再决定独立索引、预处理或排除。将压缩包全部解包进共享索引,往往会引入重复结果、恶意样本和不可控容量。
把权限和索引时间写进结果
集中代码搜索的结果天然受身份影响。同一个查询由普通开发者、平台管理员和服务账号执行,仓库集合可能不同。清单要保存身份类型、组织成员关系、仓库过滤策略和授权同步时间,不保存真实 Token。若平台只做认证而没有仓库级授权,集中索引会成为源码泄露通道;若权限同步滞后,离职或转组后的访问可能继续存在。
索引新鲜度至少包含仓库抓取时间、目标提交、索引完成时间和失败状态。仓库同步成功不等于索引成功,默认分支更新也不等于维护分支已索引。支持精确结果的系统应能把命中追溯到仓库与提交;做不到时,报告必须降级为候选发现,不得支撑删除或合规证明。
search_surface:
host: "code-host-a"
repository: "payments/service-a"
repository_state: "active"
identity_class: "read-only-search-bot"
branches:
default: "main"
maintained: ["release/2.x"]
checkout:
commit: "<full-commit-sha>"
shallow: false
sparse: false
worktree_clean: true
submodules:
initialized: true
commits_match_superproject: true
lfs:
client_available: true
objects_verified: true
file_policy:
generated: "search-separately"
vendor: "security-only"
hidden: "explicit-query"
binary: "excluded-with-owner"
index:
commit: "<indexed-commit-sha>"
generation: "<immutable-index-generation>"
completed_at: "<runtime-observation>"
authorization_snapshot: "<policy-revision>"
query:
tool_version: "<version>"
exit_code: 1
result_count: 0
stderr_class: "none"
gaps:
- "release/1.x is offline; owner must confirm retirement"用正反实验拆开四种零结果
先用一个临时目录制造普通文件、隐藏文件、被忽略文件和含 NUL 字节的二进制文件。正向查询证明默认语料能命中,反向查询再逐层放宽边界;最后故意查询不存在的根目录,把“无命中”和“工具错误”拆开。rg 的退出码 0 表示至少一处命中,1 表示搜索成功但没有命中,2 表示错误,不能把后三者都渲染成空列表。
$lab = Join-Path $env:TEMP "search-surface-lab"
$out = Join-Path $env:TEMP "search-surface-lab-evidence"
Remove-Item -LiteralPath $lab -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $out -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force "$lab/src", "$lab/generated", "$lab/.hidden" | Out-Null
New-Item -ItemType Directory -Force $out | Out-Null
git -C $lab init -q
Set-Content "$lab/src/live.txt" "SURFACE_PROBE_7F3A"
Set-Content "$lab/generated/client.txt" "SURFACE_PROBE_7F3A"
Set-Content "$lab/.hidden/policy.txt" "SURFACE_PROBE_7F3A"
Set-Content "$lab/.gitignore" "generated/"
[IO.File]::WriteAllBytes(
"$lab/src/archive.bin",
[Text.Encoding]::UTF8.GetBytes("binary`0SURFACE_PROBE_7F3A")
)
rg --files $lab | Set-Content "$out/default-files.txt"
"default_files_exit=$LASTEXITCODE" | Add-Content "$out/evidence.txt"
rg "SURFACE_PROBE_7F3A" $lab | Set-Content "$out/default-results.txt"
"default_query_exit=$LASTEXITCODE" | Add-Content "$out/evidence.txt"
rg --hidden --no-ignore -a "SURFACE_PROBE_7F3A" $lab |
Set-Content "$out/expanded-results.txt"
"expanded_query_exit=$LASTEXITCODE" | Add-Content "$out/evidence.txt"
rg --debug "SURFACE_PROBE_7F3A" $lab 2> "$out/rg-debug.log" | Out-Null
"debug_query_exit=$LASTEXITCODE" | Add-Content "$out/evidence.txt"
rg "SURFACE_PROBE_7F3A" "$lab/missing-root" 2> "$out/path-error.txt" | Out-Null
"path_error_exit=$LASTEXITCODE" | Add-Content "$out/evidence.txt"
Get-Content "$out/evidence.txt"
Get-Content "$out/default-results.txt"
Get-Content "$out/expanded-results.txt"
Remove-Item -LiteralPath $lab -Recurse -Force
Remove-Item -LiteralPath $out -Recurse -Force默认查询应以 0 退出,只命中 src/live.txt;它不会读取隐藏目录和被 .gitignore 排除的生成目录,二进制文件即使包含字节序列也不会作为普通文本结果出现。加上 --hidden --no-ignore -a 后,四个样本都应出现,证明前三次缺失分别来自隐藏、ignore 与二进制判定。不存在的根目录应以 2 退出并在 path-error.txt 留下诊断,它不是“目标不存在于代码中”。若把探针换成真正不存在的字符串,根目录有效且 stderr 为空时才应得到退出码 1,这才是工具层面的零命中。
集中索引还要做一次“两身份、两代次”反向实验。先在允许访问的测试仓库提交唯一探针,记录代码宿主提交 C1,等待平台声明索引代次 G1 完成;有权普通身份应命中,无权普通身份应得到 403/404 或仓库不可枚举,而不是返回同一命中后仅在页面隐藏。随后删除探针形成 C2,在索引仍停留 G1/C1 时查询应继续命中,这证明结果陈旧;只有新代次 G2/C2 发布后,原身份得到零命中,且平台项目清单、索引 manifest 与授权策略修订号都对齐,才能把零结果用于迁移判断。
这组实验要保存请求身份类别、HTTP 状态、结果数、仓库/分支/提交、索引代次、策略修订号和平台诊断,不保存 token 或完整源码片段。测试结束后删除临时仓库或探针提交,撤销两枚临时凭证,等待旧索引代次退出保留窗口,并用无权身份确认旧入口不可访问;只清理本地目录,不能证明集中副本已经消失。
将盘点接入项目与变更流程
项目仓库可以维护机器可读的 search-surface.yml,由 CI 校验路径、owner 和过期时间;组织级平台定期对比代码宿主仓库清单与索引仓库清单,发现未索引、已归档但仍运行、权限异常或索引滞后的对象。高风险删除和跨仓库迁移必须引用一次具体盘点快照,普通本地排障则可使用轻量命令输出。
盘点自动化需要限速和缓存。频繁遍历所有仓库、所有分支和大文件会消耗 API 配额、网络、磁盘与索引 CPU。可按活跃度分层:核心仓库高频增量更新,维护分支按发布节奏更新,归档仓库低频校验。失败队列必须有 owner,不能让“上次成功”长期掩盖当前缺口。
容量预算要按语料类别拆开,而不是只看仓库压缩包大小。至少分别测量 Git 对象、检出文件、LFS 已拉取对象、子模块、生成物、Vendor、每个维护分支的额外唯一对象和集中索引字节;再把抓取频率、全量重建临时双份空间、备份保留与查询热度放进成本模型。同一提交被十个分支引用不应机械复制十份对象,但十棵独立工作树和十套索引仍可能放大 inode、缓存与索引开销。
容量紧张时先治理权威源码:停止索引已退役分支,按用途拆分生成物与 Vendor profile,限制无 owner 的 LFS 大对象和压缩归档,把低频归档仓库移到可按需恢复的冷层。直接扩大 --no-ignore、递归解包制品或把所有历史分支永久放进热索引,表面提高覆盖率,实际会增加重复命中、敏感面、重建时间和恢复成本。每次排除都要留下替代查询路径和 owner,否则节省的存储会转化成不可见风险。
inventory_policy:
active_default_branch: "incremental"
maintained_branch: "daily"
archived_repository: "monthly"
max_api_concurrency: 4
max_repository_bytes: "5 GiB"
stale_after: "24h"
failure_owner: "developer-platform"
evidence_retention: "90d"安全、成本和职责边界
搜索面证据可能暴露仓库名称、目录结构、提交标识和敏感文件路径。报告应保存统计与分类,避免复制源码和密钥;调试日志经过脱敏后再进入工单。服务账号使用只读权限和短期凭证,网络出口限定到代码宿主,索引存储加密并配置保留期限。真实客户数据、生产转储和个人目录不应因为“搜索完整性”被默认纳入代码索引。
平台团队维护仓库发现、权限同步、索引 SLO 和容量;仓库 owner 维护生成物、Vendor、分支和构建语义;安全团队审查敏感目录与授权;迁移负责人决定业务上还需补查哪些运行事实。清单没有 owner 或过期策略,就会快速退化成静态表格。
退役同样需要盘点。仓库迁移到新宿主时,旧宿主、镜像、搜索索引和本地缓存可能继续返回陈旧命中;仅删除导航入口不会清除源码副本。退出流程要先冻结旧索引更新并标记只读,验证新宿主的仓库数、提交与权限,再撤销抓取凭证、删除旧索引和源码副本,最后用受限身份确认旧入口不可查询。删除证据和保留期限由安全与合规要求决定,不能由一次磁盘清理日志代替。
搜索面清单的完成标准不是“表格每格都有值”,而是能够推动决策。若某个维护分支离线、某组二进制无法解析或某个宿主权限无法校验,清单应明确把架构结论降级,并给出补查 owner、时限和替代证据。带缺口的真实清单优于伪装完整的全绿清单,因为它能阻止删除、批量替换和合规声明建立在不可见区域之上。
清单还要参与变更评审。删除公共接口时,评审人不只看命中列表,还要核对语料快照是否晚于最后一次消费者发布、维护分支是否仍在支持期、跨宿主权限是否覆盖合作团队,以及运行遥测能否证明动态调用已经归零。任何一项不能成立,就保留兼容入口或先增加观测,而不是用“本地没有搜到”推动删除。这样,搜索面从一次性调查材料变成架构决策的输入契约。
对于必须在短时间内完成的应急排查,可以缩小仓库和分支集合,但要把缩小条件写进结论,并在恢复阶段补做完整查询。速度来自明确降级,不来自隐藏缺口;临时结论一旦被用于永久删除、权限审计或兼容性承诺,就必须升级为完整证据。
每次升级证据后,都应重新签署结论并关闭对应缺口。
完成盘点时应能回答的问题
查询对应哪个代码宿主、仓库、分支、提交和工作树状态。子模块、稀疏检出、浅克隆、LFS 和未提交修改是否改变语料。.gitignore、.ignore、.rgignore、工具配置和平台索引规则分别排除了什么。
生成、Vendor、隐藏、二进制、非 UTF-8 和大文件由谁负责、如何补查。当前身份能看到哪些仓库,权限同步与索引完成发生在何时。零结果是无命中、被排除、无权限、未索引还是工具错误。
正反实验能否稳定复现边界,证据是否包含退出码和诊断。临时凭证、测试仓库、索引、缓存和本地实验目录是否已清理。
