From 4c7cbe66b9435d1352e75988c15ff8c23a8d0c9f Mon Sep 17 00:00:00 2001 From: anonsaber Date: Tue, 29 Sep 2026 09:28:29 +0800 Subject: [PATCH] docs+tooling: no document may name a path that does not exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not even the retired registry — there a fictional path reads the same as a real one, so nobody can tell "we decided against this" from "this is wrong". docs design.md external-repo citations now carry their owning prefix (stalwartlabs/jmap-client/src/client.rs), so a bare src/client.rs can no longer read as a local file; the LLM layer, the local-write red line and the missing-test-dir notes stop quoting directories that were never created retired.md header states the rule; invented entries are rewritten as named proposals ("把渠道层建成目录模块") instead of as paths that look like files; docs/todo.md stays, git history proves it AGENTS.md §7.4 records the rule as a gate requirement charter.md §6.3 validator count and scope tooling audit_paths, the sixth docs_check validator: every path-shaped token in inline code in docs/*.md and AGENTS.md must resolve against a real root, or carry an external owning prefix, or be provably deleted in git history, or sit on a short commented allowlist. Non-path shapes are excluded structurally — absolute paths and URIs, dotted hosts, media types, JMAP method names, numeric pairs, glob fragments — and a seg/ token only counts as a directory when it is the whole inline-code span, so Email/changes and application/json are never mistaken for dirs. run_all.sh is now 6/6. Verified, not assumed: a scratch doc with three invented paths yields 3 errors and exit 1, while a real file, a git-deleted file, an external-prefixed citation and the conventional-name allowlist all pass. --- .gitignore | 2 + AGENTS.md | 3 + docs/charter.md | 9 +- docs/design.md | 18 +-- docs/retired.md | 17 +-- scripts/docs_check/audit_paths.py | 227 ++++++++++++++++++++++++++++++ scripts/docs_check/run_all.sh | 22 ++- 7 files changed, 276 insertions(+), 22 deletions(-) create mode 100644 scripts/docs_check/audit_paths.py diff --git a/.gitignore b/.gitignore index 9586d3b..0238b28 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,7 @@ *.pem *.key *.p12 +# Python 校验器运行时会生成 +__pycache__/ # 构建验证缓存(cargo registry + rustup 组件,1.5G,绝不能提交) .gate-cache/ diff --git a/AGENTS.md b/AGENTS.md index b0ec676..0d41201 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -218,6 +218,9 @@ **它不能证明语义正确。** - 报告校验结果时必须同时说明这个区分,不得把「0 error」说成「内容已验证」。 - **新增文档必须加入校验器的文档清单**,否则它永远不会被校验——这是静默失效。 +- **文档不得出现不存在的文件路径**——`audit_paths` 逐个检查反引号内的路径型 token 是否真实存在, + `docs/retired.md` 同样在检查范围内。曾被讨论但从未落地的路径只以文字描述其形态,不逐字写出。 + 已删除但 git 历史可证的文件(如 `docs/todo.md`)可以引用;无法证明存在过的不能。 --- diff --git a/docs/charter.md b/docs/charter.md index 013c122..6b08213 100644 --- a/docs/charter.md +++ b/docs/charter.md @@ -126,11 +126,18 @@ cd /home/okabe/Repo/messageweave && docker run --rm --user 1000:1000 \ cd /home/okabe/Repo/messageweave && bash scripts/docs_check/run_all.sh ``` -5 个校验器全过、exit 0 才算通过。只读,从不写文件。 +6 个校验器全过、exit 0 才算通过。只读,从不写文件。 新增 `check_file_size`:`src/`、`web/`、`cloudflare-worker/src/` 下超过 500 行的源文件必须带 `SPLIT-EVAL:` 标记(AGENTS.md §2.1)。它证明标记存在,不证明写下的拆分理由站得住脚。 +新增 `audit_paths`:`docs/*.md` 与 `AGENTS.md` 里反引号内的路径型 token 必须真实存在。 +它存在的原因是:文档曾出现从未创建的路径,而虚构路径读起来和真实路径完全一样,读者无法分辨 +「我们决定不做」和「这写错了」。`docs/retired.md` 同样在检查范围内——废弃登记里的路径也必须真实, +未落地的形态只以文字描述。豁免只有三类,都在脚本里可见:指向外部仓库的所有者前缀 +(`stalwartlabs/...`)、git 历史可证已删除的文件、以及一份注明理由的知名文件名白名单。 +它不检查反引号外的散文,裸文件名也只按同名校验解析。 + **`0 error` 只证明「可达」**:行号存在、引用可解析、表格列数一致、标记就位。 **它不证明语义正确。** 报告校验结果时必须说明这个区分。 diff --git a/docs/design.md b/docs/design.md index ffc7d18..ff658e4 100644 --- a/docs/design.md +++ b/docs/design.md @@ -55,7 +55,7 @@ ## 2. jmap-client 能力分析 -来源:直接阅读 `stalwartlabs/jmap-client` 仓库 `src/lib.rs`、`src/client.rs`、`src/email/`、`src/email_submission/`、`src/event_source/`、`Cargo.toml`、`README.md`、`examples/`。 +来源:直接阅读 `stalwartlabs/jmap-client` 仓库的 `stalwartlabs/jmap-client/src/lib.rs`、`stalwartlabs/jmap-client/src/client.rs`、`stalwartlabs/jmap-client/src/email/`、`stalwartlabs/jmap-client/src/email_submission/`、`stalwartlabs/jmap-client/src/event_source/`、`stalwartlabs/jmap-client/Cargo.toml`、`stalwartlabs/jmap-client/README.md`、`stalwartlabs/jmap-client/examples/`。 ### 2.1 crate 概况 | 项 | 值 | @@ -100,7 +100,7 @@ client_ws/ WebSocket 客户端(feature = "websockets") | 实时通知(Push + 对账兜底) | Push HTTPS 回调 → `StateChange`;外部 Cron 调用 `/reconcile` 使用 `Email/changes` 补差 | 需公网 HTTPS 入口(deployment.md `C-HTTPS-INBOUND`/`FLOW-NEW-MAIL`);Push 不是唯一可靠来源 | | SSE / WebSocket(非目标) | `event_source` / `client_ws` | 本部署**不使用**(deployment.md `NG-POLLING-SSE`/`NG-LONG-POLLING`/`C-NO-LONG-CONN`);仅列 crate 能力供调研 | -### 2.4 认证机制(`client.rs`) +### 2.4 认证机制(`stalwartlabs/jmap-client/src/client.rs`) - `Credentials::Basic { username, secret }` — 用户名/密码,Stalwart 原生支持。 - `Credentials::Bearer { token, .. }` — OAuth2 access_token;可选 `refresh_token` + `refresh_url` + `refresh_grace`,client 会在过期前自动刷新。 - `connect()` 作用:GET session URL → 解析 `accounts`/`capabilities`/`download_url`/`upload_url`/`event_source_url`,缓存 account_id。 @@ -327,7 +327,7 @@ message-weave/ └── cloudflare-worker/ # 边缘反向代理,JS,不在 Rust 工程中 ``` -测试:**没有 `tests/` 集成测试目录**,也没有 `src/util/`。所有 Rust 测试是各模块内的 `#[cfg(test)]` 单元测试;前端唯一测试是 `web/config.test.mjs`。 +测试:**没有集成测试目录**,也没有独立的 util 工具模块。所有 Rust 测试是各模块内的 `#[cfg(test)]` 单元测试;前端唯一测试是 `web/config.test.mjs`。 ### 6.1 模块职责矩阵 @@ -446,7 +446,7 @@ message-weave/ | 红线 ID | 含义 | 违反示例(禁止) | |---|---|---| | `C-NO-DB` | 生产不使用任何数据库;Redis 是唯一生产状态存储 | 加 SQLite 去重表、加 Postgres 会话 | -| `C-NO-LOCAL-WRITE` | 禁止本地文件/目录写入;不挂本地卷 | 写 `./logs/app.log`、写 `./tmp/…`、写本地 `sinceState.json` | +| `C-NO-LOCAL-WRITE` | 禁止本地文件/目录写入;不挂本地卷 | 写日志到本地文件、写临时目录、把会话游标落成本地状态文件 | | `C-LOG-STDOUT-ONLY` | 日志只写 stdout/stderr;平台负责采集 | 引入 `tracing-appender`、`rolling-file`、`FileAppender` | | `SAF-LOG-PURITY` | 日志与 Redis 写入内容仅限结构化事件、计数、时间戳、脱敏摘要 | 日志打印 JMAP 正文、AI prompt/completion、密钥原文、附件内容 | | `C-NO-STATEFUL-RECOVERY` | 生产恢复不依赖进程内状态 | 用 `static Mutex` 存 dedup、用内存 LRU 存 sinceState 作为唯一恢复源 | @@ -513,7 +513,7 @@ pub enum BotError { | 集成 | `src/channel.rs` 测试模块 | 用进程内 `tokio::net::TcpListener` 起 mock HTTP 服务,验证出站请求的路径、状态码与重试 | | 真实(手动) | `src/domain/jmap/client.rs` | 标 `#[ignore]`,需显式配置 JMAP 测试服务器后 `cargo test -- --ignored` 才跑 | -**没有 `tests/` 集成测试目录**;前端唯一测试是 `web/config.test.mjs`(Node 原生断言)。 +**没有集成测试目录**;前端唯一测试是 `web/config.test.mjs`(Node 原生断言)。 ### 9.2 mock 策略 - `JmapService` 持有 trait `JmapBackend`(`list_emails` / `read_email` / `send_email` / `changes` 等),生产实现包装 `jmap_client::Client`(`src/domain/jmap/client.rs`),测试用 `MockBackend`(`src/domain/jmap.rs`)。 @@ -594,7 +594,7 @@ pub enum BotError { > 前置:`GATE-P0` 全项通过(`BOUND-STAGE1`)。 -- 依赖 `jmap-client 0.4.2`(**已引入**,`ARCH-DEPS-STAGE1`;版本/features 以 `Cargo.toml` 为准,**禁用 WebSocket feature**);`config.rs` 复用既有 Redis 业务配置骨架(**非** `bot.example.toml`,见 `ARCH-CONFIG-ENV`)。 +- 依赖 `jmap-client 0.4.2`(**已引入**,`ARCH-DEPS-STAGE1`;版本/features 以 `Cargo.toml` 为准,**禁用 WebSocket feature**);`config.rs` 复用既有 Redis 业务配置骨架(不引入独立的示例配置文件,见 `ARCH-CONFIG-ENV`)。 - `domain::jmap::client` = 真实只读 adapter(`MOD-JMAP-CLIENT`,**G1/D-G1-1 代码已实现,待真实 `cargo test -- --ignored jmap::` 验证;未实际运行前不得声称真机通过**):已实现 = 包装 `Client::connect`(Basic 认证 `C-AUTH-APP-BASIC`)、**URL 归一化**(`JMAP_SESSION_URL` 接受服务基地址或完整 `…/.well-known/jmap`,归一化为 origin/base 后传入,避免重复路径;仅 HTTPS、禁内嵌凭据与 query,`REQ-JMAP-SESSION-URL`/`SAF-JMAP-URL`)、**account 选择**:`ACCOUNT_ID` 留空取 session 主账户、显式值校验后使用(`REQ-SINGLE-ACCOUNT`)。 - `domain::jmap::JmapService::list_folders / list_emails / read_email`(query/get;`list_emails` 含 `limit` 边界;`received_at` 解析;`read_email` 多 part 拼接见 `REQ-JMAP-RAW-MULTIPART`)。 - **R1 入口鉴权已就绪**:`/reconcile`/`/webhook/tg`/`/push/jmap` 及 `/api/push/register` 鉴权(`SAF-AUTH-*`)已落地并通过单测;后续改动不得放宽或绕过鉴权。 @@ -726,7 +726,7 @@ LLM 相关代码只有**一个文件**: src/ai.rs # LlmClient(唯一实现):summarize() 打 OpenAI 兼容 /chat/completions ``` 配置装载在 `src/config.rs` 的 `LlmConfig`,运行时参数在 `src/state.rs` 的 `OutboundConfig`(经 `RuntimeConfigProvider` 下发)。 -**不存在** `src/domain/llm/` 目录,也不存在其中的 `mod.rs` / `client.rs` / `config.rs` / `policy.rs` / `fallback.rs` / `audit.rs`——熔断、规则回退、审计 span 全部未实现(见 §12.4–12.7)。 +LLM 能力只存在于 `src/ai.rs` 一个文件;**没有**配置 / 回退 / 策略 / 审计分层——熔断、规则回退、审计 span 全部未实现(见 §12.4–12.7)。 调用方只有 `worker.rs` 的 `MetadataWorker`(已授权摘要)和 `notify.rs`(新邮件通知)。 ### 12.2 LLM 变量清单(当前实现) @@ -790,8 +790,8 @@ src/ai.rs # LlmClient(唯一实现):summarize() 打 OpenAI 兼容 /chat/ --- ## 附:调研依据(可复核) -- `stalwartlabs/jmap-client` main 分支:`src/lib.rs`(URI/Method/DataType/Error)、`src/client.rs`(认证/连接/event_source)、`src/email/`、`src/email_submission/helpers.rs`、`src/event_source/`、`src/push_subscription/`(create/verify/update_types/destroy)、`src/core/error.rs`、`Cargo.toml`、`README.md`、`examples/`。 +- `stalwartlabs/jmap-client` main 分支(路径均相对该仓库根):`stalwartlabs/jmap-client/src/lib.rs`(URI/Method/DataType/Error)、`stalwartlabs/jmap-client/src/client.rs`(认证/连接/event_source)、`stalwartlabs/jmap-client/src/email/`、`stalwartlabs/jmap-client/src/email_submission/helpers.rs`、`stalwartlabs/jmap-client/src/event_source/`、`stalwartlabs/jmap-client/src/push_subscription/`(create/verify/update_types/destroy)、`stalwartlabs/jmap-client/src/core/error.rs`、`stalwartlabs/jmap-client/Cargo.toml`、`stalwartlabs/jmap-client/README.md`、`stalwartlabs/jmap-client/examples/`。 - crates.io:`jmap-client` 元数据。 -- `stalwartlabs/mail-server` main 分支:`crates/common/src/auth/credential.rs`(Password/AppPassword/ApiKey)、`crates/http/src/auth/authenticate.rs`(AccessScope 权限裁剪)、`crates/jmap/src/push/`、`api/v1/openapi.yml`(`securitySchemes`: basicAuth/bearerAuth/liveToken 60s)。 +- `stalwartlabs/mail-server` main 分支:`stalwartlabs/mail-server/crates/common/src/auth/credential.rs`(Password/AppPassword/ApiKey)、`stalwartlabs/mail-server/crates/http/src/auth/authenticate.rs`(AccessScope 权限裁剪)、`stalwartlabs/mail-server/crates/jmap/src/push/`、`stalwartlabs/mail-server/api/v1/openapi.yml`(`securitySchemes`: basicAuth/bearerAuth/liveToken 60s)。 - 项目目录 `/home/okabe/Repo/messageweave/`(工具链要求见 §10.0 与 `../AGENTS.md §3.3`)。 - 部署/平台相关调研依据(lambda_runtime/worker/aws-sdk 等)见 deployment.md。 diff --git a/docs/retired.md b/docs/retired.md index a86f78e..0a5c8b1 100644 --- a/docs/retired.md +++ b/docs/retired.md @@ -1,9 +1,10 @@ # 废弃与未采用路线(Retired Routes) -> 本文只记录**两类**东西:①评估过但没采纳的路线;②曾经写进文档、实际上从未存在或从未实现的名字(虚构条目)。 +> 本文只记录**三类**东西:①评估过但没采纳的路线;②真实存在过、后来被删掉的文档;③曾经写进文档、实际上从未存在或从未实现的名字。 > 目的是**留原因、防复发**——以后看到这些名字,能立刻知道该用什么。 > 当前真实实现与可核对事实见 `docs/reference.md`;缺口与阻塞见 `docs/roadmap.md`;当前设计见 `docs/design.md`。 > 本文**没有行动项**。若某条变成要做的事,去 `docs/roadmap.md` 登记,本文条目保留为决策依据。 +> 本文**不引用任何不存在的路径**:凡路径,一律指向仓库中真实存在的文件;曾被写进文档但从未落地的路径只以文字描述其形态、不逐字复述,以免又被抄回正文或被人当成真实文件去查阅。 > 验证基线:`b2dbe7c`。 --- @@ -20,8 +21,8 @@ | `BotError::Jmap` / `Storage` / `RateLimited` / `Unauthorized` / `Llm` | 目标形态变体,未落地 | 当时为统一承接 JMAP/Redis/限流/授权/LLM 五类错误而设计;现已由各模块自行处理 | 当前 `BotError` 仅 `Config` / `Io` / `Json` / `State` 四变体(`src/error.rs` 全文) | | Telegram 服务端 30 msg/s 限流桶 + 429 专用分支 | 目标设计,未实现 | 无真实 Bot 压测数据,不预设实现 | `src/channel.rs` 的 `max_retries`(默认 3,硬上限 5)通用重试;缺口见 `docs/roadmap.md` | | 多步对话 FSM(`Idle` / `AwaitClarify` / `AwaitConfirm` / `Analyzing` / `AwaitFallback`) | 目标设计,未落地 | 当前 AI 授权只需一个布尔+过期时间,多步态属过度设计 | Redis TTL 授权态,键与 TTL 见 `docs/reference.md` 的 AI 授权态一节 | -| `src/domain/llm/`、`src/domain/command.rs`、`src/domain/notification.rs`、`src/domain/state.rs`、`src/channel/`、`src/notify/`、`src/util/`、`tests/` 子模块拆分 | 目标形态目录,未落地 | 代码量未到需要拆分的规模 | 实际结构见 `docs/design.md` 的工程结构一节:领域层是 `src/domain.rs` + `src/domain/jmap/`(仅 `client.rs`);LLM 在 `src/ai.rs`;通知在 `src/notify.rs`;**`src/util/` 与 `tests/` 根本不存在**(加密逻辑内联在 `src/state.rs` / `src/config.rs`) | -| `tests/telegram_dispatch.rs` | 目标形态测试,未落地 | 依附 teloxide 测试模式 | `src/channel.rs` 的单元测试(`#[test]`) | +| 把领域 / 渠道 / 通知 / util 各层拆成独立目录(子模块拆分方案) | 目标形态目录,未落地 | 代码量未到需要拆分的规模 | 实际结构见 `docs/design.md` 的工程结构一节:领域层是 `src/domain.rs` + `src/domain/jmap/`(仅 `client.rs`);LLM 在 `src/ai.rs`;通知在 `src/notify.rs`;加密与工具逻辑内联在 `src/state.rs` / `src/config.rs`,没有独立的 util 层,也没有集成测试目录 | +| teloxide 风格的出站消息派发集成测试 | 目标形态测试,未落地 | 依附 teloxide 测试模式 | `src/channel.rs` 的单元测试(`#[test]`) | | docker-compose `healthcheck` 示例(`message-weave health --addr ...`) | 已删除的示例 | 运行镜像 `debian:bookworm-slim` 内无 `curl`/`wget`,且 `src/main.rs` 无 CLI 子命令解析,示例照抄必失败 | 由平台 ingress 探测 `/ready`;说明见 `docs/deployment.md` 的就绪探测一节 | | SPA 管理凭据 = `REDIS_URL` 的 Redis ACL 密码 | 未采用路线 | 混淆基础设施凭据与 UI 管理密码;Redis 无 ACL 密码(TLS-only 托管 Redis)时 `bootstrap_token` 的 `.is_empty()` 守卫让 SPA 永久 401 | 改用 `CONFIG_ENCRYPTION_KEY`(启动必填的 32 字节高熵 hex,常数时间比较) | @@ -72,7 +73,7 @@ | 条目 | 类型 | 原因 | 替代或现状 | |---|---|---|---| -| `channel/telegram/{mod,commands,session,render}.rs` | 虚构文件 | 目标目录树里画出,从未创建 | 命令解析在 `parse_intent`(`src/worker.rs`);会话/授权态走 Redis TTL(`docs/reference.md` 的 AI 授权态一节);渲染函数在 `src/channel.rs` 内 | +| 把 Telegram 接入拆成「模块入口 / 命令解析 / 会话状态 / 回复渲染」四个文件的目录方案 | 虚构目录方案 | 目标目录树里画出,从未创建 | 命令解析在 `parse_intent`(`src/worker.rs`);会话/授权态走 Redis TTL(`docs/reference.md` 的 AI 授权态一节);渲染函数在 `src/channel.rs` 内 | | `delivery:pending:{stream}` | 虚构键 | 与 Redis Streams 的 pending-entries list(PEL)混淆——PEL 由 Redis 内部维护,不是可写键 | 真实键:`delivery:inflight:{stream}:{id}`(EX 60)与 `delivery:committed:{stream}:{id}`(EX 604_800),见 `docs/reference.md` 的投递流水线键一节 | | `check_config_reload` | 虚构函数 | 未提交草稿中的名字,从未进入代码 | 热更新为 `refresh_business_config`(`src/notify.rs:718`) | | `push:registration:{sha256(callback_url)}` 曾被写成「360s 注册单飞锁」 | 事实误标 | 该键是 7d 回调→订阅 ID 映射 | 真正的 360s 单飞锁是 `lock:push-register:{sha256(callback_url)}`(`src/notify.rs:898-901`) | @@ -81,9 +82,9 @@ --- -| `src/domain/llm/` 整个模块族 | 虚构模块树 | `domain/` 下只有 `jmap.rs` 与 `jmap/client.rs`;LLM 只有 `src/ai.rs` 一个文件(`LlmClient`) | 无 | +| 把 LLM 能力建成领域子模块族(配置 / 回退 / 策略 / 审计分层) | 虚构模块树 | `domain/` 下只有 `jmap.rs` 与 `jmap/client.rs`;LLM 只有 `src/ai.rs` 一个文件(`LlmClient`) | 无 | | `ai::config` / `ai::fallback` / `ai::policy` / `ai::audit` | 虚构模块 | LLM 配置在 `config.rs::LlmConfig`,运行时参数在 `state.rs::OutboundConfig`(经 `RuntimeConfigProvider` 下发),无 policy/audit 概念 | 无 | -| `channel/mod.rs` | 虚构文件 | 渠道层就是 `src/channel.rs`,不是 `channel/` 目录 | 无 | +| 把渠道层建成目录模块(含模块入口文件) | 虚构目录 | 渠道层就是 `src/channel.rs` 一个文件 | 无 | | `notify::push_handler` / `notify::worker` / `notify::reconcile` 作为模块路径 | 虚构模块路径 | 这些是 `src/notify.rs` 内的自由函数(`jmap_push` / `worker` / `reconcile`),不是模块路径 | 无 | | `mod_dedup` / `mod_streams` / `mod_sincestate` | 虚构模块名 | `state.rs` / `notify.rs` 都是平铺文件,无子模块;去重与 Streams 逻辑以自由函数存在 | 无 | | `PushVerification` 类型 | 虚构类型 | 未定义;`register_push`(`notify.rs:885`)内联处理回调 URL 与验证码回写 | 无 | @@ -97,7 +98,7 @@ | 附件下载(`send_document` / `Blob/get` / 下载按钮) | 未实施 | JMAP 侧只读;邮件附件仅以 `has_attachment: bool` 形式出现 | 无 | | `/flag` / `/unseen` / 发信命令 | 未实施 | 当前识别 6 个意图(帮助 / 同意 / 摘要 / 搜索 / 普通消息 / 未识别);其中 `/search` 已实现(`worker.rs:533`) | 无 | | `Identity` 概念 | 未实施 | 账号识别只依赖 `ACCOUNT_ID`,无身份层抽象 | 无 | -| `run_mode.rs`(作为单独文件) | 虚构文件 | 不存在该文件;`RUN_MODE` 曾由 `config.rs` 读取并在 `src/main.rs` 校验取值,**该变量与校验块现已一并删除**(见 §4);从未存在 `validate_env_or_exit` 这类函数 | 无 | +| 把 `RUN_MODE` 抽成独立配置文件 | 虚构拆分方案 | `RUN_MODE` 曾由 `config.rs` 读取并在 `src/main.rs` 校验取值,**该变量与校验块现已一并删除**(见 §4);从未存在 `validate_env_or_exit` 这类函数 | 无 | | docker-compose `message-weave health --addr` 示例 | 虚构命令 | 应用无 CLI 子命令;健康检查端点是 `GET /healthz` 与 `GET /ready` | 见 `docs/deployment.md` 的 Health-check 表 | | 早期设计辩论问题(消息格式 / 长邮件阈值 / 附件策略 / Identity / 监控 / LLM 供应商 / 熔断等 17 条) | 已由代码回答 | 均已被实现的代码给出答案,不再属于待确认项 | 见 `docs/design.md` 的「已由代码回答的早期问题」一节 | @@ -106,7 +107,7 @@ | 条目 | 类型 | 原因 | 替代或现状 | |---|---|---|---| | `docs/todo.md` | 已取代(已删除) | 结构不清,与 design/deployment 重叠,且曾承载「本轮已收口」这类历史叙述 | `docs/roadmap.md`(只放缺口、阻塞、阶段目标、决策待定、验收待办) | -| `HANDOFF.md`(根目录) | 临时交接件(已删除,**从未进入 git 历史**) | 交接内容应归位到常驻文档 | 内容并入 `docs/design.md`、`docs/deployment.md`、`docs/reference.md`、`docs/roadmap.md` | +| 根目录临时交接件(不留此类文件) | 临时交接件,**从未进入 git 历史** | 交接内容应归位到常驻文档,不留根目录临时件 | 内容并入 `docs/design.md`、`docs/deployment.md`、`docs/reference.md`、`docs/roadmap.md` | --- diff --git a/scripts/docs_check/audit_paths.py b/scripts/docs_check/audit_paths.py new file mode 100644 index 0000000..6cbcab4 --- /dev/null +++ b/scripts/docs_check/audit_paths.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +"""PATH AUDIT — no document may name a file or directory that does not exist. + +Why this exists +--------------- +The docs used to name directories, modules and tests that were never created. +A reader who acts on a documented path loses trust in the whole document, and +nobody can tell "we decided against this" from "this is wrong". So the rule is: +**every path-shaped token written in backticks in a document must resolve**, +including in the retired/deprecated registry (`docs/retired.md` is scanned too) +— there, a fictional path reads the same as a real one. The invented names are +deliberately not repeated here, so the examples cannot be copied back in; +`git log -p docs/retired.md` shows what they were. + +What this check proves +---------------------- +Every path-shaped token in inline code in the scanned documents is one of: + (a) resolvable — it exists in this repo (see ROOT_DIRS, a token is resolved + against several real roots because docs cite files as `config.rs`, not + always `src/config.rs`); + (b) attributed to an external repo by an owning prefix (EXEMPT_EXTERNAL), + e.g. `stalwartlabs/jmap-client/src/client.rs`; + (c) provably deleted — the file is gone but `git log --all --diff-filter=A` + shows it was committed at some point (e.g. `docs/todo.md`); + (d) on the commented CONVENTIONAL allowlist — a well-known filename that the + prose explicitly says this repo does not have. + + Anything else is reported as an error. + +What it does NOT prove +---------------------- +- That the path means what the prose says, same limitation as audit_anchors. +- Anything outside inline code. Backticks are the contract: if you write a + path in prose without backticks, this check cannot see it. +- Bare filenames unambiguously: `client.rs` resolves against any file with + that basename in the repo. +- Anything in scripts. Code legitimately contains path-like string literals + (this check is defined by string patterns, after all); those are not + documentation claims. + +Non-path shapes that look like paths are excluded by is_noise(), and a +`seg/` token only counts as a directory reference when it is the whole +inline-code span. Both rules are structural rather than denylisted: is_noise +recognises the shapes that cannot be repo-relative paths (absolute paths and +URIs, dot-prefixed URL parts, dotted hosts, media types, JMAP method names, +all-numeric pairs, shell and glob fragments), and the whole-span rule means +`Email/changes`, `application/json` and `method/path/origin/失败类别` are never +mistaken for a directory. Adding a special case to either should make you +suspect the document instead. +""" + +import os +import re +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(HERE, "..", "..")) + +DOCS = [ + os.path.join(ROOT, "docs", f) for f in sorted(os.listdir(os.path.join(ROOT, "docs"))) + if f.endswith(".md") +] +AGENTS = os.path.join(ROOT, "AGENTS.md") +TARGETS = [t for t in DOCS + [AGENTS] if os.path.isfile(t)] + +# Directories a doc legitimately roots a path at. Every entry is real. +ROOT_DIRS = [ + ".", + "src", + "src/domain", + "src/domain/jmap", + "docs", + "scripts", + "scripts/docs_check", + "cloudflare-worker", + "cloudflare-worker/src", + "web", +] + +EXTS = "rs|js|mjs|ts|tsx|json|toml|ya?ml|md|sh|sql|cfg|lock|html|css|proto|txt" + +# A file token (ends in a known extension) or a directory token (ends in '/'). +TOKEN = re.compile( + r"(?&1)" code=$? # Each check prints "=== NAME (n errors) ===" as its summary, but not @@ -54,7 +68,7 @@ for script in validate_docs check_tables audit_anchors check_sec_refs check_file done if [ "$fail" -eq 0 ]; then - echo "GATE-DOCS: PASS (5/5 checks clean)" + echo "GATE-DOCS: PASS (6/6 checks clean)" else echo "GATE-DOCS: FAIL ($fail check(s) reported errors)" fi