From b3dd2c7cbb70c21bb0103ef0d4c90eb289fb55bd Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 1 Sep 2026 14:31:00 -0500 Subject: [PATCH] docs(engagement): the two walk defects, fixed -- and what `digest` is Follows docs#203, which recorded the two defects Phase 13's acceptance walk found and left them for placing. They are fixed in website#181, so Phase 13's as-built says so and says what the fix was rather than leaving a reader to find the PR. Two other files were describing a contract the code did not honour, which is why the second defect could exist at all: * BACKEND_DESIGN's `template_keys` column row said only that keys are shape-checked. It now says the one thing that was actually load-bearing -- `digest` is a key that is NOT a channel, because it names a MODE's body -- and that the rules validator rejected it until website#181, which made every rule shipping one unsaveable from the Rules screen, core's own included. * MODULE_API 2.4 already told modules they may point `template_keys` at `notify.digest` and never said what `digest` was. A module author reading it would reasonably have expected it to be a channel. One bullet, next to the generic-bodies one that sends them there. No contract changed; the docs were right and the code was wrong. No MODULE_API_VERSION bump for the same reason. Co-Authored-By: Claude --- website/BACKEND_DESIGN.md | 2 +- website/ENGAGEMENT.md | 11 +++++++++-- website/MODULE_API.md | 3 +++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/website/BACKEND_DESIGN.md b/website/BACKEND_DESIGN.md index 0c0baf9..b2b1281 100644 --- a/website/BACKEND_DESIGN.md +++ b/website/BACKEND_DESIGN.md @@ -508,7 +508,7 @@ is only harmless while the default is off. Existing subscriptions are carried ac | audience_segment_id | INT NULL | a composed segment (§5.1a). **Deliberately no FK** — see below | | max_sends_per_hour | INT NOT NULL DEFAULT 100 | the hard per-rule ceiling (§7.1 Q3), counted in `engagement_sends` and enforced before an outbox row is written | | channels | JSON NOT NULL | `['email','inapp']` — a rule may span channels | -| template_keys | JSON NOT NULL | `{ email: 'idoc-warning' }`. Keys are shape-checked, not existence-checked: templates are Phase 5 | +| template_keys | JSON NOT NULL | `{ email: 'idoc-warning' }`. Keys are shape-checked, not existence-checked: templates are Phase 5. Every key is one of the rule's `channels` **plus one that is not a channel at all**: `digest` names the body the digest worker renders for a rule whose email channel an individual set to digest mode, so it belongs to a mode and can never appear in `channels`. The rules validator rejected it until website#181, which made every rule shipping one — core's own Team and news rules included — unsaveable from the Rules screen | | conditions | JSON NULL | a small closed and/or/not grammar over the trigger's **declared** variables | | cooldown_seconds | INT NOT NULL DEFAULT 0 | 0 = no cooldown | | delay_seconds | INT NOT NULL DEFAULT 0 | the grace window (§4.2a) | diff --git a/website/ENGAGEMENT.md b/website/ENGAGEMENT.md index 320afec..4c30c1d 100644 --- a/website/ENGAGEMENT.md +++ b/website/ENGAGEMENT.md @@ -3852,8 +3852,15 @@ is the Phase 11b defect class staying fixed. key they never typed. The only way to save is to delete the digest body, which silently removes digest support from that rule. The two validators have to agree about what `digest` is. -Neither is fixed here: this phase's PR is the documentation of a cutover, and a core change belongs -in its own. Both are recorded for the org lead to place. +**Both are fixed in website#181.** `settings.db` gained `claim(key, value)` — the same +`INSERT IGNORE` as `seedDefault`, reporting its own `affectedRows`, so exactly one caller can win a +key — and both seeders now claim before inserting rather than stamping after. The trade the code +already documented is unchanged, only its order: a process that dies mid-loop leaves the group +stamped and partly seeded, which both functions already called the better failure. The stubs are +`claim`-shaped now and each suite gained a test that runs two seeders under `Promise.all`; reverting +the fix makes the module one report every rule inserted twice, which is how the fix was checked. And +`engagementRules.model.js` now admits `digest` as the one key that is a slot rather than a channel, +so the two validators agree; anything else is still refused, with a test in each direction. **Also still open:** nothing else. `.profile/README.md` (step 8) landed — the org lead called the engagement system a headline capability — and Phase 12's second finding became **Phase 14**. diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 56e502e..bb9e3f5 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -856,6 +856,9 @@ are in §1.1 under **1.9.0**; four things are contract rather than implementatio `notify.event` / `inapp.event` / `notify.digest` and author nothing — §4.6.1 property 1. Ship a bespoke body when the message has something to say that the structural projection cannot; a trigger whose message is "this happened, here is the link" should not have one. +- **`digest` is a slot, not a channel.** It names the body the digest worker renders for a rule whose + email channel an individual set to digest mode, so it is legal in `template_keys` and never appears + in `channels`. Every other key must be one of the rule's channels. - **It is not a send path.** Every value on the object is data. Core still decides who is told. **`onBoot(fn)` / `onShutdown(fn)`** — §2.5.