Commit Graph

21 Commits

Author SHA1 Message Date
anonsaber 7aedcd4145 docs: merge deployment.md into README, delete deployment files
- Move operational content (env var reference, multi-instance prerequisites,
  troubleshooting, cron-drain, CF Dashboard Git integration, BACKEND_ORIGINS_JSON
  format, security checklist) into README and README.zh-CN
- Delete docs/deployment.md and docs/deployment.zh-CN.md
- Update 5 validator DOCS lists to remove deployment entries
- Update charter §7 doc boundary table, AGENTS §2.5 line-anchor exception
- Fix cross-doc references in design/reference/opengaps/retired/worker READMEs
- Fix cron-drain.sh header comment reference (deployment.md §6.3 → README §3.10)
- GATE-DOCS 6/6 PASS (55 anchors / 441 sec-refs / 699 path-refs, 0 errors)
2026-10-10 18:53:43 +08:00
anonsaber d5403d8b5b feat(cron): add --app-domain/--worker-domain/--debug-domain CLI flags, auto-prepend https://
Bare domains like messageweave.example work without the https:// prefix.
Old --*-url flags and MW_*_URL env vars still work as aliases.
Flag values override env vars; defaults applied after parsing.
2026-10-10 18:12:31 +08:00
anonsaber 29178cb093 docs: fold quickstart into README, rename gateway→load balancer
Two changes requested by the user:

1. Fold the hand-holding quickstart tutorial into README §3 (both
   languages). Delete docs/quickstart.md and docs/quickstart.zh-CN.md.
   Remove them from all 5 validator DOCS lists, README §6 doc index,
   and charter §7 boundary table. The tutorial now lives as
   README §3.0–§3.11: prerequisites, create Telegram bot, find chat id,
   get JMAP credentials, run backend, sign in, fill config, register
   callbacks, enable processing, verify, schedule drain, troubleshoot.

2. Replace 'gateway'/'网关' with 'load balancer'/'负载均衡器' across
   all markdown (README, docs/, AGENTS.md, cloudflare-worker/README).
   Also fix 4 markdown anchor links whose heading slugs changed.
   Also fix charter §8 '18 safe routes' → '19' (was missed in the
   earlier reference.md fix).

GATE-DOCS 6/6 PASS (71 anchors, 727 path-refs, 455 sec-refs).
2026-10-08 21:35:14 +08:00
anonsaber 7ed1a086fe docs: add hand-holding quickstart tutorial (EN + ZH)
New docs/quickstart.md and docs/quickstart.zh-CN.md walk a human operator from
zero to the first notification: create a Telegram bot via BotFather, find the
chat id, get a Stalwart app password, provision Redis, deploy the container,
sign in with CONFIG_ENCRYPTION_KEY, fill the business config, register
callbacks, enable processing, verify end-to-end, and schedule the cron drain.

Every claim was verified against the code:
- healthz is static 200 'ok'; /ready returns 503 until all upstreams
  reachable, 200 with per-component booleans
- admin session TTL = 1800s (30 min) in state.rs
- first save must be complete -> 422 (is_complete in config.rs)
- 409 conflict on revision mismatch
- secrets are write-only (Option<SecretString>, never echoed)
- /worker and /reconcile return 204 on success, 503 when disabled
- webhook secret mismatch returns 401 (not 403)
- search triggers: 搜索/查找/检索 + /search command only
- cron-drain.sh uses MW_APP_URL (req), MW_WORKER_TOKEN (req),
  MW_RECONCILE_TOKEN (opt)

Registered both files in all 5 validator DOCS lists (audit_paths globs
automatically). Added rows to README §6 and charter §7 in both languages.
Added discoverable links from README §3 to the quickstart.

