Commit Graph

46 Commits

Author SHA1 Message Date
anonsaber d452f95485 fix(push): PUSH_SUBSCRIPTION_SECONDS 300→WEEK (604800)
Root cause of push notifications failing while cron works:
- Stalwart sends the verification request (RFC 8620 §7.2) once at
  subscription creation; event pushes (§7.3) arrive without
  verificationCode and rely on dedup_exists("push:subscription:{id}")
  to confirm prior verification.
- TTL was 300s (5 min): key expired → every event push 401'd.
- Cron path bypasses this key entirely (polls /changes directly),
  which is why cron-delivered emails still arrived.

Fix: align with PUSH_REGISTRATION_SECONDS (1 week). Stalwart does not
re-verify; the key must survive restarts and any gap between
subscription creation and events.

Also fixes: removed fictional push:subscription-code:{code} from
opengaps TTL table (no such key exists in code); corrected
reference.md caller from register_push→jmap_push verification path;
fixed 前→EX typo in reference.zh-CN.md; design.md updated with
fifth 'why' bullet and historical context.
2026-10-12 23:42:28 +08:00
anonsaber 1bdda7c471 fix: reconcile lock robustness — TTL 300s→60s, heartbeat 90s→30s, renewal interval 10s
- state.rs: RECONCILE_LOCK_SECONDS 300→60, RECONCILE_HEARTBEAT_SECONDS 90→30,
  new RECONCILE_HEARTBEAT_INTERVAL_SECONDS=10; ttl_contract_is_pinned asserts
  interval*2 <= TTL < 60 and interval < LOCK
- notify.rs: heartbeat sleep uses ttl::RECONCILE_HEARTBEAT_INTERVAL_SECONDS
- docs: 7 files updated (reference/charter/opengaps/design × en+zh + index.js
  comments); retired.md/opengaps.md anchor line numbers shifted for state.rs
  (+9) and notify.rs (+3); notify.rs:1484-1485 range trimmed to avoid blank
- GATE-DOCS 6/6, cargo fmt/clippy/test clean (103/0/4)
2026-10-12 22:03:11 +08:00
anonsaber 77bb253cc0 docs: clean remaining deployment.md refs in design docs
Replace 9 residual deployment.md references in design.md (EN) with
correct targets (charter §8, reference §3/§9, README §3.0/§3.10,
cloudflare-worker/README). Mirror all changes in design.zh-CN.md.
GATE-DOCS 6/6 PASS.
2026-10-10 19:06:40 +08:00
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 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 74d53ae542 docs: restore indentation the ZWSP cleanup destroyed in design.md
The U+200B sweep from 5f182f9 also collapsed leading whitespace to a
single space on 140 lines. That silently wrecked the fenced ASCII
architecture diagram, the tree listings and the indented JSON/Rust code
blocks in design.md.

This commit restores HEAD^'s indentation and re-applies only the intended
content edits from 5f182f9 (11 lines in section 5.6/5.7, plus the 14
ZWSP characters). Verified: the working tree differs from 5f182f9 on
exactly 140 whitespace-only lines and on nothing else.

GATE-DOCS 6/6 PASS (anchors 71).
2026-10-01 19:04:55 +08:00
anonsaber 5f182f97b0 docs: fix facts that the code contradicts, in both languages
SAFE_ROUTES has 19 entries (backends.js), not 18. reference.md and
reference.zh-CN.md both said '18 entries'; both now say 19.

reference §4 said the /api/enabled and /api/admin/session calls are
'admin-session Bearer-auth'd'. The code says otherwise: config_authorized
(notify.rs:423) accepts the shared worker token OR a valid admin session,
and create_admin_session (notify.rs:1187) is gated by worker_authorized
against the admin credential CONFIG_ENCRYPTION_KEY — it is what mints the
session, so it cannot be session-auth'd. Both languages now state the two
distinct credentials and why adding /api/enabled opens no new credential
path.

