Skill v0.1.0
currentAutomated scan100/100name: business-attr-ux description: Tiered confidence-table UX, confidence thresholds, and conflict-resolution protocol for confirming inferred service identity and business attributes. Use from /otel-business-attrs. version: 0.1.0
Business Attribute Inference UX
Confidence Tiers
Tier 1 — Auto-write (confidence ≥ 0.9)
Write immediately. Show a collapsed "✓ Auto-applied" block so the user can see what was written without being asked to confirm it.
Example sources that reach Tier 1:
service.versionfrom the language's manifest (package.json#version,
pyproject.toml [project].version, *.csproj's <Version>, *.gemspec's spec.version, pom.xml's <version>) → confidence 0.97. No manifest source for go — Go module versions come from VCS tags, not go.mod — so omit it there rather than guessing.
`service.name` is NOT derived here at all — see #57/#132. The scanner already resolved it under its own observed-name ladder (agents/repo-context-scanner.md) and recorded it as services[i].name with nameSource/nameConfidence, plus a conflicts[] entry if a source disagreed. Show it in the Auto-applied block as a carried-over fact, not a freshly computed Tier 1 candidate — computing a second answer from the manifest here is exactly how #132 overwrote an already-correct observed name (bootstrap-literal / env:OTEL_SERVICE_NAME) with a stale manifest name at auto-write confidence. A service.name conflict is resolved through the conflict-resolution protocol below, never by re-deriving it in this tier.
Tier 2 — Confirm (0.5–0.9)
Show in an approval table. Each row shows: attribute | proposed value | confidence | source. Each row has three actions: [A]pprove / [E]dit / [R]emove. User must explicitly approve before any Tier 2 value is written.
Example sources that reach Tier 2:
service.namespacefrom directory structure → confidence 0.71service.teamfrom CODEOWNERS → confidence 0.63 (CODEOWNERS may be stale)deployment.environment.namefrom CI config → confidence 0.55
Tier 3 — Propose (confidence < 0.5)
Show as flagged proposals. NEVER write without explicit user action. User must promote to Tier 2 by typing the row number (this acts as immediate approval — no second confirmation needed) or reject by typing 'r <number>'.
Example sources that reach Tier 3:
- Business transaction name inferred from route analysis → confidence 0.35–0.45
- Domain inferred from git remote URL → confidence 0.40
Business Attributes — Always Confirm
Any candidate business metric or business attribute is ALWAYS presented as Tier 2 (approval required), regardless of confidence score. Business semantics are NEVER assumed. This rule overrides the Tier 1 threshold.
`biz.` is a placeholder, never a name that gets written. Inference produces a bare shape like biz.checkout.conversion_rate, but semconv-discipline requires every custom attribute to carry a reverse-DNS prefix and names bare prefixes as WRONG. Both rules cannot be followed at once, so resolve it before the user sees the row rather than after they approve it:
- If
namespaceHintis known (or the user has supplied a namespace), **present candidates
already namespaced** — com.myorg.checkout.conversion_rate. What the user approves is then exactly what gets written.
- If no namespace is known yet, ask for it before presenting business candidates. That
question is Step 3 of /otel-business-attrs; run it first when there are candidates to show.
- If a candidate is somehow shown as
biz.*, say in the same breath thatbiz.is a
placeholder that will be replaced at write time, and record the pre-namespace form as candidateName alongside the final name.
Never write a bare biz.* name to the context cache: it is non-conformant, and the user approved a different string than the one that reaches disk.
Enforced, not just requested. Each businessAttrs entry written to .claude/otel-context.json MUST carry "confirmed": true, set only after the user explicitly approves it. The write-guard PreToolUse hook parses every write to otel-context.json and BLOCKS it if any business attribute is missing "confirmed": true. So an unconfirmed business attribute cannot reach disk even if this prose is ignored — write approved attributes with "confirmed": true, and never write rejected/unreviewed ones.
Confidence Thresholds
AUTO_WRITE_THRESHOLD = 0.9CONFIRM_THRESHOLD = 0.5
These are calibrated constants. Do not change them without user instruction.
Presenting the Confirmation Table
Format:
✓ Auto-applied (confidence ≥ 0.90):service.name = "checkout-api" [0.97 · already resolved by the scanner: bootstrap-literal]service.version = "1.4.2" [0.97 · package.json#version]⚠ Needs your approval (confidence 0.50–0.89):# Attribute Proposed Value Conf Source1 service.namespace "payments" 0.71 dir: services/payments/2 service.team "@payments-team" 0.63 CODEOWNERS line 4Actions: [A]pprove all [number] approve one [E number] edit [R number] remove→● Business attributes (always confirm) — namespace com.myorg:# Attribute Kind? Source1 com.myorg.checkout.orders_placed counter route POST /checkout (AST inference)2 com.myorg.checkout.conversion_rate gauge route POST /checkout (AST inference)3 com.myorg.checkout.customer_tier dimension route POST /checkout (AST inference)4 com.myorg.checkout.payment_duration histogram route POST /checkout (AST inference)Actions: [A]pprove [K number counter|gauge|dimension|histogram] set kind [R number] reject→
Each business attribute carries a `kind`, confirmed like everything else here — never guessed silently downstream. It decides how /otel-backend renders the attribute on a dashboard, and the counter/gauge split matters because the aggregation differs (a rate is only valid on a counter):
- `counter` — a monotonically increasing total you view as a rate:
orders_placed,
checkouts_completed, anything _total/_count. Rendered as rate(...) over time.
- `gauge` — a level / ratio / value read as-is, NOT rated:
conversion_rate,cart_value,
queue_depth, anything _rate/_ratio/_value. Rendered as the value directly (avg / latest).
- `dimension` — a low-cardinality attribute you break traffic down BY:
customer_tier,
plan, region. Rendered as a breakdown (facet / group-by) of request volume.
- `histogram` — a duration or size distribution:
payment_duration,upload_size,
queue_wait_time, anything _duration/_latency/_size/_bytes. Rendered as quantiles (p50/p95/p99), never as an average — an average is the one view that hides exactly what a histogram exists to show (a p99 that doubles while the mean holds flat is invisible on an average panel, which is the ordinary shape of a latency regression). Filing a duration as gauge plots its average instead and loses this; filing it as counter and rating it is meaningless. This is the same reasoning as the counter-vs-gauge split, one step further out.
Propose a kind from the candidate's shape (_total/_count → counter; _rate/_ratio/ _value → gauge; _duration/_latency/_size/_bytes, or an observed instrument unit of s/ms/By → histogram; a categorical noun → dimension), but it is a proposal the user can flip with [K <n> counter|gauge|dimension|histogram]. A high-cardinality identifier (an id, email, raw UUID) is none of these — it belongs in derived.highCardinalityAttributes, not here; do not accept it as a business dimension.
`unit` (optional, best-effort). When the source makes an instrument's unit directly observable (e.g. meter.CreateHistogram<double>("...", unit: "s", ...)), carry it through to the written entry — a quantile panel needs it to label its axis (ms, s, bytes), and it is otherwise only recoverable by re-reading the instrument definition in source. null when not observed; never guess one from the name.
After the user responds:
- "a" or "A" → approve all items in that tier (with their currently-shown
kind) - A number (e.g. "1") → approve that specific item
- "k 2 gauge" → set item 2's kind before approving (also accepts
histogram, e.g. "k 4 histogram") - "e 2" → edit item 2's value (ask for new value inline)
- "r 1" → remove item 1 from the proposal
Do NOT proceed to write until all Tier 2 items have been explicitly acted on, and every approved business attribute has a confirmed kind.
Conflict Resolution Protocol
When multiple sources disagree on the same attribute value, show a conflict block:
⚡ Conflict: service.name1. package.json#name → "checkout-api" [confidence 0.97]2. repo path → "services/cart" [confidence 0.70]3. Dockerfile LABEL → "cart-service" [confidence 0.88]Pick a number, or type a custom value:→
Rules:
- NEVER silently pick one source. Always surface the conflict.
- Write the chosen value with
"conflict_resolved": truein the context JSON. - The conflict itself is diagnostic — it indicates the team has not aligned on a service name.
Add a comment in the written manifest pointing this out.
Written Output Format
agents/repo-context-scanner.md owns the context-JSON schema — do not restate it here, or the two copies drift and later commands read whichever they happen to find. This section covers only the fields this skill is responsible for producing.
On approval, update .claude/otel-context.json in place, changing only:
{"namespace": "com.myorg","deploymentEnvironment": "production","confirmedAt": "<ISO-8601 timestamp>","businessAttrs": [{"name": "com.myorg.checkout.conversion_rate","candidateName": "biz.checkout.conversion_rate","kind": "gauge","unit": null,"source": "route POST /checkout (AST inference)","confidence": 0.42,"confirmed": true,"confirmedAt": "<ISO-8601 timestamp>"},{"name": "com.myorg.checkout.payment_duration","candidateName": "biz.checkout.payment_duration","kind": "histogram","unit": "s","source": "route POST /checkout (AST inference)","confidence": 0.42,"confirmed": true,"confirmedAt": "<ISO-8601 timestamp>"}]}
unit is always present (nullable) once this fix lands — null for every kind where it doesn't apply or wasn't observed, not just omitted. A cache written before unit existed simply lacks the key; treat an absent unit the same as null, never as a reason to skip the entry.
and, per service, the identity fields the user confirmed:
{"namespace": "payments","namespaceSource": "user-confirmed","namespaceConfidence": 0.71,"team": "@payments-team","teamSource": "user-confirmed","teamConfidence": 0.63}
plus resolvedValue / resolvedSource / resolvedAt / "conflict_resolved": true on each entry in conflicts the user settled.
All of these are user-owned fields in the cache ownership contract: a later re-scan carries them over verbatim and must never reset them to null or []. Mark a confirmed namespace or team "user-confirmed" as its source — that marker is what tells the scanner not to overwrite it with a fresh inference.
Every object in businessAttrs MUST include "confirmed": true — the write-guard hook rejects the whole file otherwise. Omit attributes the user rejected or did not review; never write them with "confirmed": false expecting them to be ignored. Never write a bare biz.* name; keep the pre-namespace form in candidateName.