GATE-DOCS 6/6 PASS (71 anchors, 735 path-refs).
2026-10-08 17:35:35 +08:00
anonsaber 42128433ed docs: add the worker bilingual README and name both deployment forms
Root README gains §2 "Two ways to run it": a side-by-side table of
Form A (backend origin only) against Form B (behind the Cloudflare Worker
gateway) — what answers, failover, the callback URL to register, the
reachable route surface, which health endpoint to watch, and the docs to
read. Late in the switch: registration is idempotent and re-adding retires
the previous subscription, so the switch is one SPA field, not a migration;
nothing secret moves to the gateway; /api/bootstrap and /debug/* stay
origin-only either way. Later sections renumbered, and the monitoring
paragraph now points at the gateway aggregate GET /healthz-worker instead
of the origin-passthrough /healthz.

cloudflare-worker/README.md rewritten as the canonical English version and
cloudflare-worker/README.zh-CN.md added as its translation, covering the
route safelist (19 paths with per-path methods), the timeout/retry budget
with per-route overrides, bounded failover, the two health endpoints,
callback registration, config, local verification, deploy, and the design
invariants.

Both fixes along the way:

- README.zh-CN said the startup env block has 2 required + 2 optional
  variables; the table has 3 rows, 2 required + 1 optional.
- docs/charter.md §8 / registry list now names the zh-CN twin next to the
  gateway README.

scripts/docs_check gains the new file in validate_docs, check_tables,
audit_anchors and audit_anchor_semantics so it is checked like every other
document. check_sec_refs resolves §-refs against the document being scanned
when a basename is shared: README.md exists both at the repo root and under
cloudflare-worker/, and the basename-keyed heading map silently dropped one
of them. The gate was passing for the wrong reason before.

GATE-DOCS 6/6 clean: 72 anchors, 454 section refs, 727 path refs.
audit_anchor_semantics still reports the same 7 pre-existing candidates,
none in the new or edited files.
2026-10-01 13:40:39 +08:00
anonsaber 7a27f7ff00 docs: add bilingual guides and callback setup in SPA 2026-10-01 02:36:03 +08:00
anonsaber f9889288ba docs: 同步 /worker 进 safelist(17 -> 18 条),cron 示例收敛到单一域名
- deployment.md:§6.3.1 两条端点都可直接打 Worker 域名,手动 curl 与 crontab、
  k8s CronJob 示例去掉 WORKER_ORIGIN 只留 APP_URL,-m 分别给 320s / 300s(要略大于
  LB 侧 per-route 超时,否则 curl 先掐掉一个本来能完成的排空);§10.4 safelist
  18 条并补 /worker、不在 safelist 的从 3 类减为 2 类;LB_WORKER_TIMEOUT_MS 补进
  secret put 清单与 §10.9 变量表;测试计数 35 -> 37(index 12 -> 14)
- README.md:路由白名单 17 -> 18 条、/worker 入列、LB_WORKER_TIMEOUT_MS 补进
  变量表与配置片段、/reconcile 例外段落扩成 /reconcile + /worker 两条长超时
- charter.md / reference.md:17 -> 18 条、Safe Routes 16 -> 18 entries
- cron-drain.sh:MW_WORKER_URL 由「必须指源站直连地址」改为「可选,默认走平台域名」;
  404 route not forwarded 的提示改为 LB 版本太旧(需 >= 2026.10.3),升级或绕过 LB

顺手修正 6 处已漂移的行号锚点(此前指向空行或已迁移的文件):
backends.js:10-28 -> 10-30、index.js:47-65 -> 54-73、index.js:76-78 -> 87-91、
index.js:87-89 -> 95-96、index.js:93 -> 105、src/index.js:138 -> src/lb.js:155
(x-lb-backend 的 catch 在 656034d 修不可变响应头时已迁到 lb.js,文档一直指旧文件)。
GATE-DOCS PASS(115 line-refs / 477 path-refs / 252 section-refs,0 error)。
2026-09-30 23:08:57 +08:00
anonsaber fae37b35bc chore(ops): scripts/cron-drain.sh 加可执行位,便于直接 ./ 调用 2026-09-30 22:29:45 +08:00
anonsaber b9b7114fd3 feat(ops): scripts/cron-drain.sh,外部调度脚本,参数全走环境变量
后端没有内部调度器(C-NO-LONG-CONN),消费出站队列的唯一入口是 POST /worker,
增量对账的唯一入口是 POST /reconcile,两者都必须外部触发。没有这一步,新邮件
会一直堆在 stalwart:jmap,一行通知都不会发出来。

- 一次触发做两步:先 /reconcile 再 /worker,顺序不可反
- 两个端点成功码都是 204 且响应体为空;401/409/503 各给明确提示
- 503 时把服务端的 Retry-After 一并打进日志,cron 侧不用自己猜
- MW_WORKER_URL 单独可配:/worker 不在 LB 白名单内,走 LB 会 404,脚本会点出来
- --diagnose 走 /debug/*:配置是否保存、chat_id 是否占位值、webhook secret、
  Redis/JMAP/Telegram 依赖可达性、游标是否还停在 null
- --test-notify 绕过队列复用生产出站路径直发一条:这条能收到,问题一定在调度
- 凭据只从 MW_* 环境变量读,不读 .env、不写文件,日志不含 token
- --once / --loop;MW_BATCH(服务端上限 10,超出提示)、MW_LOG_FILE、MW_TIMEOUT
- 退出码 0/1/2 二值,失败步数看日志行数
2026-09-30 22:24:41 +08:00
anonsaber 2447106840 docs: describe the reconcile cursor, re-anchor drifted refs, fix three stale claims
reference.md: new Section 6.5 explains the bimodal cursor, the two token
classes (Session.state is not an Email-collection token; newState is), why a
truncated walk persists as a walk, and why oldState and destroyed are dropped.
Section 6.2 budget table is renamed to the new constant names and gains
RECONCILE_INITIAL_CHANGES. state:jmap:since is listed as a pipeline key.

opengaps.md: the re-baseline fix is described with the new session_state()
name, and the position-walk note no longer claims a state argument is passed
as None - the None is folder_id, and the method has no state parameter at all.

retired.md: the dead-code removal no longer claims those were the last three
#[expect(dead_code)] attributes in src/; reload_async still has one.

deployment.md / design.md: the 54+14 anchors the code move had shifted by one
hunk are re-anchored, plus two manual corrections (the /reconcile route and the
unconditional 204 return). charter.md: baseline 89 -> 93 passed.
AGENTS.md: the two anchor counts it quotes (60, 44) were stale; they are 54
and 17.

check_sec_refs.py: two-part section refs attributed to an RFC (RFC 8620
Section 2.1) are external, not doc references - the trailing-dot test only
caught the three-part form.
2026-09-30 14:11:52 +08:00
anonsaber 655167e862 docs: close the TTL gap, re-anchor drifted code refs, add anchor tooling
opengaps.md: the TTL item is now closed on the code side. The 21-value
table is keyed by ttl constant rather than by line number, so it stops
drifting. The honest caveat is called out in the open instead of being
implied away: the two real-Redis assertions are wired but unrunnable with
the credentials on hand, so 'assertion ready' is not the same as 'verified
on real hardware'. The residual-risk note that was still sitting under the
closed item has been removed along with it.

charter.md: test baseline 88 -> 89 passed.

deployment.md, design.md, retired.md: 41 stale src/ line anchors. The
drift was real and the audit missed it every time, because
audit_anchors.py only asserts that a referenced line exists and is
non-blank. Three are off-by-one; the rest are wrong code --
state.rs:379 pointed at XREADGROUP XACK while the claim was about
read_batch, retired.md:99 pointed into Intent::Search::parse, the two
route-table anchors landed on state: Arc::new(state),, and one
register_push anchor sat 31 lines above the fn.

scripts/docs_check/reanchor.py: maps HEAD -> working-tree line numbers
straight off git diff -U0 hunks, applies the fix or reports only with
--check. scripts/docs_check/show_anchors.py: prints every doc anchor next
to the line it lands on, which is what caught the semantic errors the
audit structurally cannot.
2026-09-30 13:20:13 +08:00
anonsaber 166b77eb34 docs: stop maintaining source line numbers in reference.md
`docs/reference.md` is the single source of truth and gets touched on every API
change, yet 40 of its 56 `notify.rs` anchors had drifted onto a closing brace,
a doc comment, or the wrong symbol - including one whole line pointing at
`revoke_admin_session` where a jmap dedup key is claimed - while
`scripts/docs_check/audit_anchors.py` reported every one of them clean. Its own
docstring says it only checks that the target line exists and is non-blank.

Remove the anchors rather than correcting them: fixing 40 numbers creates a
treadmill that re-drifts on the next unrelated edit to the same file, and the
file's own convention already called the line number "not an identity". Symbol
names are kept everywhere, which is what the gates and the readers need.

`docs/deployment.md` keeps its line numbers - it is an operator runbook read
mid-incident - and the five anchors that were wrong there are corrected.

docs/reference.md
- Drop the false "Verified against commit b2dbe7c" header claim; the anchors
  never matched that commit. Replace it with the design decision and the
  reason, and drop the "Only this file carries line numbers" convention that
  `deployment.md` was already violating.
- Remove all `*.rs:NNN` and bare `:NNN` anchors, keeping symbol names.
- Fix a wrong fact the anchors were hiding: the session issued by
  `POST /api/admin/session` is a fresh CSPRNG token generated per call, not a
  "fixed 32-byte hex secret derived from admin_token". The anchor pointed at
  `bootstrap`'s body.

docs/deployment.md
- `retry_or_dlq` call site 481 -> 479; `lock:reconcile` acquire 289 -> 290
  (twice); heartbeat `renew_lock` 302 -> 303 (twice).

AGENTS.md
- Record the convention: symbol names in docs, line numbers only in
  `docs/deployment.md`, plus the audit script's limits so nobody reads its
  output as "the anchors are correct".

scripts/docs_check/audit_anchor_semantics.py (new)
- Independent script, not wired into `run_all.sh`, always `exit 0`, prints
  candidates only. It flags an anchor whose target line has no relation to the
  symbol the doc claims. Candidates go 20 -> 5 and `reference.md` disappears
  from the list entirely. It cannot catch a one- or two-line off-by-one - that
  still needs a human - and a bare `:NNN` shorthand is not a matchable anchor.

Gates: GATE-DOCS 6/6 clean (ANCHOR AUDIT 324 -> 129 refs, 0 errors).
`src/` is untouched, so the test baseline stands at 86 passed / 0 failed /
2 ignored.
2026-09-30 02:02:34 +08:00
anonsaber d4c4c1d09b docs: rename roadmap.md to opengaps.md and drop completed items
- rename docs/roadmap.md -> docs/opengaps.md: file now holds only
  still-open / still-unverified items, so the name matches its content
- removed items proven done during the 2026-09-28 live integration run:
  /search E2E, Telegram 429 backoff, multi-instance duplicate window,
  /ready end-to-end dependency probe, JMAP read-only adapter real-device
  verification, real platform log collection acceptance
- removed the C-NO-LONG-CONN long-polling item (permanent non-goal,
  already registered in docs/retired.md) and the duplicated Redis ACL
  encoding clause (merged into the Redis TLS item)
- removed the whole 验收待办 section (all 3 items resolved)
- narrowed the phase goal to setWebhook + PushSubscription callback chain
- baseline corrected 61 -> 69 passed / 0 failed / 1 ignored, re-measured
  2026-09-29 with cargo test --locked --bin message-weave
- docs/charter.md D-G1-1 marked real-device verified, closing the
  contradiction with docs/design.md:397/:600
- update the DOCS list in all 4 docs gate scripts + TARGET_BEFORE regex
- GATE-DOCS 6/6 pass: VALIDATION, TABLE, ANCHOR 269, SECTION-REF 224,
  SIZE, PATH 392
2026-09-29 19:38:38 +08:00
anonsaber 633b41394c docs: 回答 hoststack.yaml 的来历与官方做法,补上命令字段不回写的证据
回答用户追问「这个文件哪里来的、官方推荐怎么做」。git 考古 + agent 源码两条路:

来历:a571033 自己手写的(2026-09-25),不是 HostStack 生成的。agent 镜像里既没有
init 类命令也没有内嵌模板 YAML(find 全仓 0 个 yaml 文件),只负责在仓库根读取
hoststack.yaml 或 hoststack.yml。所以"官方推荐怎么写"没有标准模板可抄,只有 schema。

官方做法:源码自己的定位是 Infrastructure-as-Code——
  "hoststack.yaml is Infrastructure-as-Code: when the repo declares a command,
   it wins over whatever happens to be cached on the services row"

新证据,把"UI 更方便管理"这个前提彻底否掉:agent 里只有两个 YAML->DB 回写事件
(yaml_port_declared、yaml_disk_declared),**命令字段没有**。注释原文明说
"the two that need to reach the services row have their own handled frames"。
所以每次部署都是拿 YAML 临时覆盖 services 行里的旧值,DB 里永远是过期的
./target/release/app。port 与 disk 要回写是因为 autoscaling 和 sleep-wake 直接
读那行;命令只有部署路径自己读,不值得持久化。

结论:控制台对命令字段永远是过期缓存,不存在"以 UI 为准"的稳态。
要删 hoststack.yaml,必须先改控制台那三条命令,文档里已有此前提。

audit_paths.py:hoststack.yml 加入 CONVENTIONAL allowlist——它是 yaml-parser
实际接受的第二种文件名,与已在白名单里的 docker-compose.yml 同类。

验证:GATE-DOCS PASS 6/6(PATH AUDIT 378 path-refs / 0)。
2026-09-29 12:25:08 +08:00
anonsaber bfbaae6f01 fix(docs_check): make audit_paths' brace-glob handling actually work
Two advertised mechanisms were dead code, found by self-testing the checker
rather than trusting its 0-error output:

1. variants() could never see a brace glob. Its segment class excluded ',',
   so `channel/telegram/{mod,commands}.rs` tokenised as two bogus fragments,
   `channel/telegram/` and `commands}.rs`. The glob was still caught as an
   error, but only the first alternative was ever reported and it was
   reported under the wrong filename — a reader chasing that error would not
   find `commands}.rs` in the document. ',' is now in the segment class and
   removed from PLACEHOLDER, so the glob stays whole, expands per variant,
   and the message shows the glob alongside the failing alternative:
   `channel/telegram/{mod,commands}.rs -> channel/telegram/mod.rs`.

2. The docstring attributed the non-path exclusions to is_noise(). They are
   not: JMAP methods (Email/changes), media types (application/json), dotted
   hosts and template URLs are dropped by the whole-span rule, and absolute
   paths never arrive at all because TOKEN's lookbehind refuses to start a
   match after a '/'. is_noise() is reachable fallback for dot-prefixed URL
   parts and whole-span seg/ tokens. The docstring now says which rule does
   which, so the next reader is not pointed at the wrong function.

Verified, not assumed. A scratch doc with src/util/, src/domain/llm/client.rs,
tests/telegram_dispatch.rs, run_mode.rs and one brace glob yields exactly
5 errors and exit 1. src/ai.rs, src/domain/jmap/client.rs, docs/todo.md
(git-deleted), a stalwartlabs/ citation and docker-compose.yml all pass, as do
Email/changes, application/json, api.telegram.org/bot, 200/503,
.well-known/jmap, {jmap_origin}/.well-known/jmap and a log-format string.
GATE-DOCS still PASS (6/6), 362 path-refs checked, 0 errors.
2026-09-29 09:35:08 +08:00
anonsaber 4c7cbe66b9 docs+tooling: no document may name a path that does not exist
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.
2026-09-29 09:28:29 +08:00
anonsaber 1ecba9d961 docs(tooling): close the phantom-identifier blind spot for bare filename anchors
audit_anchors.py's phantom check only ran on anchors carrying a line number
(`.rs:NN`), so a claim written next to a bare filename was never checked. And
fixing the regex alone would not have been enough: the verdict was a substring
test, so `TelegramClient::send` passed twice over -- `send` is a substring of
`send_text`, and reqwest's `.send()` is `send` at a word boundary, both in
src/channel.rs.

Adds PAIR_BARE for the bare-filename shape plus a stricter member_present test
(a definition site, or a reference not preceded by `.` or a word char) for it.
The numbered PAIR branch is untouched, so both the established
"248 line-refs checked, 0 errors" baseline and the `checked` counter are
unchanged.

The numbered branch deliberately keeps the loose test: a `T::member` claim next
to `foo.rs:NN` still passes when the member only exists as a longer name, but
there the anchor hands the reader a line to read, which is a weaker but honest
guarantee than the bare shape can offer.
2026-09-29 08:45:12 +08:00
anonsaber c8af429ffe fix(mode): restore executable bit on the doc validators
audit_anchors.py lost its exec bit and check_file_size.py was created without
one, so the scripts/ directory no longer had uniform permissions. run_all.sh
invokes them through python3 so nothing broke, but every sibling validator was
100755.
2026-09-29 01:16:43 +08:00
anonsaber 638c3a8cbc docs: consume all four self-checks; make the file-size rule enforceable
Every member's audit is now read and each real finding fixed, not just reported.

AGENTS.md §2.1
- The 500-line soft cap now has a machine-checkable exception: an over-limit
  file must carry a one-line `SPLIT-EVAL:` marker stating the split decision.
- Six files were over the cap with no justification: notify.rs 2036, state.rs
  1586, worker.rs 1045, config.js 1079, client.rs 601, config.rs 521. All now
  carry the marker. client.rs was missed by my own earlier line count, which
  did not descend into src/domain/jmap/.
- New validator scripts/docs_check/check_file_size.py; GATE-DOCS 4/4 -> 5/5.
  It proves the marker exists, not that the written reason is sound.
- Re-based 144 line anchors across six docs for the +1 line each file gained;
  every shifted anchor verified against the code, not just for non-blankness.

audit-arch.md
- design.md §6.1 module matrix omitted the `debug` module entirely; added.
- Seven numeric duplications in design.md now defer to reference.md §6.1 as
  the single authority instead of restating the values.
- reference.md §5.1 was missing DEBUG_TOKEN; §5.3 listed six recognised names
  and guessed `JMAP_*`. It now lists all 20 exactly as config.rs reads them.
- Corrected two wrong claims in the audit itself: debug.rs is compiled
  unconditionally, not under #[cfg(debug_assertions)] (there are no cargo
  features at all), and /debug/* exposes 7 routes, not the invented session
  list / resend / rebuild surface.

audit-fileplanner.md
- reference.md cited a nonexistent `jmap_service.rs`; replaced with the real
  adapter chain worker.rs -> domain/jmap.rs -> domain/jmap/client.rs.
- cloudflare-worker/README.md was outside every validator's scope; all four
  doc validators now include it (208 section refs, 250 line refs).
- README.zh-CN.md stated only half of what README.md says about RUN_MODE; the
  forward-placeholder sentence is now bilingual.
- charter.md §7 boundary table was missing rows for itself and for web/.

audit-rust.md / audit-frontend.md
- Error envelope corrected: `error` is a bare machine code, no `message`.
- Status table, AI-consent labels and ~15 stale or phantom facts fixed against
  notify.rs; `TelegramClient::SendMessage` and `web_config` phantoms removed.
- GET|PUT /api/enabled forwarded by the worker so the SPA kill switch works.
- Ten UI strings that called the admin credential the Redis ACL password now
  say CONFIG_ENCRYPTION_KEY.
- POST /healthz returns 405; dead HTTP_STATUS removed.
- audit_anchors.py now flags identifiers that do not exist in the file they
  are anchored to.

charter.md
- `SAF-ENABLE-FLAG` registered (85 IDs); `SAF-ADMIN-SESSION` row corrected;
- `REQ-PUSH-TYPES` registered; the stale `admin_session_digest` removed.

Gates: GATE-DOCS PASS 5/5 (250 line-refs, 208 section refs, 0 errors);
GATE-P0 62 passed / 0 failed / 1 ignored, baseline unchanged; worker and SPA
36 tests, 0 failures.

Real Stalwart / Telegram / managed Redis verification stays blocked on missing
credentials and remains labelled as unverified.
2026-09-29 01:16:11 +08:00
anonsaber e0882c7c68 docs: de-projectize AGENTS.md into language-agnostic norms; add docs/charter.md
AGENTS.md (276 -> 239 lines) is now a language-agnostic engineering-norms
document: code style, config and secrets handling, build environment,
testing gates, document governance. It states no project fact and names no
language, framework, dependency, or file. Verified: zero hits for JMAP,
Telegram, Redis, axum, Cloudflare, LLM, *.rs, and every project env var.

All project-specific content moved to the new docs/charter.md (~276 lines):
project goals, locked technology choices, 23 numbered security invariants,
implementation stages, prohibitions, test/acceptance gates with the full
GATE-P0 command and baseline, document boundary rules, and the stable ID
registry.

docs/charter.md §8 is the single authority index for stable IDs: 83 rows,
no duplicates, ID | definition file | one-line statement | category. Four
IDs that were referenced across the docs but never registered are now
registered: ARCH-STATE-REDIS, C-REDIS-EXTERNAL, GATE-UPTIME-KUMA, GATE-DOCS.
All previously registered IDs are preserved.

Defect fixed while rebuilding the registry: SAF-ADMIN-SESSION cited
src/config.rs `admin_session_digest`, a name that has 0 hits in src/ -- a
phantom identifier that survived in the authoritative registry since before
this restructure. Corrected to `session_digest` (src/config.rs:7). All 8
distinct backticked symbols in the registry's definition-file column now
verify against src/.

Two false project facts removed from the docs (both claimed the SPA writes
through POST /api/bootstrap, which web/config.js never calls -- 0 hits; the
actual path is PUT /api/business-config): README.md, README.zh-CN.md,
docs/reference.md. The first successful write creates the configuration and
later writes hot-reload it; POST /api/bootstrap remains the one-shot
automation endpoint.

Cross-reference redirections in 9 files: README.md, README.zh-CN.md,
docs/deployment.md, docs/design.md, docs/reference.md,
cloudflare-worker/README.md, cloudflare-worker/src/index.js,
cloudflare-worker/wrangler.toml now point project facts at charter.md and
language-agnostic quality rules at AGENTS.md.

Four validators extended to cover docs/charter.md: DOCS lists in
audit_anchors.py, check_tables.py, validate_docs.py, check_sec_refs.py, plus
charter.md added to check_sec_refs.py's TARGET_BEFORE regex and to
validate_docs.py's index-file cross-doc exemption.

Gates: GATE-DOCS PASS 4/4 (VALIDATION 0 errors, TABLE STRUCTURE 0
mismatches, ANCHOR AUDIT 227 line-refs / 0 errors, SECTION-REF AUDIT 191
refs / 0 errors). GATE-P0 PASS, baseline unchanged 62 passed / 0 failed /
1 ignored. Anchor audit proves line reachability only, not that the line
contains what the prose claims.
2026-09-28 23:39:26 +08:00
anonsaber a2297c21e8 tools: 固化文档校验器为 GATE-DOCS(此前只存在于 /tmp,保证不可复现)
四个校验器原先散落在 /tmp,本会话 /tmp 就被清过一次(validate_docs.py
中途消失),仓库里没有任何引用——「222 个行号锚点 0 error」这个结论在脚本
被清之后就无从再证,属于静默漂移。现固化进 scripts/docs_check/:

- validate_docs.py  代码块围栏闭合 / 行内反引号偶数 / 本地链接与 #锚点可达
- check_tables.py   表格表头、分隔行、数据行列数一致(行内代码里的 | 不计)
- audit_anchors.py  每个 foo.rs:N / foo.rs:N-M 锚点指向存在且非空的目标行
- check_sec_refs.py 每个 §N.N 交叉引用解析到归属文档的真实标题
- run_all.sh        驱动脚本,任一失败即 exit 1,仿 GATE-P0 的用法

改动说明(相对 /tmp 原版)
- ROOT 由硬编码绝对路径改为 Path(__file__).resolve().parents[2],干净 clone
  即可运行,无需改任何路径。四个文件已 diff 确认仅此一处差异。
- audit_anchors.py 加了 docstring 写明能力边界:只证明锚点可达,不证明该行
  内容与所述一致,无法发现「指向错行」的锚点。
- run_all.sh 用 grep -m1 '^===' 取摘要而非 head -1:validate_docs.py 摘要前
  带一个空行(原版自带瑕疵,保留原样以免偏离已验证版本),按位置取会取错。

双向验证:干净树 PASS/exit 0;注入 worker.rs:99999 与 §99.9 后分别被
audit_anchors 与 check_sec_refs 报出、exit 1;git checkout 还原后回到 PASS。

AGENTS.md 记录跑法与「可达 ≠ 准确」的区分。GATE-P0(cargo)不受影响,本次
无任何 src/ 改动。
2026-09-28 21:58:07 +08:00