Explicit blockers: record them as structured linked context and derive the active set #63

Closed
opened 2026-09-23 00:16:42 +00:00 by yuris · 0 comments
Owner

Context

docs/technical/domain-model.md says: "Blockers are explicit structured user facts in linked context, never a fabricated standard STATUS named BLOCKED."

#22 implements the matching half of that (a candidate is excluded when a canonical composite itm_… id appears in FilterContext.active_blockers) and tests it, but nothing in the tree derives that set: there is no way to record "this task is blocked by X" as a structured fact, so today active_blockers is always empty in practice and the exclusion never fires end-to-end. PR #62 discloses this honestly; this issue closes it.

What to build

  1. Record a blocker as an explicit, structured linked-context fact. Extend context capture (POST /v1/context / life context add, or an explicit kind/field on it) so a caller can assert "item A is blocked by B" as a fact, not as free text a model would have to interpret. Store the link by canonical composite item id, with the source event recorded as usual.
  2. Derive the set. A query that produces FilterContext.active_blockers for the policy filter (#22) from the journal — only explicit blocker facts count. Never infer a blocker from recalled prose, from a Jev judgment, or from an unresolved external RELATED-TO link, and never invent the standard BLOCKED status.
  3. Resolve a blocker explicitly. A blocker clears by an explicit later fact (or the supersession mechanism from #18), not by age or by silence. A resolved blocker must stop excluding the task, and the history must show why.
  4. Wire it into the next-task path (#24) so the filter's blocker boundary is exercised by a real command, with the exclusion reason observable.

Acceptance criteria

  • A caller can record a blocker for an item as a structured fact, and the source event is journaled.
  • The derived blocker set contains exactly the explicitly recorded, still-active blockers, matched by canonical composite item id.
  • A blocked task is excluded from next-task candidates with an explicit exclusion reason, end to end through the command path.
  • Recalled prose, model output and unresolved external RELATED-TO links never produce a blocker.
  • An explicitly resolved blocker stops excluding the task, and the resolution is journaled.
  • No BLOCKED status is introduced anywhere.

Notes

  • Reviewed by an independent reviewer agent. Verdict went REQUEST CHANGES → APPROVE across two heads.
  • Reviewer finding (major), fixed before merge: the first canonical-id predicate accepted any non-whitespace suffix after itm_ — a compiled repro returned true for itm_! and itm_not-an-id. Since it gated both recording and derivation, a typo'd blocked_by would have been journaled as a structured fact and then silently suppressed a real task in life next until explicit resolution. The item routes' rule is now one shared src/api/types.rs::is_well_formed_id (itm_ plus ≥8 RFC 4648 base32 characters, single shared alphabet const also used by base32_push), called by the route, the context link and derivation; malformed input → 422 invalid_input with no journal and no outbox row, and a malformed journal row is ignored rather than trusted.
  • Reviewer also confirmed: derivation exactness, append-only resolution via the supersession ledger (the original fact stays intact and the later event names it), that accepted model output is genuinely accepted before the no-blocker assertion, CLI observability of the blocked reason, unchanged blocker-free fingerprints, and no fabricated BLOCKED status anywhere in src/.
  • Repository-level limitation surfaced by the review (pre-existing, not introduced here): docs/ is excluded from git in this clone (.git/info/exclude:7), so the minimal cli-api.md addition documenting the blocked_by member and blocker paragraph is local-only and not part of the commit. The tracked command reference (skills/life/references/commands.md) carries the change.
  • Test count: 755 passed / 0 failed (743 baseline + 12 new).

Blocked by

None for the recording and derivation work; the end-to-end wiring in criterion 3 depends on #24 (life next) landing first.

Refs #22 (implements the matching half), #18 (linked context and supersession), #24 (consumes the set).

## Context `docs/technical/domain-model.md` says: *"Blockers are explicit structured user facts in linked context, never a fabricated standard STATUS named BLOCKED."* #22 implements the **matching** half of that (a candidate is excluded when a canonical composite `itm_…` id appears in `FilterContext.active_blockers`) and tests it, but nothing in the tree **derives** that set: there is no way to record "this task is blocked by X" as a structured fact, so today `active_blockers` is always empty in practice and the exclusion never fires end-to-end. PR #62 discloses this honestly; this issue closes it. ## What to build 1. **Record a blocker as an explicit, structured linked-context fact.** Extend context capture (`POST /v1/context` / `life context add`, or an explicit `kind`/field on it) so a caller can assert "item A is blocked by B" as a *fact*, not as free text a model would have to interpret. Store the link by canonical composite item id, with the source event recorded as usual. 2. **Derive the set.** A query that produces `FilterContext.active_blockers` for the policy filter (#22) from the journal — only explicit blocker facts count. Never infer a blocker from recalled prose, from a Jev judgment, or from an unresolved external `RELATED-TO` link, and never invent the standard `BLOCKED` status. 3. **Resolve a blocker explicitly.** A blocker clears by an explicit later fact (or the supersession mechanism from #18), not by age or by silence. A resolved blocker must stop excluding the task, and the history must show why. 4. **Wire it into the next-task path** (#24) so the filter's blocker boundary is exercised by a real command, with the exclusion reason observable. ## Acceptance criteria - [x] A caller can record a blocker for an item as a structured fact, and the source event is journaled. - [x] The derived blocker set contains exactly the explicitly recorded, still-active blockers, matched by canonical composite item id. - [x] A blocked task is excluded from next-task candidates with an explicit exclusion reason, end to end through the command path. - [x] Recalled prose, model output and unresolved external `RELATED-TO` links never produce a blocker. - [x] An explicitly resolved blocker stops excluding the task, and the resolution is journaled. - [x] No `BLOCKED` status is introduced anywhere. ## Notes - Reviewed by an independent reviewer agent. Verdict went REQUEST CHANGES → APPROVE across two heads. - **Reviewer finding (major), fixed before merge:** the first canonical-id predicate accepted any non-whitespace suffix after `itm_` — a compiled repro returned true for `itm_!` and `itm_not-an-id`. Since it gated both recording and derivation, a typo'd `blocked_by` would have been journaled as a structured fact and then silently suppressed a real task in `life next` until explicit resolution. The item routes' rule is now one shared `src/api/types.rs::is_well_formed_id` (`itm_` plus ≥8 RFC 4648 base32 characters, single shared alphabet const also used by `base32_push`), called by the route, the context link and derivation; malformed input → 422 `invalid_input` with no journal and no outbox row, and a malformed journal row is ignored rather than trusted. - Reviewer also confirmed: derivation exactness, append-only resolution via the supersession ledger (the original fact stays intact and the later event names it), that accepted model output is genuinely accepted before the no-blocker assertion, CLI observability of the `blocked` reason, unchanged blocker-free fingerprints, and no fabricated `BLOCKED` status anywhere in `src/`. - Repository-level limitation surfaced by the review (pre-existing, not introduced here): `docs/` is excluded from git in this clone (`.git/info/exclude:7`), so the minimal `cli-api.md` addition documenting the `blocked_by` member and blocker paragraph is local-only and not part of the commit. The tracked command reference (`skills/life/references/commands.md`) carries the change. - Test count: 755 passed / 0 failed (743 baseline + 12 new). ## Blocked by None for the recording and derivation work; the end-to-end wiring in criterion 3 depends on #24 (`life next`) landing first. ## Related Refs #22 (implements the matching half), #18 (linked context and supersession), #24 (consumes the set).
yuris closed this issue 2026-09-23 05:15:46 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/life-service#63
No description provided.