deployment §10.8 said every request-bearing entry point, listing /healthz
and /api/*, calls refresh_business_config. The actual 10 call sites cover
8 distinct paths (/ready, /worker, /reconcile, /push/jmap, /webhook/tg,
/api/config, /api/business-config, /api/telegram/register-webhook). GET
/healthz is a static 200 "ok" and never refreshes; /api/status,
/api/enabled, /api/push/register, /api/push/disable and the
/api/admin/session pair do not either. Both languages now list the real
set and name the exclusions.

design.md §5.7 listed the search trigger prefixes as English
'search'/'find'/'retrieval'; worker.rs:689 accepts the Chinese prefixes
搜索/查找/检索 and the help text states English aliases are not supported.
The two user-facing strings were also mistranslated: the empty-query
guidance prompt is 请提供搜索关键词,例如:/search 发票 (worker.rs:623)
and the no-match reply is 没有找到匹配「<query>」的邮件。
(worker.rs:792). The degraded-rendering marker is likewise a Chinese
string. SEARCH_SUBJECT_MAX=120 / SEARCH_PREVIEW_MAX=160 /
SEARCH_LIMIT=10 verified unchanged.

design.md §5.6/§5.7 were machine-translated from Chinese back into
English and were broken: 'the first 300 if it fails. characters', a
stray closing ** mid-sentence, '(none) deny_unknown_fields)' with a
mismatched paren, and 5 zero-width-space artefacts. charter.md:168 had a
broken '**Tables, lists, or values may not be copied across documents. **'
and opengaps.md had 4. All ZWSPs removed from docs/.

deployment.zh-CN.md §10.5/§10.6 were thin mistranslations: 'Worker 域'
for 'Worker URL', the debug-token requirement dropped entirely, and a
mangled '两者都使用用于验证部署的可用和版本'. Rewritten against the EN.

Also replaced a worker.rs:227 line anchor with the MetadataWorker doc
comment; that drops the repo's line-refs from 72 to 71, so new text no
longer adds to the drift surface.

No code changes; GATE-DOCS 6/6 (71 anchors, 455 section refs, 727 path
refs).
2026-10-01 18:47:05 +08:00
anonsaber 2089a95b79 docs: drop the SPA-footer deploy proof and reanchor notify.rs refs
Four spots told the operator to read the SPA footer to confirm a
deploy landed. The footer no longer renders a version, so restating
that would send an operator looking at a control that no longer
exists. Point them at /api/status instead; the version field itself is
unchanged.

The previous commit inserted 500+ lines into notify.rs, moving
register_push and register_telegram_webhook. Reanchor the two
deployment.md line refs; the other seven refs in that file are before
the insertion point and are unchanged.

Document the new retire-on-change contract in reference.md.
2026-10-01 11:18:07 +08:00
anonsaber af89a17f7f feat(auth): extend admin session to 30 minutes 2026-10-01 09:19:49 +08:00
anonsaber 7a27f7ff00 docs: add bilingual guides and callback setup in SPA 2026-10-01 02:36:03 +08:00
anonsaber 89dfe5cf3d feat(lb): /healthz 透传到源站,LB 聚合探针改挂 /healthz-worker
源站自己也暴露 /healthz;两者同路径时走 Worker 域名只能看到 LB 的聚合
结果,看不到源站自己的健康状态。现在拆开:

- GET /healthz        透传到源站(源站自己的健康检查)
- GET /healthz-worker LB 聚合探针(LB_VERSION + 后端汇总)

SAFE_ROUTES 增列 /healthz、ROUTE_METHODS 增列 /healthz: [GET],白名单 16 -> 17。
LB 探测源站的路径仍是 /healthz,不变。LB_VERSION 2026.10.1 -> 2026.10.2。

GATE-DOCS 6/6;node --check 全部通过。
2026-09-30 21:36:37 +08:00
anonsaber baff2dde8d docs: 关闭最后一个缺口(多账户不做),清掉由此暴露的 5 处陈旧指向 2026-09-30 15:12:53 +08:00
anonsaber 71336ea6d3 docs: record the real-Redis TTL green light and drop the credential ask
- opengaps: stage goal 2 is fully closed, the blocker list is empty, and the
  remaining user action is only the multi-account product decision.
- opengaps: record the fourth self-inflicted test bug -- the flaky 1s TTL
  window -- and what the corrected assertion actually proves, including the
  B-class Lua EXPIRE value now verified on a real instance.
- design: the stage-0 paragraph still claimed 69 passed / 1 ignored, which
  contradicted charter TEST_BASELINE; point it at the authoritative line.
- No credential values anywhere; the URL is written as a placeholder.
2026-09-30 14:52:06 +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 b764d6dc30 feat: reject stale business-config submits with a 409
revision becomes a request-body control field on BusinessConfigPatch. It is
never a setting: apply() and is_complete() ignore it, so the stored wire
never gains a revision key, and a PUT body that omits it keeps
last-write-wins for callers that never read the revision back.

The SPA records the revision it read, echoes it on every partial submit, and
on a 409 re-reads the stored values into the form and tells the operator to
recheck what they changed. Without that reload every retry would 409
forever; if the reload itself fails the SPA falls back to last-write-wins
rather than trapping the operator. The control field is excluded from the
empty-patch check so echoing it never counts as a change.

The compare-then-write is deliberately not atomic. Without a Lua script an
optimistic lock still has a residual window in which two requests racing
inside the same tick both pass and the later one wins; the window stays
small because a patch carries only the fields the operator actually changed.

notify.rs gained 18 lines before line 700, so its anchors in deployment.md,
opengaps.md and retired.md shifted by +18. Every one was checked against the
real line content, since the anchor audit only flags blank targets.

Gates: rust fmt/clippy clean, cargo test 87 passed 2 ignored, node --test
web/config.test.mjs 18 passed, docs_check/run_all.sh 6/6.
2026-09-30 02:56:15 +08:00
anonsaber e3924a53c1 test: cover business config read-back and partial submit
Adds the missing test coverage for the GET /api/business-config read-back
and the partial-patch PUT, then syncs the docs to the behaviour the code
actually has.

src/notify.rs
- PUT response body now carries "revision" alongside the
  x-business-config-revision header, which the SPA surfaces in its status line.
- 4 test helpers (a router that returns (Router, MemoryState) so a config can be
  seeded first, a full valid wire value, a direct state writer that skips
  validation, and a wire value whose JMAP URL fails fast) plus 9 tests:
  - GET 401 leaks no business field at all (body is exactly error + request_id)
  - GET echoes the 10 non-secret values and returns secrets only as presence
    booleans; the serialised body is asserted to contain none of the 6 secret
    values (SAF-NO-SECRET-ECHO)
  - GET with nothing stored returns 200 configured:false revision:0, not 404
  - a first save with an incomplete patch is rejected and writes nothing
  - a partial submit replaces only the fields it carries, verified by reading
    the merged result back over the same path the SPA uses
  - validation runs on the merged result, so changing one field cannot bypass
    the rest, and a rejected submit neither persists nor bumps the revision
  - telegram_chat_id is accepted as a number or a numeric string and a garbage
    string is rejected without overwriting the stored value
  - a typo in a field name is rejected rather than silently dropped

src/config.rs
- Pure deserialisation test for de_i64_from_number_or_string, with no router and
  no store. This is the only place that separates "the custom deserializer
  refuses the string" from "a later validation step refused it" - a real
  distinction, since the SPA submits telegram_chat_id as a string on a partial
  submit while the read-back model returns a number.

cloudflare-worker/test/index.test.js
- The forwarded-route test asserted a fixed upstream origin, which is not stable:
  the LB rotates its start index between tests. Assert the origin is one of the
  configured backends and check the path and method exactly instead.

docs
- 17 spots bring the docs in line with the shipped contract: GET is now
  documented, PUT as a partial patch, the worker safelist at 16 routes, and the
  preflight path listed as forwarded.
- Corrections the docs had backwards: the response field is "configured"
  (revision is a u64, 0 when nothing is stored); Invalid returns 422, only
  Unreachable returns 503; there is no chat_allowlist_empty error code.
- "Blank keeps the stored value" is now stated as a client-side contract. The
  backend does not distinguish an omitted field from an empty string - apply()
  uses unwrap_or_else, so an explicitly submitted "" really does clear the
  secret (validation then rejects it). Saying it was server-side was wrong.
- POST /worker was listed as a forwarded route though it is not in SAFE_ROUTES,
  and the per-category counts did not match the lists they introduced.
- POST /api/bootstrap does not share the patch-merge contract; it deserialises a
  full BusinessConfigWire, which is the only path that can create a config from
  nothing.
- Re-anchored 9 src/notify.rs line refs that had drifted blank and raised the
  baseline to 86 passed / 0 failed / 2 ignored.

Gates: fmt, clippy -D warnings, test 86/0/2 ignored, worker 32/32, SPA 17/17,
GATE-DOCS 6/6 clean.
2026-09-30 01:24:42 +08:00
anonsaber 953a4db6f0 docs: document the notification rendering contract and sweep anchors
New reference.md 5.4 pins the user-visible contract: notifications are three
metadata lines only (From / Subject / Received), never the body and never an
LLM summary; the mailbox display form and its fallback literals; the
timezone field, its 16-zone whitelist, the 422 on anything else, and why DST
is not tracked. Registered as REQ-TIMEZONE-DISPLAY in charter 8.

deployment 5 gains the TIMEZONE row, including the SPA section and the
reason DST zones cannot be added.

Opengaps 1 drops two blockers that are verified done and moves the remaining
two to a "what you do / what I do" checklist.

Corrected doc claims that the line-number checker cannot reach:

- reference 6.2 and the Redis key table cited notify.rs 240/253 and
  252/253 for lock TTL and heartbeat; the lock is acquired at :289 and
  renewed at :302.
- The Redis key table's dedup:tg / dedup:jmap / ratelimit / stream call-site
  anchors all pointed into the ready() handler and into unrelated config
  helpers. Re-pointed at the real statement lines.
- Both Retry-After helpers were anchored at error_response 321-329 and
  error_response_with_id 1088-1094; the real definitions are 370-378 and
  1368-1375, and the readiness path passes retry at :210, not :162.
- design claimed summarize() is also called by notify.rs for new-mail
  notifications. There is exactly one call site, the /summary branch in
  worker.rs; notifications never call the LLM.
- design described list_emails as requesting an explicit property list and
  returning Vec<EmailSummary>; the code passes no properties and returns
  Vec<EmailMetadata>.
- retired listed 6 intents but only 5; worker.rs:533 for /search is :553.
- Config is 7 fields now that timezone is mirrored into it.

The anchor audit now flags any citation that lands on a bare brace, paren or
comma - the shape these drifted references had.
2026-09-29 21:48:04 +08:00
anonsaber 8b1afac430 docs: close two integration blockers and document the worker drain schedule
- opengaps: drop the Telegram inbound callback and Stalwart PushSubscription
  blockers, both verified live; 2 items remain (newState semantics, TTL
  measurement). The public-callback mapping question is also resolved -- the
  integration env has no Worker in front and Stalwart hits the backend origin
  directly, while /push/jmap is already in the Worker safelist for production
- deployment 4.1 (new): full setWebhook and /api/push/register procedure,
  expected replies, failure symptoms, and the setWebHook rotation trap (the
  deployed secret must be re-registered after any TG_WEBHOOK_SECRET change)
- deployment 6.2: the worker is neither a background task nor a separate
  container; POST /worker is the only consumer entry point
- deployment 6.3.1: add the /worker drain step to the cron and k8s CronJob
  examples, plus a two-endpoint comparison table, ordering requirements, the
  10-message batch cap, and the "204 is not a success signal" warning
- deployment 10.4: describe /worker as scheduler-driven, not manually invoked
- reference, retired: /worker anchor notify.rs:331 -> 380; the old value fell
  inside the reconcile function body, so the anchor audit passed while being
  semantically wrong
- charter 6.6: narrow the blocker list; code baseline b2dbe7c -> fc686ab
- design 11.2: PushSubscription permission now verified; the external Cron
  must call both /reconcile and /worker

GATE-P0: cargo fmt / check / clippy clean, 69 passed, 1 ignored
GATE-DOCS: 6/6 clean (ANCHOR 299, SECTION-REF 234, PATH 428)
2026-09-29 20:55:54 +08:00
anonsaber 94b6baf7bb docs: reframe SAF-DEBUG-ALLOWLIST as never a gap, not a risk acceptance
The PM's criterion: if the documented production posture already contains
the risk, it was never a code gap to begin with -- it was just a
description of current behavior. My previous wording said it was "closed
by risk acceptance", which implies an accepted open risk that someone
decided to live with. That is the wrong frame and reads worse than the
truth in a security review.

Reworded to state the actual reasoning: the exposure is contained by the
dual-factor debug gate and the Worker safelist, each sufficient alone,
so a third layer was never needed and nothing was accepted.

No behavior, code, or ID registration changed -- wording only.
GATE-DOCS 6/6: ANCHOR 270, SECTION-REF 226, PATH 394.
2026-09-29 20:02:15 +08:00
anonsaber bfd3fefd97 docs: close SAF-DEBUG-ALLOWLIST by risk acceptance
The PM accepts the residual on the strength of the two gates that are
already the documented production posture, so the item leaves the gap
file and docs/opengaps.md no longer has a 代码缺口 section at all.

Both gates verified in code before agreeing to the premise:
- closing debug is total: the dual-factor gate in src/main.rs:98-105
  feeds src/notify.rs:1340-1342, which only then merges debug_router();
  unset and the 7 routes do not exist, axum falls through to a generic
  404, so it is not a 401 and does not disclose the path exists
- the Worker refuses rather than forwards: cloudflare-worker/src/index.js
  :81-82 returns 404 route not forwarded for any path outside LB_ROUTES,
  and LB_ROUTES = SAFE_ROUTES holds exactly 15 entries with no /debug
  path. Confirmed by reading the list, not by trusting the README

So either gate alone is sufficient, which is stronger than the assumption
recorded in the gap entry, which treated the allowlist as the thing
keeping the other two apart.

The residual is already on paper and is the only real one: direct access
to the backend origin bypasses both. docs/deployment.md:103 says exactly
this, so nothing new had to be written, and design.md:349 now points
there and to docs/design.md §7.6 instead of at a section that no longer
exists.

- drop the allowlist row from the 你要做的事 table as well, or the table
  would advertise a task whose gap no longer exists
- docs/design.md:349 would have dangled on 「代码缺口」, so it now records
  the closure and its basis rather than a count of a deleted section
- docs/retired.md:81 says 「代码缺口」4 → 3. That is the recorded effect
  of the deletion it documents, so it stays as a past-tense fact rather
  than being rewritten to the current state of 0
- the behavioral fact stays where it belongs: docs/deployment.md:102 still
  documents that an empty allowlist does not block, and charter.md:279
  still registers the ID with its 启用须确认白名单已配置 condition.
  Only the gap status was removed
- GATE-DOCS 6/6 pass: ANCHOR 270 (was 272), SECTION-REF 226 (was 224,
  the two new cross-refs I added), PATH 394 (was 393, net +1 swap)
2026-09-29 19:59:33 +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 fc686abc22 config: persist business config regardless of client build, add preflight probe
PUT /api/business-config and POST /api/bootstrap now treat validation as the only
write gate: a valid wire is always persisted, and only then are the clients built
and the running worker swapped. A failed build returns 200 with persisted=true,
runtime_applied=false and a warnings array of {component, step, detail} instead of
503, so an unreachable JMAP/LLM no longer turns a valid configuration into a silent
data loss. The previous worker keeps serving.

POST /api/business-config/preflight runs the same validation and client build
against the submitted wire without writing anything and without touching the
running worker. It answers 200 even when the configuration is bad, because that
is the answer the caller asked for; components is null after a validation
failure, and llm is null rather than vacuously ok when LLM is disabled.
Authentication is the admin-session gate, not DEBUG_TOKEN.

The SPA drops the account_id field (the backend still accepts it; omitted falls
back to the session primary account), adds a Test connection button, and shows
the build fingerprint in the footer, the topbar and the setup card. The
503 message no longer claims JMAP/LLM build failure, which is not what produces
it.

BusinessConfigWire.account_id gains #[serde(default)]; without it, omitting the
field in the wire body made deserialization fail outright.

build.rs bakes BUILD_VERSION (git sha or nogit, plus a UTC build timestamp) which
GET /api/status exposes as version.
2026-09-29 18:25:56 +08:00
anonsaber dda8e084ae fix(jmap): trust the configured origin host so Stalwart's session redirect is followed
jmap-client refuses to follow any redirect unless the target host is on a
trust list that defaults to empty. Stalwart 307-redirects /.well-known/jmap
to /jmap/session, so connect() aborted with "Aborting redirect request to
unknown host 'mail.motofans.club'" and PUT /api/business-config failed with
a 503 carrying no usable detail.

connect_with_runtime now passes the origin host parsed from the normalized
session URL to follow_redirects, trusting that host only -- a target host the
server invents is still refused. An unparseable base yields an empty host,
which matches no target and preserves the previous fail-closed behaviour.

Verified against a real Stalwart instance both ways: the ignored smoke test
session_list_and_read_smoke now passes (session -> folders -> email list ->
read) and fails with the redirect error when the change is reverted.

docs: record the redirect trust under SAF-JMAP-URL, mark G1/D-G1-1 verified
after the real-server run, and refresh two shifted line anchors in client.rs.
2026-09-29 16:30:55 +08:00
anonsaber 4cf128fcc8 feat(debug): 开关信号改为 DEBUG_ENABLED 环境变量,启动命令不再需要改动
起因:用户反馈会临时改启动命令(比如给 start 加 --debug)。而 hoststack.yaml
声明的 start.command 优先级最高且每次部署都覆盖控制台旧值,所以每次开关调试面
都得改 YAML、提交、推送、部署——最麻烦的那条路。

改法:把双因子的「开启信号」从单一 argv 变成「argv 或 env 二者之一」:
- --debug 保留,本地开发照旧用
- 新增 DEBUG_ENABLED(1/true/TRUE/True/yes/YES,精确匹配),生产走控制台环境变量
- DEBUG_TOKEN 仍然必须非空。刻意不让 token 单独生效——token 是平台上的长期
  Secret,让它自己开门就等于把双因子退化成单因子

收益:生产开关调试面 = 控制台改一个 env + 重新部署,不改代码、不改 hoststack.yaml、
不重新构建。启动命令从此静态,顺带消解了"YAML 还是控制台"的争论——命令不再需要变,
放哪都无所谓。

实现:把判定抽成纯函数 debug_surface_requested(debug_arg, debug_enabled_var),
不直接读全局 env。env 是进程级全局态,测试里读它会和同二进制内其他测试竞争;
调用方传入已取好的值,测试就能覆盖真值集合、假值集合(含 "TRUE " 带空格、
"debug" 这类近似值)与"开了开关但没 token 仍然关闭"。

文档:deployment.md §2.1 双因子条件重写 + 环境变量清单;§5 表格;§5.1 启动命令
说明;reference.md §3 表与调试面描述;design.md 架构图注与 LLM_ALLOW_NET 锚点;
charter.md 两条 ID 描述。hoststack.yaml 的 start 注释从"临时加 --debug"改成
"用 DEBUG_ENABLED 切"。

顺带修掉加代码引发的锚点漂移(新代码把 main.rs 后面的行号整体后移 13):
design.md:366、design.md:739、reference.md:205/335/336/380 共 6 处。

验证:cargo test --bin message-weave = 65 passed / 0 failed(新增 4 个测试,
含真值集合、假值集合、无 token 仍关闭)。GATE-DOCS PASS 6/6,259 line-refs 全绿。
2026-09-29 12:54:06 +08:00
anonsaber cf252a9f06 docs+build: Dockerfile 降级为仅本地开发用,并记录 hoststack.yaml 保留/移除决策
用户要求两件事:把 Dockerfile 明确降级成「仅本地开发用」并说清边界;
以及若日后决定移除 hoststack.yaml,把理由写进相关文档。

先核了一遍决定性的技术前提——删掉 YAML 到底会发生什么。读 HostStack agent
源码,优先级是三层,而且和我上一轮写的说法不一样:

  1. 控制台 services 表已存的值 -> payload
  2. YAML 声明的字段无条件覆盖(guard 只是 `if (svcConfig.start?.command)`,
     没有 `&& !payload.x` 短路)
  3. YAML 省略的字段沿用控制台值;框架默认值只在仍为空时才填,而 agent 自己的
     注释写着 "trigger-payload always populates payload.x from the DB",
     所以对已建好的服务这条分支实际是死代码

因此删掉 hoststack.yaml **不会**回落到 cargo fetch / app 那套默认值,而是直接
采用控制台现存的值——而那恰好就是坏掉的 ./target/release/app。"UI 更方便管理"
这个前提成立,但必须先改 UI 再删文件。据此写了决策记录,当前决定是保留。

Dockerfile / docs/deployment.md §3 / docs/design.md / README.md
- 统一标为仅本地开发 / 本地容器调试路径,生产不执行
- 写清边界后果:诊断工具、ENTRYPOINT tini、EXPOSE 8080 在线上都不生效
- 给出判定方法:容器内看 /proc/1/cmdline 与 /etc/os-release
- 顺带修掉 design.md 里把 hoststack.yaml 描述成"网关编排清单"的错误——
  文件里没有任何 gateway,只有 services.web

docs/deployment.md §5.1
- 三层优先级替换掉原先"YAML 省略则回落框架默认值"的错误说法
- 新增决策记录:保留的理由(进 git、可 review 可回滚)与代价(双写);
  若日后移除,必须在控制台先把 install/build/start 三条改对再删,并把本段
  移到 docs/retired.md 记下移除 commit

验证:GATE-DOCS PASS 6/6(SECTION-REF 226 refs / 0,PATH AUDIT 376 refs / 0)。
2026-09-29 12:22:00 +08:00
anonsaber 417f378d68 build+docs: 镜像内置只读诊断工具,清理 Dockerfile 的死 ENV 与文档过期表述
生产镜像此前只有 ca-certificates + tini,容器 rootfs 又只读,运维在容器内既
装不了 apt 也没有 curl/ps,排查只能靠宿主机的 /proc。把只读诊断工具装进
runtime 阶段,实测体积 +7.7 MB:

  curl procps(ps/pgrep/free) iproute2(ss/ip) jq bind9-dnsutils(dig) netcat-openbsd(nc)

全部是只读查询能力,不引入包管理之外的写能力,也不新增 shell 解释器。

顺手修掉四处已被事实推翻的表述:

  1. Dockerfile 的 ENV RUN_MODE=webhook 是死变量——该标识符随 Config::from_env()
     删除后代码零读取,文档 5 处都记着已删除,镜像却还在设。删掉并留注释防回退。
  2. deployment.md 说「runtime 只装了 ca-certificates tini」「镜像内没有 curl/wget」,
     两处都是健康检查建议的承重理由,随内置 curl 一起更新为「技术上可行但不建议」。
  3. deployment.md 说非 root 用户是 uid 1000,实际 useradd --system 给 uid 999,
     已实测修正。
  4. retired.md 里 compose healthcheck 示例的删除理由把「镜像无 curl」当主因,
     真正的承重原因是 src/main.rs 没有 CLI 子命令解析,已改写并标注 curl 已内置。

同时补齐 hoststack.yaml 缺失的 install 段(cargo fetch --locked),并在 start 段
写清临时开 --debug 的确切改法与「必须在容器内探、别走公网域名」的约束。

验证:
  - 包名与安装步骤在同一 debian:bookworm-slim 基础镜像内实测通过
  - useradd 实测输出 uid=999,与设计一致
  - docker build --check 无告警
  - GATE-DOCS PASS 6/6(SECTION-REF 217 / 0,PATH AUDIT 364 / 0)

注:install: { command: ... } 的键名是按现有 build:/start: 的同构推断的,
若 HostStack 拒绝该键,删掉这两行即可,不影响 build/start。
2026-09-29 11:24:13 +08:00
anonsaber 55a2bf5a14 docs: 修正 6 处代码行号锚点漂移,补 1 处缺失锚点
全量人工核对 44 个 `file.rs:行号` 锚点,发现并修正 6 处解析通过但指向错误行号的锚点
(`audit_anchors` 只验证锚点可解析,不验证该行是否承载文中所述内容):

- src/main.rs 的 `--debug`/DEBUG_TOKEN 双因子读取块已从 96-109 上移到 82-92:
  deployment.md:62 (96→82)、:63 (97-100→82-89)、:65 (107-109→90-92),
  reference.md:228 (96-100→82-89)
- SendMessage 结构体在 channel.rs:60 而非 :10(原指向 TELEGRAM_BACKOFF_BASE_MS 常量)
- reference.md:206 生产路由合并点是 main.rs:180 而非 :194;205 的 setup 路由锚点同处校正
- design.md:739 的 llm 构造分支锚定 111-127 整段而非孤立的 :125
- deployment.md:69 补上 worker_authorized 自身定义点 (notify.rs:446),
  原先只有 constant_time_eq (1136) 一个锚点,读者跳转后看不到被命名的符号

校验:GATE-DOCS PASS 6/6,ANCHOR AUDIT 250 line-refs / 0 errors,
PATH AUDIT 363 path-refs / 0 errors,人工 dump 复核 44/44 锚点落点正确。
2026-09-29 10:14:03 +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 b2dbe7ccbb refactor: delete three zero-implementor 'stage 0 placeholder' channel traits
src/channel.rs had Channel / Notifier / MessageAdapter, each marked
#[expect(dead_code, reason = "稳定ID+阶段0占位…")]. All three had no
implementor, no caller and no dyn binding: Telegram outbound actually goes
through channel::telegram::TelegramClient::send_notification -> send_text,
held directly by worker.rs's MetadataWorker and notify.rs. The traits were
never on any path.

- delete the three traits + their #[async_trait] attributes
- drop the async_trait import from channel.rs (unused afterwards); the
  dependency stays in Cargo.toml, still used by 5 other files
- drop the domain import from channel.rs (its only uses were inside the
  deleted traits; the telegram module has its own Notification import)
- delete UserCommand from src/domain.rs -- its only consumer was Channel.
  Notification stays (worker.rs:234 builds it)
- rewrite channel.rs's module doc: "only an adapter boundary in stage 0
  (no bot commands yet)" was already false -- /help, /search, /summary
  and /ai on|off are all parsed in worker.rs::parse_intent

The three dead-code attributes were the last three #[expect(dead_code)] in
src/. Two #[expect(clippy::too_many_arguments)] remain in notify.rs; those
are justified lint suppressions on router wiring, not placeholders.

External contracts unchanged: HTTP routes, Redis keys, error codes and the
error envelope are untouched.

GATE-P0: fmt / check / clippy -D warnings green, 61 passed / 0 failed / 1
ignored (unchanged).
2026-09-29 02:23:38 +08:00
anonsaber 2da11b9f92 docs+fix: delete the legacy env compat layer; sync every doc that described it
Code (src/config.rs 521->394, src/main.rs 232->200):
- Config::from_env() + required_secret / required_nonblank / env_bool deleted.
  config.rs now performs no std::env read; all business fields come from Redis
  business config (BusinessConfigWire -> validate_business_wire -> validate_nonblank).
- RUN_MODE deleted: variable, Config.run_mode field and the main.rs validation
  block. The two modes never changed runtime behaviour -- webhook and reconcile
  share one router, POST /reconcile is a standalone endpoint.
- jmap_password() accessor deleted; the field name is kept in
  BusinessConfig / BusinessConfigWire because it is real data, not a stub.
- Config.port and Config.redis_url deleted: both had zero readers, and port was
  hardcoded to 8080 in both constructors while the listener binds straight from
  the PORT env var, so the field could silently disagree with the real port.
  Config is now 6 fields: telegram / jmap / account_id / llm / auth / worker_token.
- redis_only() and from_business(value) signatures simplified accordingly.
- #[expect(dead_code)] count in src/ is now 0.

Test coverage: the from_env()-driven startup_rejects_blank_auth_secret case is
gone with the path it tested. The non-leak assertion it carried was reattached
to the real validation route (BusinessConfigWire -> TryFrom -> validate_nonblank)
with a stronger third assertion that the rejected value is not echoed either.

Docs: design.md 7.1 re-scoped to the Redis business config table; reference.md
5.1/5.3 rewritten (no legacy env path); deployment.md 5 and the runtime-mode
table rewritten; retired.md gains section 4 registering every deletion;
README x2, .env.example, charter.md updated.

GATE-P0: fmt / check / clippy -D warnings green, 61 passed / 0 failed / 1
ignored (was 62; the deleted case). GATE-DOCS: PASS 5/5, ANCHOR 247 line-refs /
0, SECTION-REF 216 / 0.
2026-09-29 02:02:44 +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 b70d188864 docs+fix: sweep stale and phantom references; log the actual bound port
Code (2 sites):
- main.rs logged bootstrap_config.port, which is always the hardcoded 8080 in
  the two-variable production config; the listener actually binds the port
  parsed at main.rs:31-34. Log the bound port.
- That exposed Config.port as genuinely unread (clippy -D warnings), so mark it
  with #[expect(dead_code, reason=...)] instead of silently deleting a
  documented env-parsing site.

Docs, phantom identifiers (names that do not exist in src/):
- RuntimeConfig x3 -> OutboundConfig via RuntimeConfigProvider (state.rs:34/41)
- LlmClient::noop() x2 -> None in Option<Arc<LlmClient>> (main.rs:125)
- main.rs::validate_env_or_exit x2 -> config.rs:338 read, main.rs:75 validation
- TelegramChannel::send_message -> TelegramClient::send_text (channel.rs:134)

Docs, stale facts:
- LLM table: ENABLED and ALLOW_NET defaulted true,true -> false,false;
  API_KEY, BASE_URL, MODE were "required" -> only required when enabled. This
  contradicted design.md:385-387, which already had the correct values.
- design.md:560 said RUN_MODE "is actually consumed"; it is only read and
  validated and never affects behaviour (main.rs:73-74).
- RUN_MODE operator docs (README, reference, deployment, .env.example) now say
  both accepted values produce identical behaviour.
- retired.md: 5 intents -> 6 (/search shipped, worker.rs:517); 4 LLM env vars
  -> 6; main.rs tracing events 6 -> 7.
- design.md:494 tracing count 6 -> 7, debug-gate warning added to the list.
- design.md:350 code-gap count 2 -> 1 (multi-instance window done in 6c99ce5).
- design.md:554 marks the 45-paused stage-0 baseline explicitly as historical.
- .env.example said the Redis ACL password was the bootstrap trust root; it is
  CONFIG_ENCRYPTION_KEY (main.rs:88-91).

Docs, re-anchored after the main.rs shrink (6 refs shifted by -2).

GATE-P0 62 passed / 0 failed / 1 ignored; GATE-DOCS 4/4 clean
(228 line anchors, 211 section refs, 0 errors).
2026-09-28 22:33:27 +08:00
anonsaber c746ead184 docs: 同步 /search 落地与 ⑥-a/⑥-b 后的断言、重锚 worker.rs/reference.md
⑥-d 文档同步(01a0e760-b6f5),无代码改动。

/search 落地(bfe0fd8)
- design.md 新增 §5.7「邮件搜索」:Filter::text + SearchSnippet/get、高亮降级、
  纯文本出站、失败映射、截断上限、正文级高亮在锁定版本做不到(含原因)
- design.md §2.3 接口表、§3.2 search_emails 方法行、§5.6 Intent 6 种、§10.5 待办收口
- reference.md §8「Not implemented」:删掉「/search 未实现 / SearchSnippet 零命中」
  两条已失效断言,改为「正文级高亮不可能」这一真实缺口
- reference.md §1.2 补 /search 触发词(仅前缀匹配,不授予授权、不写 Redis 键)
- AGENTS.md 阶段 5 标注搜索已完成

⑥-a/⑥-b 遗留的陈旧断言(本轮清掉)
- design.md §12.8 速率:429 从「不做专门的自动退避」改为按 parameters.retry_after
  退避(截断到 60s 预算,缺字段回退指数退避 250ms→4s)
- design.md §10.6 已知边界 + §9:XAUTOCLAIM 阈值从「批大小 × 300s」改为按运行配置
  推导,公式、300s 下限与 6h 上限指向 reference.md §6.3
- reference.md §6.3 重写阈值公式,新增 §6.4 搜索上限常量表

重锚
- worker.rs:569 → 613(help_message,⑥-c 插入后下移)
- AGENTS.md 版本基线 → bfe0fd8;retired.md 验证基线 → bfe0fd8;
  retired.md read_batch 锚点 343→384 / 379→425 / 376→394-401
- design.md §3.5 交叉引用 §12.9 → §12.8
- SAF-DEBUG-ALLOWLIST 保持「不改代码」原状,按产品决策未动
2026-09-28 21:45:05 +08:00
anonsaber 399b26918e docs: ④ /debug/* 配套文档 + 全量锚点重锚 + AGENTS v3 版本基线(01a0e6de)
## 锚点重锚(afa6cec 之后)

④ 对 notify.rs 的编辑使一批 `notify.rs:N` 引用整体失效。全量重扫 8 个 md、
165 个 src 锚点,0 硬错误(越界 / 区间全空 / 单行指空行三类全查)。修正要点:
- reference.md(30+ 处):dedup/committed/inflight/ratelimit 键调用点、
  error_response 320-328、error_response_with_id 1087-1093、readiness 161、
  router 1275 + routes 1304-1318、worker 330、main.rs 40/50/34/78-81、
  NoopWorker 由 notify.rs 更正为 worker.rs:142-144、
  PROBE_TIMEOUT 73 与 tokio::join! 147、state.rs 639。
- deployment.md:149-152→227-230、159/172→239/252、index.js 40-55→45-60。
- retired.md:704→717、887-890→897-900、326→330、873→884。
- roadmap.md:95→175;design.md:main.rs 109→128。
- cloudflare-worker 侧 4 处 stale:ROUTE_METHODS 45-60、404 77-78、
  405 80-83、503 85-93。

## /debug/* 文档

- deployment.md 新增 §2.1:双因子门禁、7 条路由状态码表、/debug/config 字段清单、
  三条部署约束、生产建议;§5 env 表补 --debug / DEBUG_TOKEN。
- README.md / README.zh-CN.md 各加一段「远程联调面(可选,默认关闭)」,指向 §2.1。
- design.md 新增 §7.6「为何默认绝对关闭」:双因子挂载、404 而非 401
  (401 会泄漏路由存在性)、无默认开启回退、不在网关白名单内 fail-closed。

## 文档纠错(与任务书相反,已按源码核实)

- /debug/worker 是 GET 不是 POST(debug.rs:79)。
- /debug/config 不是「只出布尔」:明文回 jmap.{session_url,username}、
  telegram.chat_id、llm.{base_url,model} 与全部 timeout/retry,
  仅 5 个凭据字段出 *_configured。文档措辞改为「不含凭据,不是全脱敏」。
- 门禁位置更正:--debug 读 main.rs:99、DEBUG_TOKEN 读 main.rs:100-103、
  merge 在 notify.rs:1320-1321(无 DEBUG_BASE 常量,路径为字面量)。

## AGENTS.md

- 版本基线 v3(代码 afa6cec / 文档 e502c40),三处文档基线号 c5bc7b8→afa6cec。
- 新增 2 个 ID:SAF-DEBUG-ORIGIN-ONLY(不在网关 14 条白名单,Worker 404
  route not forwarded,只能直连 origin)、SAF-DEBUG-ALLOWLIST。
- §6 deployment.md 行、§7 六行 ID 定义点回填文档位置。

## 未决项登记

- roadmap.md 新增:SAF-DEBUG-ALLOWLIST —— debug.rs:232-235 的白名单判定
  「非空且不含」导致业务白名单为空时 /debug/notify 的 chat_id 不拦截。
  本轮不改代码(该面默认关闭且需 Bearer,空白名单下主产品本就接受任意 chat);
  若要收紧需改为「空白名单则拒绝」+ 补测试 + 跑 gate。

## 校验

文档校验器 0 error;负向对照断链/死锚点/奇数反引号/页内锚点 4 类注入全命中。
48 张 markdown 表 0 处单元格数不符。行数合计 2415。纯文档,未触碰 src/ web/
cloudflare-worker/。代码基线仍 afa6cec,gate 49 passed / 0 failed / 1 ignored。
2026-09-28 16:20:18 +08:00
anonsaber cc645020d9 docs: /ready 端到端就绪语义同步 + 删除 Redis 错误映射虚构断言
两件事一批提交(全部 docs-only,无代码改动):

① /ready 语义升级为端到端就绪(对应 commit c5bc7b8 的实际行为)
- ARCH-READY-BASELINE 从「配置完整性 + Redis 可达性」改为:配置完整性
  + Redis 可达 + JMAP session 探针 + Telegram getMe 探针,四者全过返 200
  与就绪报告,任一失败返 503 + 标准错误 envelope + Retry-After: 30
- 探针细节:各 PROBE_TIMEOUT 3000ms、tokio::join! 并行、最坏约 3s
  (先前文档写「串行 / 最坏 6s」是错的,已全量修正 10 处,含 AGENTS.md /
  design.md / deployment.md / roadmap.md / reference.md / README 中英)
- LB 探针超时下限从「> 6s」改为「> 3s(建议 >= 10s)」
- JMAP 探针实际行为:取 JMAP_SESSION_URL 的 origin 后 GET
  {origin}/.well-known/jmap 并带配置的 Basic 认证(不是打裸 session URL);
  原因是裸 URL 不带认证会永远 not-ready,进而让 ingress 停止路由
- deployment.md 新增 /ready 出站 egress 硬依赖说明:egress 不通则长期 503,
  而 reqwest 以 default-features=false 编译不解析 HTTPS_PROXY
- reference.md Retry-After 规则改准:error_response 仅在 retry=true 时设置;
  error_response_with_id 无条件设置但仅用于 push register/disable 的
  push_state_unavailable / push_destroy_failed(恒 503);503 错误码表补上
  这两个此前漏掉的码
- GATE-READY-DEPS 门禁已闭合,从 AGENTS.md 与 roadmap.md 删除该 ID

② 删除「Redis 错误映射」这条虚构断言(代码缺口 4 -> 3)
- roadmap.md 与 design.md:634 的同一条断言删除;design.md 脚注「4 条」->「3 条」
- 根因:消费入口是 HTTP handler worker(notify.rs),不是后台循环
  —— 全仓 src/ 零 select!、无信号处理、无常驻 worker 进程;read_batch 与
  retry_or_dlq 的 Err 均经 ? 上抛,调用方一律返 503 + retryable=true。
  原先描述的「循环不因单次失败退出」这个前提本身不成立
- retired.md 新增一条虚构断言记录,完整保留原始断言与推翻依据
- roadmap 验收待办同步:/ready 端到端探测转已完成,并注明两个探针是可复用
  纯函数、④ 远程 debug 应直接复用同一实现,不要另写一套

③ 顺手修正本轮代码移动导致的失效行号锚
- retired.md:refresh_business_config 628->704;lock:push-register 810-819->887-890;
  register_push 668->873;notify::push_handler 改引真实函数名 jmap_push /
  worker / reconcile(原行号 250/97/136 全错,且 push_handler 非真实函数名);
  worker safelist fail-closed index.js:69-82 -> :77-92
- reference.md:router 1238-1265 -> 1250-1277;/worker 314 -> 326;
  路由表补上此前漏掉的 /、/assets/config.js、/assets/styles.css

基线:三处「验证基线」(roadmap.md:5、retired.md:7、reference.md:6)由
ed93358 更新为 c5bc7b8。

校验:8 个 md 代码块偶数、无行尾空格、无 CR;5 个文件全部表格列数一致
(剥掉 code span 后再计数);相对链接与锚点全部可达。
Rust 门禁不受影响(无 src/ 改动),基线仍为 45 passed / 0 failed / 1 ignored。
2026-09-28 12:08:33 +08:00
anonsaber e35cc64ce3 docs: 清理废弃/虚构条目,新增 retired.md 独立废弃记录
新增 docs/retired.md(114 行)作为唯一废弃/虚构条目登记处:
- 12 条未采用的路线(teloxide / feature 集 / teloxide-core 回退 /
  grammers / BotError 六个未落地变体 / 速率桶+429 分支 / 多步 FSM /
  模块子树拆分 / telegram_dispatch 测试 / docker healthcheck 示例)
- 原 teloxide 候选对比评估记录 + FSM 五态状态表(保留决策依据,非当前事实)
- 24 条虚构条目(文档曾写、src/ 从未有):幽灵模块树、delivery:pending 键、
  check_config_reload、PushVerification、CancellationToken、Preview、
  LlmErr、四个不存在的 LLM 环境变量、llm.call span、熔断/半开设计等
- 2 条已删除文档(docs/todo.md、HANDOFF.md,后者从未进入 git 历史)
- 3 步重启流程

docs/design.md 850 → 764 行:17 处目标形态改写为可核实的当前事实。
重点:§4 压缩并指向 retired.md;§5.2 补 Channel/Notifier/MessageAdapter
三 trait 零实现;§6 树改为真实 12 文件结构;§8.1 换成真实 src/error.rs
(原目标形态 6 变体整体移至 retired.md);§8.3 明标无 tracing/metrics/
优雅关闭;§10.0 GATE-P0 已过;§10.5 明标 notify.rs 平铺单文件;
§11.4-11.7 的 17 条 Q 收敛为一节(均已由已实现代码回答);
§12 全节重写(LlmClient 非 AiClient、AiError 3 变体、无熔断、
无附件、无长邮件保护、read_email 实取全文送 LLM);§12.8/§12.9 合并。

docs/reference.md:删 delivery:pending:{stream} 换真实 stalwart:{jmap,telegram}
流名;删 cloudflare-worker/src/commands.rs 论断;键锚点校正;补网关
fail-closed 无开关说明。

AGENTS.md:§6 注册 retired.md;新增 §6.3 幽灵名规则(文档出现的模块/函数/
键必须是 src/ 真实存在,要留不存在的名字必须同时登记 retired.md);
teloxide 专属措辞通用化为「渠道层不引入第三方 Bot 框架类型」;
保留「不得声称使用未引入依赖」约束。

docs/roadmap.md:item 2/26 按真实数字重写(18 条路由 12 条经 Worker 转发,
不转发的 6 条中仅 2 条需业务直连);决策待定补处置建议。
README / README.zh-CN 阅读表加 retired.md,保持 1:1 对等。

待决策项收敛结论:真剩 2 项(Q-DEP-A Worker 白名单、Q-DEP-B 平台选型),
均为上线时运维选择,不影响代码结构与门禁,可挂起不产生技术债。

src/、web/、cloudflare-worker/ 零改动。基线仍 45 passed / 0 failed / 1 ignored。
2026-09-27 21:53:58 +08:00
anonsaber 0e10591534 docs(design): §8.1 BotError 目标形态代码块加消歧注释
Q5 一致处理:代码块内补一行「// 目标形态(teloxide 未引入)」,与上方免责互证,消除重构后唯一例外。
2026-09-27 20:49:53 +08:00
anonsaber 24cfbb45b0 docs: 重构文档体系 — README 瘦身中英对等 + 新增 reference.md 权威事实表
- README.md / README.zh-CN.md 瘦身至 ~100 行,五节 1:1 对等(是什么/5分钟/配置入口/安全边界/去哪读更多)
- 新增 docs/reference.md:Redis 键+TTL+写者+读者、错误 envelope+Retry-After、后端路由与网关白名单矩阵、env 三层等单一权威表(顶部 verified against ed93358)
- 新增 docs/roadmap.md 取代 docs/todo.md(只放缺口/阻塞/阶段目标/待决策)
- docs/design.md 保留架构语义,可查表改引 reference.md;修正 /search 未实现、XAUTOCLAIM 阈值矛盾、teloxide 幽灵引用等 9 处标注
- docs/deployment.md §8.1 重写,删除依赖不存在子命令/curl 的必然失败 healthcheck 示例
- AGENTS.md §6 登记 reference.md/roadmap.md + 冲突仲裁「以 reference.md 为准」+ 中英同轮同步/公共 API 变更规则
2026-09-27 18:55:38 +08:00
anonsaber ed93358e29 feat(notify): unify /ready not-ready 503 into the error envelope
- GET /ready: ready -> 200 + diagnostic report (unchanged success body);
  not ready -> 503 + {"error":"service_unavailable","request_id":id}
  + Retry-After: 30, consistent with the other public entry points
- drop the legacy {"status":"not_ready"} diagnostic block from the 503 body
  (backward compatibility not required)
- HTTP 200/503 semantics preserved, so Uptime Kuma HTTP(s) monitors
  (status-code based) keep working
- sync docs: README.md, README.zh-CN.md, docs/design.md, docs/deployment.md,
  AGENTS.md, docs/todo.md
2026-09-27 13:11:29 +08:00
anonsaber a8fad19daa docs: default README to English, reconcile docs, drop HANDOFF
- README.md is now English; add README.zh-CN.md with cross-links; both versions
  cover the same 40 sections and the same key facts
- correct stale docs/design.md 10.5.1 and docs/todo.md to the current code
  (XAUTOCLAIM threshold, malformed-entry ACK, error envelope now done)
- replace the temporary HANDOFF.md with docs/todo.md (unfinished work + blockers)
- adjust docs/deployment.md and AGENTS.md accordingly
2026-09-27 12:56:04 +08:00
anonsaber e18cecca16 refactor: remove legacy project identity 2026-09-25 10:40:10 +08:00
anonsaber c75858fda0 refactor: rename project to MessageWeave 2026-09-25 10:29:18 +08:00
anonsaber 9064fc5f1d fix: hide admin authorization in setup mode 2026-09-25 10:05:47 +08:00
anonsaber dceecaf2f4 feat: initial MessageWeave release 2026-09-25 09:14:04 +08:00