Skip to content

Spec: Temporal Clarification Loop ("Telha Asks Back")

File: .ai/specs/integration/2026-07-04-temporal-clarification.md Status: Approved (final review 2026-07-04 by J. Porter — responder-assertion condition incorporated in v0.6) Owners: integration lane; reviewers: storage lane (bitemporal correction), workers lane (ambiguity detection), app-layer lane (bot surface, RBAC gate) Related: ingestion-provenance spec (spans, source versions), job-queue-leases spec (routing substrate), grpc-contracts spec (wire additions), version-record-model spec (append-only correction), claim-verification spec (clarifications are an evidence class; see §5 evidence classes), gdpr-erasure spec (attribution tombstoning, OQ-5), PR-030/031/032 substrate; product promise recorded in the sales brief 2026-07-04; review notes incorporated 2026-07-04 (v0.2).


1. Overview

Defines how temporal ambiguity detected at ingestion becomes a routed, RBAC-gated, one-tap question to the humans best placed to answer it (delivered in Teams or Slack), and how answers become governed evidence: append-only response records, severity-driven quorum for high-stakes facts, and a bitemporal correction of the target fact once the binding policy is satisfied. Framing principle: clarification is governed evidence capture, not chat. This spec owns four normative guarantees: who may be asked, what they may be shown, how answers bind (including quorum and conflict), and how pending uncertainty surfaces in answers. Detection heuristics per format belong to the worker specs; this spec owns the wire shape, lifecycle, routing, authorization, and binding policy.

2. Problem Statement

Ingestion meets documents whose validity cannot be pinned: "effective next quarter", a signature date and a countersignature date, undated scans, conflicting amendment versions. Without this loop: (a) the projection guesses silently and the guess becomes state indistinguishable from a confirmed fact, producing wrong-time answers with high confidence; (b) any ad-hoc "ask someone in chat" path leaks confidential span text to people outside the workspace, an RBAC bypass through the side door; (c) a hand-maintained routing table for "who knows about this document" is stale the week it is written; (d) answers given in chat evaporate: they never become queryable, versioned, provenance-bearing state, so the same question gets asked again next quarter; (e) without a quorum policy, a contract termination date can be rebound by a single tap from a single person, which is too weak as binding evidence for regulated workflows; (f) without an explicit conflict lifecycle, two authorised people answering differently produces either silent last-writer-wins or an undefined state.

3. Proposed Solution

Normative decisions, mechanism in later sections:

  1. Ambiguity MUST NOT block ingestion. The best-guess interval (first candidate) is applied at reduced confidence and the version is marked pending clarification; queries over it remain answerable and carry the pending caveat.
  2. Authority (who to ask) MUST be derived from the graph: source-span author, person named in the fact, source-record owner, in that rank order. The workspace steward is the only configured fallback. A standalone routing table is prohibited.
  3. A question containing span text MUST pass the same read authorization the recipient would need to query that span. A candidate who fails the gate is skipped with a machine-readable reason code; if no candidate passes, the steward receives a redacted question (document link, no span text).
  4. Answering is a write. A responder with editor rights or above produces a binding-capable response; a viewer's response is an attestation that requires steward or editor confirmation before it can count toward binding.
  5. Answers are recorded append-only as response records on the clarification. Corrections are append-only fact versions: the superseded belief remains reconstructable at its transaction time; nothing is edited in place, including steward overrides.
  6. External ingestion workers MUST NOT be able to lease clarification jobs (they carry routing metadata and question text). Delivery is reserved to the clarify-bot audience.
  7. Clarifications MAY require quorum depending on severity and policy. Critical-severity clarifications MUST NOT bind on a single response unless the tenant's steward-override policy explicitly permits it. Quorum satisfaction is itself recorded as evidence, and the corrected fact version is created only when the required confirmation policy is met.
  8. Ambiguity kind and severity are separate axes. Kind describes the detection pattern; severity describes the business risk and controls delivery urgency, quorum requirement, escalation, and fatigue handling. Workers may emit a severity hint; core applies tenant/workspace policy and owns the final severity.
  9. Any answer that materially depends on a pending-clarification fact MUST expose that uncertainty in its evidence chain (pending clarification id, candidates, applied-candidate confidence, quorum state). A guessed interval is never presented as a confirmed fact.
  10. Free-form ("Something else…") answers MUST be normalised into a valid structured interval before they can bind. An answer that cannot be safely normalised is recorded as a non-binding attestation, never silently coerced.

4. Architecture

