feat(ql3): inventory legacy data directory

This commit is contained in:
whyour
2026-08-21 01:34:18 +08:00
parent c9e41812cb
commit 878a360b09
9 changed files with 1329 additions and 3 deletions
+15
View File
@@ -11,6 +11,21 @@
最新增量证据(2026-08-21):
- D-384/ADR-0477(已接受):完整 2.x data directory 接管先落地为一次性、只读、有界 inventory,而不是直接复用 legacy shell `tar`
或盲拷整个目录。既有 `ql3-adoption` 新增 exact 私有命令 `local-data-directory.adoption.inspect`,固定分类
`config/db/ssh.d→transform``scripts/upload→copy_reviewed``log/syslog/bak→retain_external(root-only)`
`repo/raw/dep_cache/deps→regenerate(root-only)`;未知顶层条目只返回数量/摘要并转 `manual_review`。递归类别按 UTF-8 字节序、
`lstat`/no-follow 和 stable descriptor 流式哈希;symlink、硬链接、特殊文件、错误 owner 与 group/world writable 条目不读取且计为
unsafe,底层错误统一脱敏。Edge 限制 8192 项、512 MiB 总哈希、64 MiB 单文件、32 层,Standalone 为 65536 项、4 GiB、
512 MiB、64 层;目录用增量 `opendir` 在保存超预算名称前失败,文件只用 64 KiB 缓冲。该 operation 不复制、转换、归档或写源目录,
也尚未绑定 D-383 SQLite activationD-385 才设计双 digest fence 的 stage/verify。没有新增 package、dependency、binary、daemon、
listener、timer、数据库连接或部署对象;Local Owner 新代码内聚在 `lifecycle/data-directory-adoption/`workspace 保持 18 packages、
`singleSourcePackages=[]``shallowSourcePackages=[]``118 source / 117 nested / 1 root binary entry`。D-384 focused `5/5`
Local Owner `195 total / 190 pass / 5 conditional skip / 0 fail`backend `1,535 total / 1,533 pass / 2 conditional skip /
0 fail``pnpm build:back` 与 18-package clean build/逐包测试通过;八项架构审计与 14 档 artifact audit 全 compatible,基础
Edge/Standalone、Adopted、Application+AI 和 MCP 制品体积/模块数均保持 D-383 基线,证明盘点 authority 未进入低配常驻闭包。
本切片不改变 PostgreSQL 语义,故不重跑且不重新占有 HA 证明。固定物理 Edge 的盘点/staging RSS、I/O、磁盘峰值、断电恢复,
以及 config/Keyv/SSH 转换和 systemd/OpenRC/Compose lineage 仍待后续完成。
- D-383/ADR-0476(已接受):把单个 2.x SQLite 主库接管从分散 API/合成 fixture 推进为产品级真实双态演练。既有一次性
`ql3-adoption` 新增 exact、私有 command-file 的 `inspect → stage → verify → activation`,从生产形态 Sequelize schema
Cron、Dependency、App、Auth、Env、Subscription、View、Stats、RunningInstance 与未知 Plugin-owned table)生成独立 recovery
@@ -0,0 +1,140 @@
# ADR-0477:有界 Legacy Data Directory 盘点
- 状态:Accepted
- 日期:2026-08-21
- 关联:QL-RFC-0001、ADR-0476
## 上下文
ADR-0476 已经证明单个生产形态 QingLong 2.x `database.sqlite` 可以经过 inspect、stage、verify、activation 和 clean/write-after
双态回滚分类接管到 3.0,但真实部署的 `data` 目录不只有主数据库。现行 2.x 配置和实际用户目录可包含:
- `config/``scripts/``db/``upload/``ssh.d/`
- `log/``syslog/``bak/`
- `repo/``raw/``dep_cache/`、历史 `deps/`
- 用户或插件创建的未知顶层条目。
直接复用 2.x `SystemService.exportData/importData` 不满足 3.0 接管要求。该路径以 shell 拼接 `tar`,默认只覆盖数据库和上传目录,
也没有固定资产分类、no-follow、硬链接拒绝、内容上限、确定性计划或敏感输出约束。直接打包整个 `data` 目录还会把日志、备份、
跨架构依赖缓存、仓库 checkout、SSH 材料和未知插件资产混成一个不可审核恢复单元;对低内存路由设备尤其危险。
完整目录复制之前,需要一个短生命周期、只读、确定性、按 Profile 有界的产品入口先回答:哪些资产存在、哪些可进入后续复制、哪些必须
转换、哪些只保留为外部恢复资产、哪些应在 3.0 重新生成,以及是否存在必须人工处理的不安全或未知条目。
## 决策
### 1. 复用现有一次性产品入口
`@qinglong/local-owner-cli``lifecycle/data-directory-adoption/` 内增加:
- exact operation`local-data-directory.adoption.inspect`
- exact options`dataRoot``profile`
- 既有 `ql3-adoption run --command-file ...` 私有 command-file 入口。
不新增 workspace package、第三方依赖、binary、daemon、listener、watcher、timer、数据库连接或部署对象。实现不得进入 Edge、
Standalone、Application、AI 或 MCP 常驻闭包。
### 2. 固定资产处置矩阵
| 类别 | 处置 | 盘点深度 | 原因 |
| --- | --- | --- | --- |
| `config` | `transform` | `recursive_content` | 需要迁移到 3.0 配置模型,不能盲拷旧配置 |
| `scripts` | `copy_reviewed` | `recursive_content` | 用户脚本是业务资产,但必须先审核安全和兼容性 |
| `db` | `transform` | `recursive_content` | 主库由 ADR-0476 迁移,Keyv/sidecar 需独立识别 |
| `upload` | `copy_reviewed` | `recursive_content` | 用户上传内容可保留,但必须受大小和文件类型边界约束 |
| `ssh.d` | `transform` | `recursive_content` | 属于敏感凭据材料,后续必须进入专用私有交付协议 |
| `log``syslog``bak` | `retain_external` | `root_only` | 作为历史/恢复资产保留,不进入默认 3.0 运行目录 |
| `repo``raw``dep_cache``deps` | `regenerate` | `root_only` | checkout、原始缓存和依赖缓存应按目标架构重建 |
未知顶层条目只记录数量和名称摘要,不返回原始名称,计划状态固定为 `manual_review`。本阶段不允许 caller 覆盖处置矩阵或增加任意
include/exclude glob。
### 3. No-follow、稳定身份与内容脱敏
盘点要求 `dataRoot` 为当前 UID 拥有、canonical、非符号链接、group/world 不可写的非根目录。递归类别按 UTF-8 字节序确定性遍历,
使用 `lstat` 且不跟随符号链接;只有当前 UID、group/world 不可写的普通单链接文件或目录可继续读取。符号链接、硬链接、多链接文件、
特殊文件、错误 owner 和可被组/其他用户写入的条目只计为 unsafe,不读取其内容。
普通文件通过 `O_NOFOLLOW` descriptor 读取,打开前后的 device、inode、mode、link count、UID、size、mtime 和 ctime 必须一致。
目录遍历前后也必须保持同一稳定身份。底层文件系统错误统一映射为固定错误码
`LOCAL_DATA_DIRECTORY_ADOPTION_CONFIGURATION_INVALID`,CLI 不返回原始路径、任意文件名或内容。
结果只包含固定类别名、计数、逻辑/分配字节、宽读权限计数、unsafe 计数、SQLite 主库/Keyv/sidecar 识别计数、内容摘要、未知条目
数量/摘要和完整 `planDigest`。宽读权限是审核信号;只有可写权限或身份/类型问题自动成为 unsafe。
### 4. Edge 与 Standalone 分离预算
| Profile | 最大条目 | 最大哈希字节 | 单文件上限 | 最大深度 |
| --- | ---: | ---: | ---: | ---: |
| Edge | 8,192 | 512 MiB | 64 MiB | 32 |
| Standalone | 65,536 | 4 GiB | 512 MiB | 64 |
目录通过增量 `opendir` 枚举,并在保存超过剩余预算的名称前失败,不先用一次性 `readdir` 将任意数量的目录项装入内存。文件使用
64 KiB 固定缓冲区流式哈希并在关闭前清零。`root_only` 类别不递归、不读取或哈希其子项,因此依赖缓存和历史日志规模不会进入
盘点内存或 I/O 成本。
### 5. 本阶段只发布计划,不执行迁移
该 operation 只读源目录并将结果写到 stdout。它不创建目录副本、归档、manifest、recovery 或 activation,不修改 2.x 数据,
也不把目录计划自动绑定到 ADR-0476 的 SQLite plan/activation。后续 stage 必须重新验证稳定身份并显式绑定两个计划摘要;不能把本次
inspect 输出直接当作复制授权。
## 被拒绝的替代方案
### 直接 tar 完整 data directory
拒绝。它混合不同恢复语义、可能跟随或保存不安全链接、复制跨架构缓存,并让低配设备承担不可预测的空间和内存成本。
### 为目录接管再拆一个 workspace package
拒绝。该能力只有一个短生命周期产品 owner,没有独立部署、依赖或版本生命周期;放入已有 Local Owner lifecycle 垂直目录更符合
当前包边界规则,也避免恢复“一个文件一个包”的碎片化。
### 只统计文件大小,不读取内容摘要
拒绝。大小和时间不能把后续 stage 绑定到已审核内容;确定性流式摘要提供最小的漂移证明,同时不输出文件内容。
### 在 inspect 时复制或转换文件
拒绝。盘点和 mutation 混合会让未知/不安全条目在 operator 审核前产生目标副本,也无法建立清晰的 plan-digest fence。
## 影响
### 正面
- 完整 2.x data directory 首次获得固定、可审核的资产处置模型;
- Edge 和 Standalone 使用不同硬预算,低配设备不会继承集群节点规模假设;
- 未知资产、链接和权限漂移失败关闭,且不会泄露任意文件名或内容;
- 日志、备份、仓库和依赖缓存不会污染 3.0 默认运行目录;
- 没有增加 package 粒度、常驻资源或基础制品体积。
### 代价与限制
- 对递归类别执行全内容哈希,仍会产生与资产大小线性的磁盘读取;
- 正在写入的 2.x 目录可能因稳定身份检查失败,需要先停止 writer 后重试;
- `root_only` 只证明类别根的存在、权限和类型,不证明内部历史资产完整性;
- 当前没有 stage/verify/restore,也没有固定物理 Edge 的耗时、RSS、磁盘峰值与断电演练;
- 当前目录计划尚未与 SQLite activation、service-manager cutover 和 rollback lineage 形成统一证据链。
## 验证
- D-384 聚焦 data directory CLI`5/5`
- Local Owner`195 total / 190 pass / 5 conditional skip / 0 fail`
- backend`1,535 total / 1,533 pass / 2 conditional skip / 0 fail``pnpm build:back` 通过;
- 18-package clean build 与逐包测试单次退出 0;
- package boundary、Cluster dependency、Edge import、Service Bridge import、Cluster/Worker deployment、Console 与 Console
distribution 八项审计全部 compatible/passed
- workspace 保持 18 packages`singleSourcePackages=[]``shallowSourcePackages=[]`Local Owner 为
`118 source / 117 nested / 1 root binary entry`
- 14 档 Local artifact audit 全部 compatible,基础 Edge/Standalone、Adopted、Application+AI 与 MCP 的体积和 loaded-module
基线未变化。
本阶段不修改 PostgreSQL schema、ACL、repository、role、Pool、连接或 failover 语义,因此不重跑且不重新占有 PostgreSQL HA
证明。
## 后续
- D-385:以 plan digest 和 ADR-0476 activation digest 为双 fence,设计 no-replace stage/verify manifest
-`config`、Keyv 与 `ssh.d` 定义显式转换/私有交付协议;
- 在固定物理 Edge/NAS 上测量完整目录盘点与 staging 的耗时、RSS、I/O、磁盘峰值和断电恢复;
- 把 staged data directory 证据接入 systemd/OpenRC/Compose cutover 与 rollback lineage。
+1
View File
@@ -480,6 +480,7 @@
| [ADR-0474](./ADR-0474-bounded-legacy-core-readiness-proof.md) | 有界 Legacy Core Readiness Proof | AcceptedOpenRC live actor 待补) |
| [ADR-0475](./ADR-0475-legacy-system-script-open-api-compatibility.md) | Legacy System、Script 与 Open API 兼容基线 | Accepted |
| [ADR-0476](./ADR-0476-real-legacy-sqlite-upgrade-and-rollback-rehearsal.md) | 真实 Legacy SQLite 升级与回滚演练 | Accepted |
| [ADR-0477](./ADR-0477-bounded-legacy-data-directory-inventory.md) | 有界 Legacy Data Directory 盘点 | Accepted |
## 规则
@@ -0,0 +1,103 @@
# QingLong 2.x Data Directory 盘点
本流程为完整 QingLong 2.x `data` 目录生成一个只读、确定性、按 Profile 有界的 3.0 接管计划。它不会复制、压缩、删除或修改
任何文件,也不替代 [SQLite 接管流程](./ql3-local-sqlite-adoption.md)。
## 1. 前置条件
- 使用最终运行 QingLong 的同一个 POSIX 用户执行;
- `dataRoot` 必须是绝对、canonical、非根、非 symlink 的目录;
- `dataRoot` 必须由当前 UID 拥有,且 group/world 不可写;
- command file 继续遵守 `ql3-adoption` 的当前 UID、canonical、单链接、`0600` 私有文件要求;
- 生产盘点建议先停止 2.x writer。若文件或目录在盘点中变化,命令会失败关闭,不会给出部分成功计划。
## 2. 执行 inspect
Edge/低配路由设备:
```json
{
"schemaVersion": 1,
"operation": "local-data-directory.adoption.inspect",
"options": {
"dataRoot": "/opt/qinglong/data",
"profile": "edge"
}
}
```
Standalone/NAS
```json
{
"schemaVersion": 1,
"operation": "local-data-directory.adoption.inspect",
"options": {
"dataRoot": "/opt/qinglong/data",
"profile": "standalone"
}
}
```
```sh
chmod 0600 /secure/operator/ql3-data-directory-inspect.json
ql3-adoption run --command-file /secure/operator/ql3-data-directory-inspect.json
```
命令成功时返回 `status=inspected``qinglong3-legacy-data-directory-adoption-plan`。保存完整 JSON 和 `planDigest`,但不要把它
当作已经授权复制的 manifest。
## 3. 审核处置矩阵
| 类别 | 默认处置 | 说明 |
| --- | --- | --- |
| `config` | `transform` | 迁移到 3.0 配置模型,不原样覆盖 |
| `scripts` | `copy_reviewed` | 审核兼容性和安全后复制 |
| `db` | `transform` | 主 SQLite 走独立流程;审核 Keyv 和 sidecar |
| `upload` | `copy_reviewed` | 审核文件类型与容量后复制 |
| `ssh.d` | `transform` | 进入后续私有凭据交付,不进入普通归档 |
| `log``syslog``bak` | `retain_external` | 作为历史/恢复资产另行保留 |
| `repo``raw``dep_cache``deps` | `regenerate` | 在目标架构重新 checkout/安装 |
`recursive_content` 类别会稳定读取普通单链接文件并生成摘要;`root_only` 类别只检查类别根,不扫描内部内容。
## 4. 解释 assessment
- `reviewable`:没有未知顶层条目,也没有 unsafe 条目;仍需 operator 审核分类、数量、字节和数据库识别计数;
- `manual_review`:出现未知顶层条目或 unsafe 条目;不得继续自动 staging;
- `broadReadableEntries > 0`:存在 group/world 可读条目,是敏感性审核信号,但不等同于可被外部修改;
- `activeSqliteSidecars > 0`:发现 `database.sqlite-*``keyv.sqlite-*` 活跃 sidecar,先停止 writer 并完成 SQLite
checkpoint/一致性处置后重新盘点;
- `primaryDatabaseFiles` 应按生产布局识别 `db/database.sqlite``legacyKeyValueDatabaseFiles` 识别 `db/keyv.sqlite`
输出不会包含 `dataRoot` 原文、任意文件名或文件内容。未知名称只进入摘要。不要尝试从摘要反推或把摘要当作内容备份。
## 5. Profile 预算
| Profile | 最大条目 | 最大哈希字节 | 单文件上限 | 最大深度 |
| --- | ---: | ---: | ---: | ---: |
| Edge | 8,192 | 512 MiB | 64 MiB | 32 |
| Standalone | 65,536 | 4 GiB | 512 MiB | 64 |
超过任一限制都会以 `LOCAL_DATA_DIRECTORY_ADOPTION_CONFIGURATION_INVALID` 失败。不要为了通过门禁临时改名、删除或排除资产;先保留
现场并决定它应拆分为外部恢复资产、在目标重新生成,还是进入后续人工迁移协议。
## 6. 常见失败
- 根目录或条目 group/world 可写:修正 ownership/permission 后重新盘点;
- symlink、硬链接或特殊文件:保留现场,确认来源和目标后人工处置;盘点不会跟随或读取;
- 目录在盘点中变化:停止 2.x writer、同步器、下载器和仓库更新后重试;
- 单文件或总内容超过 Profile 预算:不要改用 `tar` 绕过;为该资产设计独立流式迁移/外部保留流程;
- 未知顶层条目:根据插件或用户资产来源登记明确处置,再进入后续 staging 设计。
## 7. 当前边界
本命令只生成只读计划。它尚不:
- 复制、压缩、删除或转换任何资产;
- 创建 no-replace stage、verify manifest 或 recovery
- 把目录计划绑定到 SQLite `planDigest``manifestDigest``activationDigest`
- 授权 service-manager/Compose cutover 或 Legacy rollback
- 证明固定物理路由器/NAS 上的耗时、RSS、I/O、磁盘峰值和断电恢复。
在后续 D-385 stage/verify 合同完成前,保留 inspect 输出和原始 2.x data directory,不要把目录计划当作自动迁移完成证明。