Annotate-Only Cross-Cutting Features¶
Problem¶
When a feature spans multiple entities (e.g., a project mentions another project, a ticket references another ticket, a transcript segment relates to another segment), there is a temptation to "do the right thing" automatically: copy the cross-reference into the target, update target status, append text, re-order outputs. This breaks the user's mental model:
- Single-source-of-truth becomes ambiguous (did this update come from the speaker, or from a heuristic?).
- Order dependencies emerge (target must be processed AFTER source).
- The user loses authority over what crosses entity boundaries.
Symptoms when this is violated:
- Target entity's was_passiert (or equivalent narrative field) drifts from what the speaker actually said about the target.
- Closures or status changes happen "automatically" from ambiguous phrases (e.g., "übernommen ins Handbuch" silently flips a project to closed).
- Pipeline produces different outputs depending on processing order.
Pattern: Annotate-Only¶
Cross-cutting features must annotate the source with a structured signal, never mutate the target.
Source entity (where the cross-reference was observed)
└─ adds: cross_references: [list of CrossRef]
├─ target_name
├─ target_id (when resolvable)
├─ kind (informational | status_relevant | absorption | ...)
├─ quote (verbatim source text)
└─ reason (one-line classifier rationale)
Target entity
└─ UNCHANGED. Pipeline produces identical output as if the cross-cutting
feature were disabled.
The downstream review/UI layer becomes the place where the user decides what (if anything) to copy where.
Why this works¶
- Order-independent: Source and target can be processed in any order. The pipeline stays single-pass.
- Reversible: Disabling the feature produces the same target output. Easy A/B comparison.
- User keeps authority: Cross-entity actions (close project, append note, change status) require an explicit user decision in review.
- Auditable: Every cross-entity effect has a paper trail (which CrossRef was acted on by whom).
Three layers, three guarantees¶
When implementing, separate concerns across at least three layers:
Layer 1 — Detection (cheap, deterministic): Rule-based pre-pass that emits candidates. Optimized for recall over precision. Fast enough to run inline.
Layer 2 — Classification (semantic):
LLM (or richer model) classifies candidates into kind and writes reason. Detection candidate list bounds the LLM's job.
Layer 3 — Action (user authority): Review UI presents the annotations. User decides whether to copy, ignore, or close.
If the LLM is unavailable, Layers 1+3 still produce a useful artifact (Layer 2's kind can default to informational or unclassified, and the review UI shows raw candidates). The feature degrades gracefully — it does not break the pipeline.
Status-preservation invariant¶
When a signal could suggest changing an authoritative field on the source (e.g., absorption suggests the source project is closable), the signal MUST NOT overwrite the authoritative field. Instead, it sets a suggestion field:
# WRONG — auto-overwrite
if cross_ref.kind == "absorption":
extraction.status_farbe = "erledigt" # silently flips status
# RIGHT — annotate, preserve authority
if cross_ref.kind == "absorption":
extraction.closure_suggested = (
f"Content moved to {cross_ref.target_project_name} — close?"
)
# status_farbe equals what rule-based detection produced. Unchanged.
The user (in review) is the one who decides whether the suggestion becomes a fact.
Where this came from¶
Pattern emerged during SPEC-069 (Cross-Referenz-Handling) for the V000-Projektupdates pipeline. The naive design would have:
- Detected "Notfallkoffer ins Handbuch übernommen" in the Notfallkoffer block
- Auto-flipped Notfallkoffer's status_farbe to erledigt
- Auto-appended a note to Handbuch's was_passiert
That design was rejected because: - The phrase is ambiguous. "Übernommen" could mean "absorbed" or "we covered the topic in the other meeting." - Auto-flipping status_farbe removes user authority over closures. - Appending to Handbuch creates an order dependency and an untraceable narrative drift.
Architecture-strategist review (during plan iteration 2) confirmed the annotate-only choice and added a Stage 6 visibility hint: when cross-references exist, the review UI shows a count badge so the user knows to look for them. This is the third guarantee — the user is informed there are decisions to make, but no decision is made for them.
Applicability¶
This pattern applies whenever a feature needs to surface a relationship between two entities and one of these is true:
- The relationship is detected by a heuristic that has any non-trivial false-positive rate.
- The downstream effect is destructive or hard to reverse (status change, closure, narrative mutation).
- The user is the authority on whether the relationship matters.
It does not apply when:
- The relationship is structurally certain (e.g., a foreign-key reference).
- Both entities are owned by the same actor in the same workflow step.
- The downstream effect is purely additive and reversible (e.g., adding a non-authoritative tag).
Related¶
- SPEC-069 (Cross-Referenz-Handling) — origin
- SPEC-065 (Status-Extraktion) — host pipeline
- SPEC-066 (Interaktive Review) — Layer 3 consumer
- SPEC-067 (Asana-API-Integration) — writes source-only, never auto-writes targets