worker: detect ambiguity ──Submission.ambiguities──▶ core: apply submission
  (kind, candidates, confidences,                      ├─ fact version (best guess, pending,
   severity hint)                                      │    applied_candidate_confidence)
                                                       ├─ CLARIFICATION node (state: open,
                                                       │    severity via tenant policy,
                                                       │    quorum_policy derived from severity)
                                                       └─ enqueue job clarify:temporal
                                                            (payload: ranked candidates)
app-layer clarify-bot (token audience "clarify")
  PollJobs kinds=["clarify:"] ──▶ lease job
    ├─ resolve candidate person ▶ aliases ▶ channel identity
    ├─ RBAC gate: span read-auth check ──fail──▶ skip + reason code ▶ next candidate
    └─ send quorum-aware one-tap card ────────▶ Teams / Slack
         ◀──────────── answer callback ────────
    POST /v1/clarifications/:id/answer ──▶ core:
      ├─ append response record (rights captured at answer time)
      ├─ evaluate quorum policy
      │    ├─ not required / satisfied ──▶ bind: new fact version (corrected validity,
      │    │                                confidence restored, provenance = clarification)
      │    ├─ pending ──▶ state partially_confirmed (202, quorum_remaining)
      │    └─ disagreement ──▶ state conflicted ▶ steward review event
      └─ audit events: response, quorum state, binding result
no eligible candidate ▶ redacted steward escalation (skip reasons attached)
unanswered past escalation window ▶ fail(retryable) ▶ backoff ▶ next candidate
ranking exhausted ▶ dead_lettered ▶ steward digest
steward override on conflicted ▶ override response ▶ new fact version (history preserved)

5. Data Models

