清理脚本最危险的地方,往往不是它不能删除文件,而是它太容易删除文件。
Asset Governance 因此被我拆成两个程序:audit.hhy 只观察并生成报告,cleanup.hhy 只执行报告中明确列出的 copy、move 和 remove。判断与副作用之间保留了一道可以阅读、评审和 dry-run 的边界。
自动化越接近破坏性操作,计划越应该成为一等产物。

审计器先建立资产清单
inventory 用 files("**/*") 收集文件,以 Bytes 累计总大小,并检查三类元数据:超过配置阈值、文件名包含大写或空格、修改时间超过 365 天。
if file.size > limit {
findings = append(findings, finding(
join(["large", file.name], "."),
"large_file",
"warning",
"File exceeds configured size limit",
file.name
))
}
内容检查只读取明确支持的文本扩展名。疑似凭据会生成 critical finding,但原文绝不进入报告。重复文本通过 group_by { record -> record.content } 识别,输出也只包含文件名列表。
这里有意没有把图片和视频交给解码器。这个项目治理的是文件元数据和放置规则;如果目标只是发现超大 MP4,没有必要为它引入媒体解析依赖。
报告既是证据,也是整改输入
报告包含 inventory 统计、findings 和配置提供的 actions。出现 critical 时 audit.hhy 返回 1,但报告仍会完整原子写入。
这和直接在扫描循环中移动文件有本质区别。审计失败不等于程序失控,它意味着程序成功发现了一个必须处理的问题;操作者仍然拿得到完整证据。
cleanup.hhy 不接受自由格式命令,只识别三个动作:
if action.kind == "copy" {
copy(source, path_join(root, action.target), { overwrite: true })
} else if action.kind == "move" {
move(source, path_join(root, action.target), { overwrite: true })
} else if action.kind == "remove" {
remove(source)
} else {
exit(3)
}
动作来源受报告约束,执行也不经过 shell。这把注入面和不可预测命令排除在模型之外。
Dry-run 应该由 Runtime 保证
我没有在 cleanup.hhy 中写一套 if dry_run。同一个程序直接通过:
hhy run --dry-run practical-projects/asset-governance/cleanup.hhy PROJECT report.json
Runtime 的 EffectDispatcher 拦截 copy、move、remove 等副作用并输出计划。业务代码走过相同分支,却不会真的改变文件。相比在每个调用点手工维护预览逻辑,这种语义更统一,也更难遗漏新的副作用。

自测用 mktemp 创建隔离工作区,先验证 large、naming、stale、sensitive 和 duplicate findings;dry-run 后逐项确认文件完全不变;正式执行后再确认 copy、move、remove 都生效。
我保留的工程原则
安全自动化不是永远不做危险操作,而是把观察、决定和执行分开,让每一步都有可检查的输入输出。
Asset Governance 最重要的产物不是清理后的目录,而是这条可审计链:文件事实形成 findings,findings 与人工配置形成 actions,actions 经过 dry-run 才进入真实副作用。HHY 的原生 Path、Bytes、Duration、Regex 和 Effect 模型让这条链不需要退回字符串拼接。