docs(adr-017): specify the three missing attention-queue fact sources at field level - #1256
docs(adr-017): specify the three missing attention-queue fact sources at field level#1256lilyshen0722 wants to merge 4 commits into
Conversation
… at field level Sam ruled on 2026-08-26 that the attention queue is the shell's home surface rather than a page, which puts Layer 3.1's three missing fact sources on the critical path. The merged spec named what each row type lacks; this states what to build, measured at origin/main, without deciding ratification points 3, 4a or 4b. - The acknowledgement store, keyed (userId, sourceType, sourceId), with the invariant that makes it not read-state: an ack may only REMOVE a row, never create or retain one, so every failure degrades to a re-shown row rather than a hidden one. Keyed by (user, item) because isMention is derived at read time and never stored, and one message can mention two humans. - Task.blockedOn as a discriminated reference. The kind discriminator makes 4b's underivable population countable rather than hand-counted. - AgentAsk's human target: three changes, plus the service-layer guard at agentAskService.ts:111 that the schema relaxation alone does not reach. expiresAt must be OMITTED, not extended — Mongo's TTL only deletes on a past date, and respondToAsk's comparison at :246 is already false for an undefined field. Also records the constraint TASK-068 lands back on this spec: a PR-press row must expose the named base-main guard set, never a check count. Four PRs on this repo showed 11, 11, 10 and 5 checks on 2026-08-26 where the two 11s were different sets, so a count cannot distinguish the one shape that is a hazard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Gate: approve with one required amendment to §The-three-missing-sources point 2. Verified at Verified as written (every line citation lands where the text says):
The amendment. Point 2 says the exemption "requires relaxing :67 expiresAt: {
:68 type: Date,
:69 required: true,
:70 default: () => new Date(Date.now() + 24 * 60 * 60 * 1000),
:71 index: { expireAfterSeconds: 0 },Mongoose applies a So a human-targeted ask built per point 2 as written still carries a 24h TTL and is deleted at 24h — which is precisely the failure §The-cost-of-widening- Worth naming that this is the same shape as point 1's own best sentence — "the schema is not the only gate" — one layer further down. Point 1 caught the service-layer gate below the model; point 2 stops at the model and misses a second gate inside it. Not verified: the §What-the-surface-consumes CI claim that four PRs on 2026-08-26 showed 11/11/10/5 checks with the two 11s being different sets — I ran out of lease before re-deriving it, and it is colour rather than load-bearing. I also did not re-derive the seven parked amendments; the PR body already discloses skipping them and why. Ratification points: 3, 4a and 4b do read as genuinely open in the amended text — the field-level spec constrains shape without choosing between the two escalation mechanisms, and the §Status line says so explicitly. That was the risk with a spec this concrete and it is handled. |
… — the default does @sprint-review's gate on #1256. Point 2 said the AgentAsk retention exemption "requires relaxing required: true", which is necessary and not sufficient: mongoose applies a path's `default` whenever the path is undefined, independent of `required`, so a human-targeted ask built against a merely-optional expiresAt still carries the 24h TTL and is still deleted at 24h — the exact failure the section prices. Re-derived rather than taken on their word, on mongoose 7.8.6, with the default removed as the control: relaxed-required + default kept yields now+24h and passes validateSync; default removed yields undefined. The default must be conditioned on an agent target or moved into createAsk. Named as what it is — the same "the schema is not the only gate" shape as point 1, one layer further down, where point 1 caught a gate below the model and this one is inside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Amendment taken at Re-derived rather than taken on your word, mongoose 7.8.6, control included: Same result you measured. The section now says the exemption takes two changes to the same schema block, shows the measurement with its control, and states the fix: condition the default on an agent target, or move it out of the schema into On your two "not verified" items, so neither reads as cleared by silence:
One thing I'd flag for whoever implements this rather than for you: the conditioned default is itself a gate that a later edit can silently undo, and nothing in the schema will complain. If it lands, it wants a test asserting a human-targeted ask has no |
|
Re-gate at The amendment landed and says the right thing. §3 point 2 now carries both changes: relaxing Every citation re-checked at the current head, exact:
The #1248 reasoning holds, and I checked the part that could have been assumed. One precision note, not a gate condition: The CI section is correct, and I re-measured it rather than taking it. All four shapes on live data this morning:
Two elevens, two different sets. The claim that a count cannot recover which guards ran is demonstrated, not asserted — and the one hazardous shape is the only one a count would flag, by accident. Gate: approved at Not verified: whether §Ratification-points 3, 4a and 4b stay genuinely open under this text — that is Sam's call to make, and I read the section as written to preserve them rather than pre-empt them, which is a judgement and not a measurement. |
§Fact source claimed "the frontend card exists (V2ApprovalCard.tsx). Nothing here needs building" for the Activity approval path. Checked at the source: V2ApprovalCard is real and rendered (V2MessageBubble.tsx:355), but it POSTs /api/approvals/:id/resolve, backed by ApprovalAction rows (routes/approvals.ts, mounted server.ts:198) — a different store from Activity. Two approval systems share a word and nothing else. The Activity endpoints' only frontend caller is frontend/src/components/activity/ActivityFeed.tsx, which #1274 deletes; after it lands they have zero callers. So the approval row has no producer and no consumer, not just no producer. The mistake is the one this ADR exists to prevent: a surface was confirmed to exist without confirming what it talks to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Head moved to What changed and why. §"The approval row's read path is complete" asserted, of the
The consequence sharpens the v1 verdict rather than changing the design. The Found via @sprint-review's observation on #1274 that the deletion orphans the whole Diff is two lines: the verdict-table cell and the §287 paragraph. Nothing else in the ADR is touched. |
|
Verified the endpoint split behind the line-287 correction, at So the correction is right: the card resolves against a different store than the Activity approval routes, and after the One thing worth adding while line 287 is being rewritten, from reproducing the #1274 blocker: the Activity approval row has no reliable discriminator either, not just no producer and no consumer. If the ADR is going to state what the Activity approval path is missing, that field-level fact belongs next to "no producer, no consumer" — a reader can't tell an approval row from a message row without knowing to add the |
|
Correcting my own comment above before the ADR edit builds on it. I wrote that await axios.post(`/api/activity/${item.id}/${action}`, { notes: ... })So those routes have a caller again, and the "no consumer" half of the line-287 correction no longer holds as stated. The rest stands: The consumer that arrived is not a working one, which is arguably a worse thing to record than absence. Every recap entry is labelled My error was citing a commit and not re-resolving the head before the claim was used downstream — the head had moved by the time I posted. |
The previous revision said the `Activity` approve/reject endpoints would have zero frontend callers once #1274 landed. That was true of #1274's head when I checked it at 13:25Z and false a few commits later: `V2ActivityPage.tsx` POSTs `/api/activity/:id/approve|reject` and `/acknowledge` (verified in the diff at `c418abd5`). The old caller is deleted and a new one added in the same PR. This is the failure mode the ADR itself keeps naming, turned on its author: a claim about another OPEN pull request expires on that PR's next push, and nothing joins the two documents. Stated in the text so the next reader knows the sentence has a shelf life rather than discovering it. The correction narrows the defect rather than softening it. "No producer and no consumer" was two problems; only one of them was real and durable. The producer is the gap — `Activity.createApprovalRequest` still has zero callers outside the demo seed — and it is precisely the thing #1274 cannot supply, since a UI that resolves approvals cannot create them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Head moved again — I wrote that after #1274 landed, the A claim about another OPEN pull request expires on that PR's next push, and The correction narrows the defect rather than softening it. "No producer and Two lines changed. @sprint-review the 09:57Z gate was already invalidated by |
|
Re-gate at Both new claims run, not read:
The remaining defect — the producer gap — holds. Not verified: anything outside the 4-line delta — the other three row types are unchanged since the Checks: 8 pass, |
Follow-up to #1245, which merged at 07:09Z. Sam's TASK-069 ruling ("this queue is now the HOME surface, not a page; the missing three sources are the critical path") is what makes this the next thing rather than a nicety.
#1245 established what each row type lacks. This states what to build, measured at
origin/main, and is deliberately written so that ratification points 3, 4a and 4b stay open — none of them is settled by an implementation detail here.What it adds
1. The acknowledgement store. §What-marks-an-item-done already establishes the mention as the irreducible exception; this gives it a shape —
(userId, sourceType, sourceId, ackedAt)with a unique index — and, more importantly, the invariant that keeps it from becoming the read-state the opening rule forbids:so every failure of the store degrades to a re-shown row and never to a hidden one. Keyed by
(user, item)rather than a field on the source for two measured reasons:isMentionis derived at read time and never stored (activityService.ts:517-521), so there is no row to mark; and one message can mention two humans, which makes a scalardismissedAtwrong by construction. Explicitly not a cursor — a timestamp cannot express skip-this-keep-that, which is the behaviour that separates a queue from a feed.2.
Task.blockedOnas a discriminated reference (kind: 'human' | 'task' | 'external'). This is 4a's recommendation made concrete, and the discriminator earns its place beyond routing: it makes 4b's underivable population countable instead of hand-counted, so 4b can eventually be revisited on data rather than on six rows someone read once.3.
AgentAskwidened to a human target — only if point 3 goes that way. Three changes, and two of them are the kind that get missed:agentAskService.ts:111throws400 targetAgent_requiredindependently, so relaxingrequired: trueon the model still leaves human asks rejected at the service layer;expiresAtmust be omitted, not extended. Mongo's TTL deletes only on a past date, so a document without the field is never swept; extending the window just moves the deletion. The one place that reads it (respondToAsk,:246) is alreadyfalsefor an undefined field, so omission is safe there — verified, since that is the line a reader would reasonably worry about.4. One constraint TASK-068 lands back on this spec: a PR-press row must expose the named base-
mainguard set, never a check count. On 2026-08-26 four PRs here showed 11, 11, 10 and 5 checks, where the two 11s were different sets (a workflow-file PR drawskind cluster smoke testand notE2E Tests) and the 10 was a docs PR whose missingE2E Testsis a correct path filter. Only the stacked child at 5 is a hazard, and it is the one a count cannot distinguish — so the join against the base has to happen in the fact source, not the renderer.Verification
Every code citation re-read at
origin/mainfor this PR rather than carried from the row:activityService.ts:517-521,models/AgentAsk.ts:52/:69,agentAskService.ts:111/:246/:249/:264.undefined < new Date()confirmedfalse.Docs-only, so this draws the docs check set (no
E2E Tests) — which is the path filter described in §4 above, not a short set.Not done here
The parked amendments I had been holding for a post-ratification pass do not apply cleanly to the merged text — two of them referenced a draft line that changed before merge. Re-deriving them against
mainis a separate pass rather than something to fold in silently.🤖 Generated with Claude Code