Wire (Submission JSON, additive field). ambiguities: [{node_ref, kind, severity_hint?, candidates, detector_confidence, applied_candidate_confidence}] where node_ref is the submission-local node index (same convention as span refs), kind ∈ {relative_date, conflicting_dates, undated, low_confidence_extraction}, severity_hint ∈ {low, normal, high, critical} (optional; core policy wins), and candidates: [{valid_from?, valid_to?, label, span_ref?}] (µs ints; at least one candidate; first candidate is the detector's best guess and is applied to the fact version).

Two confidences, normative distinction. detector_confidence scores how sure the worker is that ambiguity exists. applied_candidate_confidence scores how likely the applied best-guess interval is correct. They MUST be modelled separately: the former gates clarification creation; the latter drives answer caveats, query confidence, and severity escalation.

Severity. severity ∈ low | normal | high | critical, assigned by core from tenant policy (worker hint advisory). Behaviour is fixed in v1: low = store pending, do not ask; normal = ask via digest; high = ask directly, steward notified; critical = ask directly, quorum required.

Provenance extension. Provenance gains clarification: Option<Uuid> (msgpack additive, skip-if-serializing-none, mirroring the spans pattern from PR-032; golden fixtures stay byte-stable).

CLARIFICATION node (system-reserved label). Clarifications are world state, not a new CF. Properties: {target: logicalId, kind, severity, candidates_json, state, asked: [personId], responses_json, quorum_policy_json, quorum_state, detector_confidence, applied_candidate_confidence, source_version_tx, span_start?, span_end?}. Edge CLARIFIES → target node. Node versioning gives the lifecycle history, tenancy, and queryability for free. Dedup identity: (source_version_tx, span, kind); re-ingesting the same source version MUST NOT reopen an answered clarification.

Response records (append-only, plural from day one). responses: [{response_id, responder: personId, rights_at_answer_time, answer_index? | answer_custom?: {valid_from, valid_to, source_text}, channel, message_ref, response_tx, binding_status ∈ binding_candidate | bound | attestation | conflicting | steward_override}]. When quorum is satisfied, the responses that counted are re-marked bound, so evidence rendering can name exactly which confirmations bound the correction. A responder MAY change their answer before binding: the new response is appended (never edited in place) and the responder's latest response is their active one for quorum evaluation. The single-answer shape (answered_by/answer_index at top level) is prohibited: quorum, conflicts, and steward review all require the plural model.

Quorum. quorum_policy: {required_confirmations, required_roles, allow_steward_override} derived from severity at creation; quorum_state ∈ not_required | pending | satisfied | conflicted. Conflict policy in v1 is steward_only: majority and weighted-quorum binding are not supported. Quorum is confirmation, not voting: if binding-capable responses disagree, the clarification MUST move to conflicted and no corrected fact version may be created until a steward resolves it; the steward's resolution is itself an append-only response with binding_status = steward_override, and all conflicting responses remain in the record. Unanimous, majority, role-weighted, and domain-owner-final policies are future candidates, gated on usage data showing conflicts are frequent enough to justify the policy complexity (see OQ-6 resolution).

Lifecycle states. state ∈ open | attested | partially_confirmed | answered | conflicted | expired | dead_lettered. open = no usable response; attested = only non-binding responses; partially_confirmed = binding-capable response(s) received, quorum not yet satisfied; answered = quorum satisfied (or not required) and correction bound; conflicted = binding-capable responses disagree, steward review required; expired = escalation window exhausted for the current candidate; dead_lettered = ranking exhausted, steward digest.

Job. Kind clarify:temporal in the jobs CF. Payload: {clarification_id, ranking: [personId]}. attempts indexes the ranking (attempt N asks ranking[N]); max_attempts = len(ranking) + 1; the final attempt is the redacted steward escalation; dead-letter feeds the steward digest.

PERSON aliases. Routing resolves a PERSON entity to delivery identities via alias properties (email, upn, aad_oid, slack_user_id). Alias resolution is tenant-scoped, always. Chat identities are delivery aliases only and grant no binding authority unless mapped to a verified enterprise identity (see OQ-3 resolution). Alias population (Entra/AAD sync first, chat profiles second) is a dependency of this spec, specified separately.

Evidence classes (consumed by claim-verification). Clarification activity extends the evidence taxonomy: source_span, source_version, system_extraction, human_clarification, human_attestation, steward_override, quorum_confirmation. Trace rendering distinguishes source-derived facts from human-confirmed, quorum-confirmed, and overridden ones.

6. API Contracts

gRPC. ambiguities rides in the Submission JSON (like seq/partial): deny_unknown_fields means an old core rejects it loudly rather than dropping it silently, so workers MUST gate emission on a clarify capability flag advertised in the job payload options (core adds it when clarify.enabled). PollJobs kind-prefix mapping: token audience "clarify" maps to ["clarify:"] and nothing else; ingestion-worker audiences continue to map to ["ingest:"] only (mirror of the PR-038 embed fence).

REST (core). - GET /v1/clarifications?state=&severity=&cursor= → tenant-scoped list. - POST /v1/clarifications/:id/answer body {answer_index | answer_custom: {valid_from, valid_to, source_text}, responder: personId, channel, message_ref}.

The answer endpoint MUST, in order: authenticate; resolve responder → PERSON (tenant-scoped); check tenant scope; capture bind rights at answer time; validate/normalise the answer (free-form must resolve to a structured interval); append the response record; evaluate quorum; bind only if policy is satisfied (new fact version); emit audit events for response, quorum state, and binding result. Repeat submission of the same (responder, answer) is idempotent and returns the prior outcome.

Responder assertion rule (approval condition, 2026-07-04). The answer endpoint enforces a strict rule on who may assert the responder. A caller using a service token with audience "clarify" may submit a responder that differs from the token subject, because the clarify-bot has already verified the Teams/Slack callback identity and mapped it to a PERSON. All other callers — including CLI and API-key callers — may not assert an arbitrary responder: they bind, attest, or override only as the authenticated principal associated with their token/API key. If a non-clarify caller supplies responder, core MUST reject the request with 403 RESPONDER_ASSERTION_FORBIDDEN (no response record, no fact version). This prevents a tenant API key from forging clarification evidence ("Sarah confirmed 12 June") on behalf of another person.

Outcomes:

Condition Response
Binding not required by policy, editor answer 200 {bound: true, fact_version_tx}
Quorum satisfied by this answer 200 {bound: true, fact_version_tx, quorum_satisfied: true}
First binding-capable answer, quorum still pending 202 {bound: false, partial_confirmation: true, quorum_remaining}
Viewer answer 202 {bound: false, attestation: true}
Binding-capable answer disagreeing with a prior one 202 {bound: false, conflict: true, requires_steward_review: true}
Free-form answer that cannot be normalised 422 CLARIFY_UNPARSEABLE_ANSWER (recorded as attestation only if policy says so)
Conflicting response arriving after binding 202 {bound: false, conflict: true, post_binding: true}: recorded as a conflicting attestation, steward notified; the bound fact version is untouched
Already bound and policy disallows further responses 409 CLARIFY_ALREADY_ANSWERED
Invalid state transition 409 CLARIFY_INVALID_STATE
Unknown id / out of tenant scope 404

Skip and escalation reason codes (machine-readable, audited). NO_READ_PERMISSION, NO_DELIVERY_ALIAS, ALIAS_AMBIGUOUS, DELIVERY_FAILED, DAILY_CAP_EXCEEDED, DIGESTED, TENANT_SCOPE_MISMATCH, RESPONDER_NOT_AUTHORISED. Every candidate skip, escalation, and delivery failure MUST emit one, plus a human-readable audit message; the steward digest lists them per clarification so admins can repair identity, RBAC, or connector gaps.

App layer. The clarify-bot holds a service token (audience "clarify"), performs the RBAC gate before every delivery using the same policy check as chat queries (rbac.ts allowlist; deny-by-default), and maps Teams/Slack callback identity → PERSON before calling the answer endpoint. Failure behavior: unresolvable alias → skip with reason code; delivery failure → retry within lease, then fail(retryable) to advance the ranking; empty eligible set → immediate redacted steward escalation with the skip-reason list attached.

7. UI/UX

Teams adaptive card / Slack Block Kit message: one-sentence question, candidate buttons labeled with date and origin ("12 June (signature)"), and a "Something else…" input. Cards are quorum-aware:

  • Normal, after answer: "✓ Recorded as evidence: 12 June, confirmed by Sarah, 4 Jul 2026."
  • Critical, on send: "This clarification affects a high-stakes date and requires two confirmations."
  • Critical, after first answer: "First confirmation recorded: 12 June (Sarah). One more confirmation is required before the correction becomes binding."
  • Critical, quorum satisfied: "✓ Recorded as binding evidence: 12 June, confirmed by Sarah and Ahmed, 4 Jul 2026."
  • Viewer attestation: "Your answer is recorded as an attestation. A steward or editor must confirm it before the fact is corrected."
  • Conflict: "A conflicting answer has been recorded. This clarification now requires steward review."

Deliveries are batched into a digest when more than clarify.digest_threshold questions target the same person inside the digest window. Answers that arrive for an already-resolved or superseded card get a "this clarification has moved on" reply and are recorded per the conflict rules (§6, §5 lifecycle). Operator fallback and forensics: telha clarify ls|show|answer|review CLI (answer requires editor scope, review requires steward scope; recorded with channel "cli"). The steward review surface is CLI-first in v1: list open critical / partially confirmed / conflicted clarifications, inspect delivery history and skip reasons, bind an override. Every delivery, skip, answer, attestation, quorum transition, conflict entry, steward notification, steward override, escalation, and binding emits an audit event through the app-layer audit sink (PR-042).

8. Configuration

[clarify] table: enabled (bool, default false), escalation_window_hours (48), max_candidates (3), per_person_daily_cap (5), digest_threshold (3), digest_window_minutes (30), redact_fallback (true), channels ("teams,slack").

[clarify.quorum]: enabled (true), critical_required_confirmations (2), required_roles (["editor"]), allow_steward_override (true).

[clarify.severity]: high_notifies_steward (true), plus optional kind→minimum-severity elevations (elevate.conflicting_dates = "high"), which is how "high-stakes kinds" are expressed without conflating the kind and severity axes. The severity→behaviour mapping itself (low pending / normal digest / high direct / critical direct+quorum) is fixed in v1, not configurable.

Per-kind job backoff override: clarify:temporal backoff base = escalation window (jobs spec allows per-kind overrides). Deliberately NOT configurable: the RBAC gate, append-only response and answer recording, the audience fence, tenant-scoped identity resolution, audit event emission, the pending-caveat visibility rule (§3.9), and history-preserving correction behavior (overrides always create new versions).

9. Alternatives Considered

  • Standalone routing/ownership table. Rejected: stale by construction, second source of truth. Graph-derived authority updates with every sync of the source systems, which is exactly the "live integration" requirement.
  • Email as the v1 channel. Rejected as primary: no one-tap semantics, weak identity binding, slow loops. May be added later as a fallback channel.
  • Blocking ingestion until clarified. Rejected: availability wins. Best-guess plus pending flag keeps the graph queryable and the caveat honest.
  • A dedicated clarifications CF. Rejected: clarifications are world state. Graph nodes give versioning, provenance, tenancy, and query for free; a CF would re-implement all four.
  • LLM auto-resolution of ambiguity. Rejected as a binding path: the goal is group-validated understanding with named accountability. The detector may rank candidates; only humans bind. Revisit only as a suggestion layer.
  • Broadcasting the question to a channel. Rejected: violates least-privilege (span text visible to the whole channel) and produces noise instead of accountability. Ranked individuals only.
  • Single-answer binding for critical facts (v0.1 position). Rejected on review 2026-07-04: a single tap is too weak as binding evidence when the ambiguity touches validity, expiry, obligations, liability, or entitlements. Quorum is a v1 trust feature; deferring it would undermine the product promise the loop exists to serve.
  • Single-answer data model with quorum bolted on later. Rejected: retrofitting answered_by into response records is a breaking model change; the plural, append-only response model costs little now and makes quorum, conflicts, and steward review natural.
  • Chat identity as binding authority. Rejected: a Slack display name is not evidence-grade identity. Chat surfaces route; verified enterprise identity (Entra/AAD-mapped PERSON) binds.

10. Implementation Approach

Four PRs, slotted into the build doc as Phase 3b (build doc v2.1): PR-060 = (1), PR-061 = (2), PR-063 = (3) (chat-delivery-surface spec; the Entra connector PR-062 precedes it), PR-064 = (4); ask-in-chat split out as PR-070 per the chat-delivery spec:

  • PR (1) core: Provenance field, CLARIFICATION label, severity assignment from tenant policy, dual confidences, response records, quorum policy/state + lifecycle transitions (incl. conflict detection and idempotent re-answer), answer endpoint with the §6 outcome matrix, free-form normalisation gate, steward override path, fact-version correction, clarify:temporal kind + audience fence + capability flag, audit hooks. This spec lands as the first commit.
  • PR (2) workers: detection in docling/tabular/email projections emitting ambiguities (kind, candidates with span refs and origin labels, both confidences, severity hint) behind the capability gate; detection rules specified per worker spec addenda. Workers never decide quorum policy.
  • PR (3) app-layer: clarify-bot package (Teams first, Slack second), RBAC gate wiring, identity mapping, quorum-aware cards, digest batching, skip reason codes, steward fallback, conflict messaging, answer callback, audit events, CLI fallback.
  • PR (4) steward review surface (recommended, CLI-first): list/inspect/override flows over the states and reason codes; a UI can follow without model changes.

Downstream fan-out (normative). A bound clarification is a normal version write and MUST trigger invalidation for temporal projections, verified-claim/evidence indexes, confidence metadata, and retrieval filters. Full re-embedding is required only when the semantic text used for embedding changes; mechanism: embed fan-out compares the assembled embed-text hash and skips when only the validity interval or provenance changed. Crash behavior: delivery is at-least-once within a lease; answer binding is idempotent on (clarification_id, responder, answer) so a re-delivered card cannot double-write.

11. Migration Path

Greenfield feature, additive everywhere: the Provenance field is optional with skip-if-empty (existing goldens unchanged); ambiguities is a new Submission JSON field gated on the core-advertised capability, so the deploy order is core before workers; no key-layout changes; feature ships disabled (clarify.enabled = false) until the bot surface exists. The response-record and quorum model ships in the first core PR, so no single-answer data ever exists to migrate. Rollback: disabling the flag stops enqueue and delivery; open CLARIFICATION nodes remain as inert, queryable state.

12. Success Metrics

Named tests, CI-gateable:

  • clarify_rbac_gate: two-workspace fixture; a question whose span text targets workspace A is never delivered to a workspace-B candidate (delivery-log assertion) and the redacted steward fallback fires with NO_READ_PERMISSION reason codes when no candidate passes.
  • clarify_bitemporal_correction: binding an answer creates a new fact version; an as-of query at (old valid time, pre-answer tx time) still returns the best-guess belief; a current query returns the corrected interval with restored confidence.
  • clarify_escalation: unanswered past the window advances to the next ranked candidate; exhausted ranking dead-letters and appears in the steward digest with per-candidate reason codes.
  • clarify_dedup: re-ingesting an identical source version does not reopen an answered clarification.
  • clarify_fence: an ingestion-worker token cannot lease clarify: jobs (mirror of the embed-fence test).
  • clarify_caps: the per-person daily cap and digest batching are enforced.
  • clarify_attestation: a viewer's answer returns 202, binds nothing, and becomes binding only after steward/editor confirmation.
  • clarify_quorum_required: critical clarification, one editor response → response recorded, no fact version, state partially_confirmed, 202 {quorum_remaining: 1}.
  • clarify_quorum_satisfied: two matching editor responses → quorum satisfied, state answered, corrected fact version created with clarification provenance, 200 {fact_version_tx, quorum_satisfied: true}.
  • clarify_quorum_conflict: two differing editor responses → both recorded, no fact version, state conflicted, steward review event emitted.
  • clarify_steward_override: override on a conflicted clarification appends an override response, creates the corrected fact version, leaves prior responses queryable, records steward_override evidence, emits audit.
  • clarify_freeform_validation: unnormalisable free-form answer → 422 (or attestation per policy), no fact version.
  • clarify_pending_answer_caveat: a query depending on a pending clarification still answers, carries the caveat, references the clarification id in the evidence chain, and shows reduced confidence.
  • clarify_temporal_reindex_only: a bound answer changing only the validity interval updates temporal and evidence indexes without triggering embed fan-out (embed-text hash unchanged).
  • clarify_gdpr_tombstone: erasing a responder tombstones attribution (asked[], responder ids, message refs), preserves the corrected interval, the role-level confirmation marker, and the reconstructable audit shape.
  • clarify_post_binding_conflict: a conflicting response arriving after binding is recorded as a conflicting attestation, notifies the steward, creates no fact version, and leaves the bound correction untouched.
  • clarify_responder_supersede: the same responder answering differently before binding appends a superseding response; only their latest counts toward quorum; nothing is edited in place.
  • clarify_answer_responder_assertion: a clarify-audience service token may submit a verified third-party responder; a normal API key submitting responder = <other person> gets 403 RESPONDER_ASSERTION_FORBIDDEN with no response record and no fact version; a CLI/API-key answer binds as the authenticated caller only.

13. Open Questions

  • OQ-1: Should asking be gated on the fact being load-bearing (referenced by active contracts/risks/queries) rather than on detector confidence alone? Resolved 2026-07-04: not a v1 gate. v1 prioritises via kind, severity, both confidences, caps, and digest batching (severity table in §5). A later version may raise priority when a pending fact becomes load-bearing, and may add query-triggered clarification ("I can answer, but this depends on an unresolved clarification. Ask Sarah now?"), which is likely more valuable than asking everything at ingestion.
  • OQ-2: Quorum (two confirmations) for high-stakes documents? Resolved 2026-07-04: yes, in v1. Critical severity requires quorum per [clarify.quorum]; the response-record model, partially_confirmed/conflicted states, and the §6 outcome matrix are the supporting structure. Reversal of the v0.1 stance, on review.
  • OQ-3: Identity alias population: Entra/AAD graph sync first, Slack profiles second? Resolved 2026-07-04: yes. Canonical identity comes from the corporate IdP (Entra/AAD: object id, UPN, verified email, groups); source systems contribute authorship/ownership; chat surfaces are delivery aliases only and never create binding authority without a verified enterprise mapping. Alias resolution is tenant-scoped, always. The Entra connector gets its own spec before PR (3).
  • OQ-4: Does a bound correction re-trigger embed fan-out for the corrected fact? Resolved 2026-07-04: yes, but targeted (§10): temporal/evidence/confidence invalidation always; full re-embed only when the assembled embed text changes.
  • OQ-5: GDPR: clarification records name people (asked, answered_by). Resolved 2026-07-04: erasure tombstones personal attribution (asked list, responder identity, message refs, channel callback metadata) while preserving the corrected fact interval, answer value, transaction times, evidence class, and a role-level confirmation marker ("confirmed by an editor-level responder, since erased"). Ties into the gdpr-erasure spec; clarify_gdpr_tombstone is the gate.
  • OQ-6: Should quorum policy support majority binding when responses conflict (e.g. 2-of-3), or is steward review the only conflict resolution? Resolved 2026-07-04: v1 = steward review only; majority binding is not supported. Majority policies invite ties, gaming, false certainty (all responders may share the same unclear source), audit weakness, and policy complexity; a clarification is evidence capture, not a poll. If authorised responses conflict, the clarification enters conflicted and no corrected fact version is created until a steward resolves it (recorded append-only as binding_status = steward_override). Majority, unanimous, role-weighted, or domain-owner-final policies may be revisited with usage data on conflict frequency and steward bottlenecks (§5 quorum block is the normative text).
  • OQ-7: When should query-triggered clarification (per OQ-1 resolution) be scheduled? Resolved 2026-07-04: after the Phase-3 trace surface lands. The capability depends on the answer path exposing pending-clarification dependencies interactively and safely: candidate interpretations, confidence caveats, current clarification state, and authorised actions to ask, nudge, or escalate. Until then, clarifications are ingestion-triggered and surface through the normal pending caveats (§3.9). When it lands: the query path MUST deduplicate against existing clarification records via the §5 dedup identity (possibly extended with target_logical_id) and MUST NOT bypass RBAC or quorum policy; Telha never silently sends a clarification as a background side effect of a query, the user sees and triggers the action.

14. Changelog

  • 2026-07-06 — v0.9: PR-064 pinned GDPR gaps closed (PR-081). The two limits recorded in the v0.8 as-built are now filled by the gdpr-erasure executor: (1) prior clarification NODE versions referencing the subject are physically purged (the current attribution-tombstoned version survives as the sole remaining version, keeping answer values, response tx times, rights-at-answer-time, binding statuses, quorum state, and the corrected fact version); (2) the clarify:temporal job-payload ranking has subject positions replaced with the nil UUID (position preserved so attempt indexing stays coherent). erase_person_attribution (current-version rewrite) is unchanged and is the semantics PR-081 replays. The PR-064 clarify_gdpr_tombstone test keeps recording the as-of-PR-064 gap (it exercises erase_person_attribution alone); the flip is proven end-to-end in tests/erasure_e2e.rs (clarify_gdpr_tombstone_extension), which runs the full erasure and asserts BOTH now pass — prior clarification node versions purged AND the clarify:temporal ranking nil'd. No change to the clarification loop itself.

  • 2026-07-05 — v0.9: as-built note for PR-063 (delivery surface; the full record lives in chat-delivery-surface v0.5). The loop is now END-TO-END: detection (PR-061) → routing job → clarify-bot DM card → answer endpoint → binding (PR-060) → steward review (PR-064). §12 delivery legs clarify_rbac_gate, clarify_escalation, and clarify_caps are green in the bot's suite. Core surface added for delivery: GET /v1/clarifications/:id (span excerpt, source ACL, askedAliases — candidate delivery identities resolved server-side) and the additive JobPayload.attempts field (grpc-contracts v0.7). Escalation-window semantics realised as lease-expiry advancement: the bot stops heartbeating after delivery; the reaper's attempts+1 + per-kind backoff (base = escalation window) IS the §4 candidate advancement, with in-lease skip-forward so skips never burn a window.

  • 2026-07-05 — v0.8: as-built record for PR-061 + PR-064 (status stays Approved; none of the four normative guarantees touched). PR-061 (worker detection): detection rules landed as §15 addenda to the three worker specs (docling v0.3 — document-level role-dated conflicts / anchored relative phrases / undated; tabular v0.4 — designated-validity- column cell readings, capped 5/job; email v0.3 — envelope-date integrity only, body detection deferred). Shared plumbing in workers_common: IngestionResultBuilder.add_ambiguity (mirrors core plan_ambiguities validation, camelCase wire), clarify_enabled capability gate, temporal.py date recognition (ISO + English textual forms only; slash/dash forms reserved to tabular). Workers emit NO severityHint in v1 — kind→severity elevation is tenant policy, and no worker rule currently justifies overriding it. PR-064 (steward surface, CLI-first): telha clarify queue (attention set = conflicted/expired/dead_lettered in any severity, plus open/ attested/partially_confirmed at high/critical — needs_steward_attention), telha clarify show gains delivery history (the clarify:temporal job joined by payload clarification_id: attempt→candidate mapping + event trail), and telha clarify erase-attribution --person (spec OQ-5): tombstones asked[] entries and responder ids to nil, drops message refs, sets channel "erased", scrubs the person's id from audit-event details, and appends an attribution_erased event — answer values, response tx times, rights-at-answer-time (the role-level confirmation marker), binding statuses, quorum state, and the corrected fact version all survive (clarify_gdpr_tombstone). RECORDED LIMIT: the op rewrites the clarification's CURRENT version; physical purge of prior node versions and of job-payload rankings is the gdpr-erasure redaction rewrite (PR-081) — the test pins the job-payload gap explicitly. New tests: clarify_gdpr_tombstone, clarify_delivery_history, clarify_steward_queue, clarify_attestation_then_editor_confirmation (the §12 clarify_attestation core leg; the RBAC-gated delivery legs of clarify_rbac_gate/clarify_escalation/clarify_caps remain PR-063's).
  • 2026-07-05 — v0.7: as-built record for PR-060 (core loop; status stays Approved). Deviations and refinements, none touching the four normative guarantees: (1) Dedup identity realised as (tenant, source identity, content hash, span, kind) — strictly stronger than source_version_tx, which changes on same-job crash-retry re-apply; identical source versions share content hashes, so §5's "same source version never reopens" holds and retries are also covered. (2) Responder rights source pinned: "clarify"-audience callers assert responder AND rights in the answer body (one trust chain — the bot verified the callback identity and resolved the role); API keys gain an optional principal binding (telha api-key create --person --role), bind only as it, and cannot answer without one; asserting rights from a non-clarify caller is the same 403 RESPONDER_ASSERTION_FORBIDDEN. The CLI is the local-operator trust boundary: --responder is declared explicitly, recorded with channel "cli". (3) One ambiguity per submission node in v1 (Provenance.clarification carries a single id); multi-ambiguity nodes are rejected as Invalid. (4) All severities above low enqueue the routing job at severity-mapped priority; digest-vs-direct batching is delivery-surface behaviour (severity rides on the clarification node). Low never enqueues (§5 table). (5) Post-binding responses: disagreeing → recorded conflicting attestation + steward notification (per matrix); an AGREEING duplicate from a new responder → 409 CLARIFY_ALREADY_ANSWERED (the "policy disallows further responses" row). Expired/dead-lettered clarifications still accept answers — a late human answer is better evidence than none. (6) Steward override requires the explicit stewardOverride flag (a steward's plain answer counts as a normal confirmation); allowed on conflicted state or wherever allow_steward_override permits. (7) clarify:temporal pacing lands as a per-kind backoff-base override in the job queue (base = escalation window, cap lifted to match) plus a per-job max_attempts = len(ranking) + 1 (additive JobRecord field). (8) Capability flag = options.clarify = true injected into every ingest job payload when clarify.enabled (core wins over worker-supplied options). (9) Authority ranking v1: depth-1 PERSON neighbours of the target, bucketed AUTHORED_BY/SENT_BY/LAST_MODIFIED_BY → MENTIONS/NAMES/TO/CC → OWNED_BY, first-seen order within buckets, capped at max_candidates; the steward fallback is the job's final attempt, never part of the ranking. (10) Pending caveat (§3.9) surfaces as pendingClarification on POST /v1/query records and related nodes; the GET-by-id surface is a follow-up. (11) Unparseable free-form answers are a plain 422 with NO attestation record (no policy knob in v1; §6's "if policy says so" is unexercised). (12) Core audit = append-only events embedded in the clarification node (its version history is the audit trail) + structured tracing; the app-layer audit sink wiring is PR-063's. (13) REST DTO casing follows the house camelCase convention (answerIndex, messageRef); §6's snake_case shapes are conceptual.
  • 2026-07-04 — v0.6: Approved (final cross-spec review by J. Porter, jp@densops.com). One condition incorporated: answer-endpoint responder assertion pinned — only "clarify"-audience service tokens may assert a third-party responder; all other callers bind as themselves; violations → 403 RESPONDER_ASSERTION_FORBIDDEN. Test clarify_answer_responder_assertion added. Cross-referenced from the chat-delivery-surface spec.
  • 2026-07-04 — v0.5: PR numbers filled in from build doc v2.1 Phase 3b (PR-060/061/063/064, ask-in-chat PR-070); no design changes.
  • 2026-07-04 — v0.4: OQ-6 and OQ-7 resolved per review addendum. (1) Conflict policy pinned: v1 is steward_only, majority/weighted binding explicitly unsupported ("quorum is confirmation, not voting"); future policy candidates recorded. (2) binding_status enum value override renamed steward_override to match the evidence class. (3) Audit event list extended with conflict entry, steward notification, steward override, and binding. (4) Query-triggered clarification deferred to post-Phase-3 with normative guardrails recorded in the OQ-7 resolution (dedup against existing records, no RBAC/quorum bypass, never a silent background side effect). All open questions are now resolved; none remain open.
  • 2026-07-04 — v0.3: completeness pass against the same review. (1) Responder may supersede their own pre-binding answer (append-only; latest counts). (2) Post-binding conflicting responses are recorded as conflicting attestations with steward notification (409 reserved for policy-disallowed responses). (3) binding_status gains bound so evidence rendering names the confirmations that bound. (4) Kind→minimum-severity elevations added to [clarify.severity] as the uncoupled expression of "high-stakes kinds". Tests clarify_post_binding_conflict and clarify_responder_supersede added.
  • 2026-07-04 — v0.2: review notes incorporated; status → In Review. (1) Quorum is v1: severity axis added (kind ≠ severity, decision §3.8), critical severity requires policy-driven multi-confirmation (§3.7), [clarify.quorum]/[clarify.severity] config. (2) Response records replace the single-answer shape: append-only responses[] with rights-at-answer-time and binding_status; lifecycle expanded to open/attested/partially_confirmed/answered/conflicted/expired/dead_lettered; conflict handling and steward override defined (history-preserving). (3) detector_confidence and applied_candidate_confidence split. (4) Pending-caveat visibility made normative (§3.9). (5) Free-form answers must normalise before binding (§3.10, 422 path). (6) Skip/escalation reason codes made normative and audited. (7) Evidence classes enumerated for claim-verification. (8) Fan-out targeted: temporal re-index always, re-embed only on embed-text change. (9) GDPR: attribution tombstoned, confirmation evidence preserved. OQ-1..5 resolved; OQ-6/7 opened; PR (4) steward review surface added.
  • 2026-07-04 — v0.1: initial draft for peer review.