Sync the stated-as-current gate numbers that the previous feature commit
(88f0923) shifted, per the same-round rule:
- Test baseline 104 -> 105 passed / 0 failed / 4 ignored across the
authoritative (charter §6.1) and deferring docs (design §Phase 0,
opengaps access-control, HANDOFF-push-401), with retest date 2026-10-14.
The new test `setup_status_reports_backend_label` is a normal (non-ignored)
test, so the ignored set is unchanged.
- opengaps anchor count 72 -> 49, matching what `run_all.sh` ANCHOR AUDIT now
reports (the design.md main.rs line-anchors were converted to symbol refs).
- AGENTS version baseline 3b36fe3 -> 88f0923 (gate-holder merged this batch;
88f0923 is the gate-green commit of the round). Charter Code Baseline
`fc686ab` left as-is — line-number anchor semantics were not bulk-reverified
this round, so claiming otherwise would overstate (§7.4).
GATE-DOCS 6/6 clean (49 line-refs, 0 errors); GATE-P0 stood at 105/0/4.
User feedback: any finite TTL causes push to silently stop working
eventually. Stalwart does not re-verify (RFC 8620 §7.2 sends the
verification request once at subscription creation). The key is
explicitly deleted by forget_push_subscription when the subscription
is retired, so no TTL is needed.
Changes:
- PUSH_SUBSCRIPTION_SECONDS: WEEK → 0 (never expires)
- remember_push_subscription: ttl_seconds=0 → SET without EX (Redis)
/ expiry=i64::MAX (MemoryState)
- ttl_contract_is_pinned test: assert 0
- docs: reference/opengaps/design updated to 'never expires'
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.
New mailbox onboarding no longer floods Telegram with the entire
mailbox history. The initial baseline walk (ReconcileCursor::Walk,
any position) now passes an EmailQueryFilter to list_emails_page that
bounds Email/query to messages received within the last 7 days and
not yet seen ($seen keyword, RFC 8621 §4.1.2).
- EmailQueryFilter { received_after: Option<i64>, unread_only: bool }
in src/domain/jmap.rs (backend-agnostic domain type).
- StalwartJmap::fetch_email_page builds a compound Filter::and from
the filter conditions (after + not_keyword "$seen").
- walk_baseline computes the 7-day cutoff once and passes it to
every page fetch, so resumed walks stay filtered.
- Replay(state) path returns early and never calls list_emails_page,
so /changes incremental is unaffected.
- const INITIAL_BASELINE_WINDOW_DAYS: u64 = 7.
- New test first_reconcile_filters_the_initial_baseline_to_recent_unread
pins the filter on every Walk page and the cursor transition to Replay.
- 3 doc anchors re-anchored (worker.rs:227→241, client.rs:485→513 ×2).
GATE-P0: 104 passed / 0 failed / 4 ignored.
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).
The retire-on-change commit inserted ~500 lines into notify.rs and ~110
into state.rs, shifting every anchor at or after the insertion point.
The ANCHOR audit only proves the referenced line exists and is
non-empty, so all of these passed while pointing at the wrong code:
deployment.md/.zh-CN register_telegram_webhook 1619 -> 1623
register_push 1464 -> 1468 (EN)
register_telegram_webhook 1439 -> 1623 (zh)
register_push 1315 -> 1468 (zh)
retired.md/.zh-CN registration_lock fmt 1328-1330 -> 1481-1483
register_push 1315 -> 1468
state.rs read_batch 491 -> 506
state.rs query_async 531 -> 546
state.rs XGROUP CREATE 499-507 -> 514-522
opengaps.md/.zh-CN get_reconcile_state 777 -> 792
set_reconcile_state 785 -> 800
acquire_lock 1076 -> 1130
Every target was verified against the source before editing: two of the
pre-existing anchors pointed inside a different function than the one
they named, and the EN deployment.md had both register anchors off while
the zh twin still carried the original pre-move numbers.
Also documents the new push:registration:current key in the Redis key
table (writer, readers, TTL, and why its literal suffix cannot collide
with the hex session_digest suffix) and carries the zh twin the EN
version already got. Mirror the retire-on-change contract in
deployment.zh-CN.md - the zh twin has its own callback section, which
the EN update skipped.
- 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.
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.
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.
real_redis_ttl_tests in src/state.rs closes the "no test asserts any TTL
value" gap for the three atomic write points that actually reach Redis:
- set_nx_ex SET NX EX, with .max(1) floor (Redis rejects a zero EX)
- retry_or_dlq EXPIRE KEYS[1] 86400, embedded in the Lua script
- set_ai_consent SET EX
Each is opt-in via REDIS_TEST_URL (#[ignore], mirrors the JMAP gating in
src/domain/jmap/client.rs:522); missing the env var prints 'skipped' and
returns, so CI stays green without credentials and the URL is never logged.
retry_or_dlq uses max_attempts=99 so the script returns before XADD/XACK and
no stream or group needs to be created.
opengaps.md item 2 is now blocked only on a reachable Redis URL. Two
defects in that section were fixed while touching it: the consent table
rendered 7 * 86_400 as 604_800 and 365 * 86_400 as 31_536_000 (arithmetic
right, literal wrong) and listed 300s/7200s values that do not exist in
the code - the sixth real consent TTL is 0 (revoke). 16 line anchors had
also drifted onto comments and neighbours; all are re-pointed. The line-
range audit passes 138 refs but only checks that a line is non-blank, not
that it holds the claimed content, which is how these slipped through.
GATE-P0 baseline moves to 88 passed / 0 failed / 4 ignored.
Item 1 asked for a downtime window to observe the newState semantics, but
jmap-client 0.4.2 exposes no reset/re-baseline signal field, so the only
observable outcome is a plain Err - the window would have proven nothing.
The real behavior was worse than the item described: Err propagated as
Err(()) with reconcile:state left untouched, so every cron retried the
same dead sinceState and reconcile answered 503 forever with no forward
progress. Fix landed in 269c8f6.
Only item 2 (TTL measurement) remains, and it needs nothing from the user.
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.
`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.
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.
GET /api/business-config returns the stored values with presence-only secrets, and PUT now merges a patch against the stored config instead of replacing it, so the SPA can show what is already set and send back only the fields that changed.
The SPA marks secrets as set-but-unreadable, edits chat_allowlist as an unordered set, and omits fields it did not touch; blank required fields keep the stored value, blank optional fields clear it. The read-back is Serialize-only, so a client cannot inject secrets_present.
Forward both paths at the gateway: /api/business-config accepted PUT only, so the read-back returned 405, and the preflight path was missing from the safelist entirely, so it returned 404. The SPA is served from the Worker domain, so the whole edit experience was unreachable behind the production gateway. Route methods are now asserted per path, which the safelist test never checked.
Also: /debug/config reports timezone again, the timezone select is styled like the other fields, and the docs follow the new contract.
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.
- 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)
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)
Every blocker now states the concrete action required, the owner, and the
expected result, instead of only explaining why it is blocked. The file
was answering 'what is broken'; nobody could tell from it what to run.
- new 你要做的事 table up front: 6 rows splitting each item between
operator action and my action, so the reader sees at a glance which two
items need a curl, which one needs an answer, which needs a downtime
window, and which need nothing
- item 1 Telegram: setWebhook + getWebhookInfo commands, allowed_updates
narrowed to message to match the only field the service parses
(message.text), and the rotation trap spelled out -- changing the SPA
secret without re-running setWebhook leaves Telegram holding the old
value, so real callbacks start failing with 401 silently, no error
and no alert
- item 2 PushSubscription: POST /api/push/register command plus the
split of responsibility. Registration is performed by this service
against JMAP, not in the Stalwart admin UI, and the verification code
comes back over the callback and is verified automatically, so the
operator never needs to obtain a code. Also documents the observable,
the failure-code decode table, and the disable endpoint
- item 3 rewritten: the operator action collapses to a single question
(which component does the public mapping), because no Worker is in
front of this deployment. The five TTL keys moved into a table with
exact keys, purposes, TTLs and line anchors, and the pending 900s /
verified 300s statuses are documented where they are set
- item 4 spelled out as a concrete downtime window: stop the service
while mail is still arriving, longer than Stalwart's delta retention,
without running POST /reconcile during it, then I reconcile and check
the cursor re-baselines instead of erroring
- corrected getWebhookInfo field: it returns last_error_date, not
last_error_time
- anchor audit caught two of my own bad anchors on the first run -- the
SPA-verification comment is at notify.rs:1163 and the callback URL
validation at :1174, I had guessed :1151 and :1163, both off by 12.
ANCHOR is now 272 refs / 0 errors, PATH 393
- GATE-DOCS 6/6 pass
- remove the 真实 Redis TLS 连接 blocker: user confirms the managed
Redis is connected; the redis crate has tokio-rustls-comp enabled so
rediss:// is a supported scheme, and /debug/redis + /ready both report
reachable/true on the live deployment (checked just before this commit)
- the old item bundled three things: rediss:// handshake, password URL
encoding, ACL password. The URL-encoding half was only ever covered by
a parse-only unit test pointing at example.invalid, which never
connects, so the handshake itself was the unverified part; the live
connection covers all three
- blockers now 5 -> 4; no cross-reference lost: docs/deployment.md:231
already documents the rediss://default:<url-encoded-password>@ form,
and :220 already draws the startup-env vs business-config hot-reload
line, so deleting the item drops no surviving fact
- docs/charter.md 6.6: the blocker enumeration mirrored opengaps.md and
must move with it, and the credential preamble ("无 Stalwart /
Telegram / 托管 Redis 凭据") was already stale since the Stalwart and
Telegram creds both verified live, so it now states what is actually
unverified, which is the inbound callback direction
- 代码缺口 has nothing to remove: its only remaining item is
SAF-DEBUG-ALLOWLIST, which is deliberately unimplemented by product
decision and src/debug.rs:233-235 still shows the empty-allowlist
short-circuit
- GATE-DOCS 6/6 pass: ANCHOR 267 (was 269, the item carried 2 refs),
SECTION-REF 224, PATH 390 (was 392)