Skip to content

feat(analyzers): 245 per-entry deprecated semconv diagnostics + codefixes - #157

Merged
ANcpLua merged 1 commit into
mainfrom
claude/goofy-cohen-8f4c45
Apr 22, 2026
Merged

feat(analyzers): 245 per-entry deprecated semconv diagnostics + codefixes#157
ANcpLua merged 1 commit into
mainfrom
claude/goofy-cohen-8f4c45

Conversation

@ANcpLua

@ANcpLua ANcpLua commented Apr 22, 2026

Copy link
Copy Markdown
Owner

Summary

Ships the 4th semconv package — a Roslyn analyzer + codefix bundle with one stable diagnostic ID per deprecated semconv entry so LLM coding assistants cannot silently re-introduce deprecated attribute names.

  • 245 diagnostics QYLSC0001 – QYLSC0245, one per deprecated semconv entry
  • Per-diagnostic codefix — offers the canonical replacement inline
  • 3 analyzers: DeprecatedAttributeAnalyzer, MagicStringAnalyzer, UnknownConventionAnalyzer
  • 1 codefix DLL shipped inside the same analyzer assembly
  • Generated registryDeprecatedDiagnostics.g.cs emitted from eng/semconv/deprecated-lookup/master-programmatic.yaml via eng/semconv/tools/gen-deprecated-diagnostics/gen.py
  • eng/semconv/tools/orphan-scan/ — CLI that detects orphaned semconv references in any repo (used by PR Add Claude Code GitHub Workflow #1 to validate the runtime cutover)

25 files, +8,637 LOC.

Scope

Target achieved: 100%.

  • Package structure follows NuGet analyzer conventions: DLL at analyzers/dotnet/cs/…, no lib/, 44 KB nupkg
  • 26 analyzer + codefix tests pass (xUnit v3 MTP): Total: 26, Errors: 0, Failed: 0, Skipped: 0, Not Run: 0
  • Release build succeeds with 0 errors
  • 245 unique QYLSC IDs verified in DeprecatedDiagnostics.g.cs (QYLSC0001 → QYLSC0245)

Missing to 100%: nothing blocking. Release-tracking metadata (RS2008) and EnforceExtendedAnalyzerRules (RS1036) are best-practice warnings that can be addressed in a follow-up without blocking donation.

Risk

8% — very certain goal is met.

  • Tests exercise analyzer diagnostics and codefix application against synthetic compilations
  • The pre-squash WIP commit (`4b591759`, 5531 lines) was folded cleanly: post-squash tree matches pre-squash tree bit-for-bit
  • Residual risk is some rarely-used deprecated attribute missed by the registry scrape (out of 245, easy to add)

What's left to do or delete to reach 100%

Test plan

  • Backend CI green
  • Install the analyzer into a test project and confirm a deprecated attribute name triggers the expected QYLSCxxxx diagnostic
  • Apply the codefix and confirm the replacement matches the registry value

🤖 Generated with Claude Code

…ted diagnostics + codefixes

Adds Qyl.OpenTelemetry.SemanticConventions.Analyzers — a ship-ready analyzer+codefix
NuGet package that emits one stable diagnostic ID per deprecated semconv entry so LLM
coding assistants cannot hallucinate deprecated attribute names without a per-name fix.

Scope
- 245 diagnostics QYLSC0001–0245 (one rule per deprecated semconv entry)
- Per-diagnostic codefix — offers the canonical replacement name inline
- Three analyzers: DeprecatedAttributeAnalyzer, MagicStringAnalyzer, UnknownConventionAnalyzer
- Roslyn codefix DLL ships alongside the analyzer DLL
- DeprecatedDiagnostics.g.cs generated from YAML registry (gen-deprecated-diagnostics/gen.py)
- Analyzer project added to qyl.slnx
- Package README for dotnet pack metadata

Also
- eng/semconv/tools/orphan-scan/ — CLI to detect orphaned semconv references

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 22, 2026 13:06
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change introduces two new tool scripts in the eng/semconv/tools/ directory: a Python generator that reads a YAML registry of deprecated diagnostics and emits a corresponding C# source file with lookup tables and descriptor arrays, and an orphan-scan utility that discovers undefined semantic convention attribute references in C# source code by scanning for tag/attribute-setter method invocations against a loaded registry. Both tools include CLI argument handling and file I/O operations with error fallthrough on missing or malformed inputs.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Title check ❌ Error Title uses conventional commits format with 'feat' prefix and scope, but exceeds the 72-character limit at 73 characters. Shorten title to ≤72 characters while preserving essential content. Consider: 'feat(analyzers): 245 deprecated semconv diagnostics + codefixes'
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed Description comprehensively documents the PR scope, implementation strategy, test results, and known gaps against stated objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@eng/semconv/tools/gen-deprecated-diagnostics/gen.py`:
- Around line 318-324: Wrap the yaml.safe_load call in a try/except that catches
yaml.YAMLError and prints a deterministic error to stderr and returns non-zero;
after loading, validate that doc is a mapping with isinstance(doc, dict) and if
not print a clear error and return non-zero; then obtain entries =
doc.get("entries") or [] and validate isinstance(entries, list) (print error and
return non-zero if not); ensure all error messages reference the YAML file
context and use the same non-zero exit code path instead of allowing
uncontrolled tracebacks (refer to yaml.safe_load, yaml.YAMLError, doc, and
entries in your changes).
- Around line 276-283: The generator builds ByDeprecatedId as a case-insensitive
dictionary but doesn’t check for duplicate deprecated_id values in a
case-insensitive way; update gen.py to detect collisions before emitting
ByDeprecatedId by scanning entries_sorted for duplicate deprecated_id keys using
a case-insensitive normalization (e.g., .lower() or .casefold()), collect any
duplicates, and raise a clear exception (ValueError) listing the colliding
deprecated_id values so the process fails fast; refer to entries_sorted, the
deprecated_id field, and the ByDeprecatedId output generation (and entry_record)
to locate where to add this validation before the loop that appends the
dictionary entries.
- Around line 235-256: The sorting happens before backfilling deprecated_id from
deprecated_member_id which causes unstable ordering; before the sort into
entries_sorted, pre-normalize each entry in entries by checking if
e.get("deprecated_id") is falsy and if so setting e["deprecated_id"] =
e.get("deprecated_member_id") (and raising the same SystemExit if both are
missing), then perform the deterministic sorted(...) key on entries so
entries_sorted is computed from normalized IDs; keep the existing
VALID_KINDS/VALID_STATUSES validation after sorting or optionally validate
earlier but ensure the deprecated_id normalization occurs prior to calling
sorted.

In `@eng/semconv/tools/orphan-scan/__main__.py`:
- Around line 224-225: Before calling args.report.write_text(text, ...), ensure
the parent directory exists to avoid FileNotFoundError: call
args.report.parent.mkdir(parents=True, exist_ok=True) (or convert args.report to
a pathlib.Path first if it might be a string) immediately before the write;
update the code near the args.report.write_text call in __main__.py to create
the parent directories when needed.
- Around line 213-214: The current falsy check "if args.limit:" treats 0 as no
limit; change the condition to an explicit None check so a provided 0 truncates
to an empty list: update the block that slices all_hits (referencing args.limit
and all_hits in __main__.py) to use "if args.limit is not None" (or equivalent
explicit None comparison) before doing the slice.
- Around line 146-148: The call to path.relative_to(repo_root) in the loop that
processes results from discover_cs_files can raise ValueError when a returned
path lies outside repo_root; wrap the relative_to call in a try/except
ValueError and fallback to a safe representation (e.g., path.as_posix() or
path.resolve().as_posix()) so the scan doesn't crash. Specifically, update the
block that checks "if literal in registry.ids: continue" and sets "rel =
path.relative_to(repo_root).as_posix()" to catch ValueError, assign a fallback
to rel, and proceed; keep references to registry.ids, discover_cs_files results,
and repo_root intact.
- Around line 66-70: The except block that wraps "with path.open(...)" and
"yaml.safe_load(f)" is silently swallowing OSError and yaml.YAMLError; update
the exception handler to write a descriptive warning to stderr (or use the
logger) including the filename (path) and the exception details, e.g.
print(f"Warning: failed to load registry {path}: {err}", file=sys.stderr) or
process via logging, then continue returning 0 as before; reference the
try/except around "with path.open" and the call to "yaml.safe_load" when making
the change.
- Around line 44-46: The current _INVOCATION regex only matches plain
double-quoted literals; update the pattern in the _INVOCATION variable (which
uses TAG_METHODS) to allow optional C# string prefixes @ and/or $ (in either
order) immediately before the opening quote so it matches plain ("..."),
interpolated ($"..."), verbatim (@"...") and combined ($@"..." or @$"...)
literals; alternatively, if you prefer not to change matching behavior, add a
clear note to the module docstring near _INVOCATION explaining the limitation
and why interpolated/verbatim forms are not handled.

In `@eng/semconv/tools/orphan-scan/README.md`:
- Around line 10-12: The README's suggested python -m invocation uses the module
name "eng.semconv.tools.orphan-scan" which is invalid because of the hyphen;
update the README to either remove the broken `-m` example entirely or rename
the package directory (e.g., to orphan_scan) so the module name becomes valid,
and keep the working direct invocation that runs __main__.py; ensure any
references to the module name in README match the new package name if you choose
to rename.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d1ae714b-c700-4e7a-bebd-933406999f53

📥 Commits

Reviewing files that changed from the base of the PR and between ea49455 and efc9867.

⛔ Files ignored due to path filters (21)
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Analyzers/DeprecatedAttributeAnalyzer.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Analyzers/MagicStringAnalyzer.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Analyzers/TagMethodMatcher.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Analyzers/UnknownConventionAnalyzer.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/CodeFixes/DeprecatedAttributeCodeFixProvider.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/CodeFixes/MagicStringCodeFixProvider.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/InternalsVisibleTo.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Model/DeprecatedDiagnostics.g.cs is excluded by !**/*.g.cs and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Model/DeprecationIndex.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Model/IsExternalInitPolyfill.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Model/RegistryIndex.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Qyl.OpenTelemetry.SemanticConventions.Analyzers.csproj is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/README.md is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Tests/DeprecatedAttributeAnalyzerTests.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Tests/DeprecatedAttributeCodeFixTests.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Tests/GlobalUsings.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Tests/MagicStringAnalyzerTests.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Tests/Qyl.OpenTelemetry.SemanticConventions.Analyzers.Tests.csproj is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Tests/RoslynTestHelper.cs is excluded by none and included by none
  • packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Tests/UnknownConventionAnalyzerTests.cs is excluded by none and included by none
  • qyl.slnx is excluded by none and included by none
📒 Files selected for processing (4)
  • eng/semconv/deprecated-lookup/master-programmatic.yaml
  • eng/semconv/tools/gen-deprecated-diagnostics/gen.py
  • eng/semconv/tools/orphan-scan/README.md
  • eng/semconv/tools/orphan-scan/__main__.py
📜 Review details
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Frontend Coverage
  • GitHub Check: Agent
  • GitHub Check: Backend (.NET)
  • GitHub Check: Schema Drift (TypeSpec → Generated)
  • GitHub Check: Analyze (csharp)
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (1)
eng/**

⚙️ CodeRabbit configuration file

Build and deployment infrastructure. Review for: correct MSBuild property usage, Nuke build target dependencies, Docker multi-stage build efficiency, and CI/CD pipeline correctness. Flag hardcoded paths, secrets, or platform-specific assumptions.

Files:

  • eng/semconv/tools/orphan-scan/README.md
  • eng/semconv/tools/gen-deprecated-diagnostics/gen.py
  • eng/semconv/tools/orphan-scan/__main__.py
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
Repo: Alexander-Nachtmann/qyl PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-21T06:24:06.977Z
Learning: Applies to **/*.{csproj,package.json} : Do not replace external dependencies: `Microsoft.Agents.AI`, `Microsoft.Agents.AI.Hosting`, `Microsoft.Extensions.AI`, `ModelContextProtocol` 1.1.0, `OpenTelemetry` SDK 1.15.0 + Semantic Conventions 1.40, `DuckDB.NET` 1.5.0, `Base UI` 1.3.0 + `lucide-react`, `xUnit v3` + `Microsoft.Testing.Platform`.
Learnt from: CR
Repo: Alexander-Nachtmann/qyl PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-21T06:24:06.977Z
Learning: Applies to **/*{Chat,Agent,Client,Instrumentation,Telemetry}*.cs : Emit OTel GenAI semconv 1.40 with required attributes: `gen_ai.system`, `gen_ai.request.model`, `gen_ai.response.model`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, `gen_ai.operation.name`, `gen_ai.tool.call.id`, `gen_ai.tool.name`, `gen_ai.agent.name`, `gen_ai.agent.id`.
Learnt from: CR
Repo: Alexander-Nachtmann/qyl PR: 0
File: src/qyl.mcp/CLAUDE.md:0-0
Timestamp: 2026-04-21T00:49:29.212Z
Learning: Applies to src/qyl.mcp/src/qyl.mcp.generators/**/*.cs : `src/qyl.mcp.generators/` must emit `QylToolManifest` with `ToolTypes[]`, `ToolDescriptors[]`, and `CreateTools()`
Learnt from: CR
Repo: Alexander-Nachtmann/qyl PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-21T06:24:06.977Z
Learning: Applies to **/generators/**/*.cs : Use `IIncrementalGenerator` only. Enforce `ForAttributeWithMetadataName`, value-equatable models, raw strings over `SyntaxFactory`. Never store `ISymbol` in models.
Learnt from: CR
Repo: Alexander-Nachtmann/qyl PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-21T06:24:06.977Z
Learning: Applies to **/*.cs : Never hand-register a tool. Instead, add `[QylSkill]` + `[QylCapability]` attributes and let the generator handle DI registration, MCP tool registration, and capability catalogs.
📚 Learning: 2026-04-21T00:49:29.212Z
Learnt from: CR
Repo: Alexander-Nachtmann/qyl PR: 0
File: src/qyl.mcp/CLAUDE.md:0-0
Timestamp: 2026-04-21T00:49:29.212Z
Learning: Applies to src/qyl.mcp/src/qyl.mcp.generators/**/*.cs : `src/qyl.mcp.generators/` must emit `QylToolManifest` with `ToolTypes[]`, `ToolDescriptors[]`, and `CreateTools()`

Applied to files:

  • eng/semconv/tools/gen-deprecated-diagnostics/gen.py
📚 Learning: 2026-04-21T06:24:06.977Z
Learnt from: CR
Repo: Alexander-Nachtmann/qyl PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-21T06:24:06.977Z
Learning: Applies to **/generators/**/*.cs : Use `IIncrementalGenerator` only. Enforce `ForAttributeWithMetadataName`, value-equatable models, raw strings over `SyntaxFactory`. Never store `ISymbol` in models.

Applied to files:

  • eng/semconv/tools/gen-deprecated-diagnostics/gen.py
📚 Learning: 2026-04-21T06:24:06.977Z
Learnt from: CR
Repo: Alexander-Nachtmann/qyl PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-21T06:24:06.977Z
Learning: Applies to **/*{Chat,Agent,Client,Instrumentation,Telemetry}*.cs : Emit OTel GenAI semconv 1.40 with required attributes: `gen_ai.system`, `gen_ai.request.model`, `gen_ai.response.model`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, `gen_ai.operation.name`, `gen_ai.tool.call.id`, `gen_ai.tool.name`, `gen_ai.agent.name`, `gen_ai.agent.id`.

Applied to files:

  • eng/semconv/tools/gen-deprecated-diagnostics/gen.py
📚 Learning: 2026-04-21T00:49:29.212Z
Learnt from: CR
Repo: Alexander-Nachtmann/qyl PR: 0
File: src/qyl.mcp/CLAUDE.md:0-0
Timestamp: 2026-04-21T00:49:29.212Z
Learning: Applies to src/qyl.mcp/src/qyl.mcp.generators/**/*.cs : Use `IndentedStringBuilder.BeginBlock()` pattern instead of `Indent()/Outdent()` (which are internal) in generator code

Applied to files:

  • eng/semconv/tools/gen-deprecated-diagnostics/gen.py
📚 Learning: 2026-04-21T06:24:06.977Z
Learnt from: CR
Repo: Alexander-Nachtmann/qyl PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-21T06:24:06.977Z
Learning: Applies to **/*.{g.cs,g.tsp,g.sql,g.ts,g.yaml} : Never hand-edit `*.g.cs`, `*.g.tsp`, `*.g.sql`, `*.g.ts`, or `core/openapi/openapi.yaml`. Fix the generator input (TypeSpec model, attribute, routing table) instead.

Applied to files:

  • eng/semconv/tools/gen-deprecated-diagnostics/gen.py
📚 Learning: 2026-04-21T06:24:06.977Z
Learnt from: CR
Repo: Alexander-Nachtmann/qyl PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-21T06:24:06.977Z
Learning: Applies to **/tests/**{Generator,Generator.Tests}/**/*.cs : Test generators via `ANcpLua.Roslyn.Utilities` test infrastructure.

Applied to files:

  • eng/semconv/tools/gen-deprecated-diagnostics/gen.py
🔇 Additional comments (1)
eng/semconv/tools/orphan-scan/__main__.py (1)

85-85: Undocumented assumption in prefix concatenation logic needs clarification.

The condition "." not in name assumes that if an attribute id contains a dot, it's already fully qualified and should not be prefixed. This assumption is not documented and is never validated. While semantically reasonable, it lacks justification—especially since OTel's own deprecated registry contains multi-dot ids like db.cassandra.coordinator.id.

Either add an inline comment explaining why names with embedded dots are treated as pre-qualified, or verify this matches the upstream semconv spec's actual group/attribute structure (the .tools/semconv-upstream/ directory in this repo is empty, so upstream validation cannot be performed here).

Comment on lines +235 to +256
# Deterministic order: by (folder, kind, deprecated_id).
entries_sorted = sorted(
entries,
key=lambda e: (
e.get("folder") or "",
e.get("kind") or "",
e.get("deprecated_id") or "",
),
)

for i, e in enumerate(entries_sorted):
if e.get("kind") not in VALID_KINDS:
raise SystemExit(f"entry {i}: bad kind {e.get('kind')!r}")
if e.get("status") not in VALID_STATUSES:
raise SystemExit(f"entry {i}: bad status {e.get('status')!r}")
# enum_member entries carry their id under deprecated_member_id; normalize.
if not e.get("deprecated_id"):
member = e.get("deprecated_member_id")
if not member:
raise SystemExit(f"entry {i}: missing deprecated_id and deprecated_member_id")
e["deprecated_id"] = member

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize IDs before sorting to prevent rule-id churn.

Line 236 sorts entries before Lines 251-255 backfill deprecated_id from deprecated_member_id. That makes QYLSCxxxx assignment unstable for those entries if upstream YAML order changes.

Proposed fix
 def render(entries: list[dict]) -> str:
-    # Deterministic order: by (folder, kind, deprecated_id).
-    entries_sorted = sorted(
-        entries,
-        key=lambda e: (
-            e.get("folder") or "",
-            e.get("kind") or "",
-            e.get("deprecated_id") or "",
-        ),
-    )
-
-    for i, e in enumerate(entries_sorted):
+    normalized: list[dict] = []
+    for i, raw in enumerate(entries):
+        e = dict(raw)
         if e.get("kind") not in VALID_KINDS:
             raise SystemExit(f"entry {i}: bad kind {e.get('kind')!r}")
         if e.get("status") not in VALID_STATUSES:
             raise SystemExit(f"entry {i}: bad status {e.get('status')!r}")
-        # enum_member entries carry their id under deprecated_member_id; normalize.
+        # enum_member entries carry their id under deprecated_member_id; normalize before sort.
         if not e.get("deprecated_id"):
             member = e.get("deprecated_member_id")
             if not member:
                 raise SystemExit(f"entry {i}: missing deprecated_id and deprecated_member_id")
             e["deprecated_id"] = member
+        normalized.append(e)
+
+    # Deterministic order: by (folder, kind, deprecated_id).
+    entries_sorted = sorted(
+        normalized,
+        key=lambda e: (
+            e.get("folder") or "",
+            e.get("kind") or "",
+            e.get("deprecated_id") or "",
+        ),
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eng/semconv/tools/gen-deprecated-diagnostics/gen.py` around lines 235 - 256,
The sorting happens before backfilling deprecated_id from deprecated_member_id
which causes unstable ordering; before the sort into entries_sorted,
pre-normalize each entry in entries by checking if e.get("deprecated_id") is
falsy and if so setting e["deprecated_id"] = e.get("deprecated_member_id") (and
raising the same SystemExit if both are missing), then perform the deterministic
sorted(...) key on entries so entries_sorted is computed from normalized IDs;
keep the existing VALID_KINDS/VALID_STATUSES validation after sorting or
optionally validate earlier but ensure the deprecated_id normalization occurs
prior to calling sorted.

Comment on lines +276 to +283
lines.append(" public static readonly ImmutableDictionary<string, DeprecatedEntry> ByDeprecatedId =\n")
lines.append(" new Dictionary<string, DeprecatedEntry>(System.StringComparer.OrdinalIgnoreCase)\n")
lines.append(" {\n")
for i, entry in enumerate(entries_sorted, start=1):
rule_id = f"QYLSC{i:04d}"
record = entry_record(entry, rule_id)
lines.append(f' ["{cs_escape(entry["deprecated_id"])}"] = {record},\n')
lines.append(" }.ToImmutableDictionary(System.StringComparer.OrdinalIgnoreCase);\n\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail fast on duplicate deprecated_id collisions before emitting lookup.

The generator does not enforce uniqueness for deprecated_id in a case-insensitive keyspace, but ByDeprecatedId is generated as case-insensitive. This can break the “one diagnostic per deprecated entry” contract.

Proposed fix
     count = len(entries_sorted)
+    seen_ids: set[str] = set()
+    for i, e in enumerate(entries_sorted):
+        dep_id = (e.get("deprecated_id") or "").casefold()
+        if dep_id in seen_ids:
+            raise SystemExit(f"entry {i}: duplicate deprecated_id (case-insensitive) {e.get('deprecated_id')!r}")
+        seen_ids.add(dep_id)
+
     lines: list[str] = [HEADER.replace("{count:04d}", f"{count:04d}")]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eng/semconv/tools/gen-deprecated-diagnostics/gen.py` around lines 276 - 283,
The generator builds ByDeprecatedId as a case-insensitive dictionary but doesn’t
check for duplicate deprecated_id values in a case-insensitive way; update
gen.py to detect collisions before emitting ByDeprecatedId by scanning
entries_sorted for duplicate deprecated_id keys using a case-insensitive
normalization (e.g., .lower() or .casefold()), collect any duplicates, and raise
a clear exception (ValueError) listing the colliding deprecated_id values so the
process fails fast; refer to entries_sorted, the deprecated_id field, and the
ByDeprecatedId output generation (and entry_record) to locate where to add this
validation before the loop that appends the dictionary entries.

Comment on lines +318 to +324
with yaml_path.open("r", encoding="utf-8") as f:
doc = yaml.safe_load(f)

entries = doc.get("entries") or []
if not entries:
print("[gen-deprecated-diagnostics] no entries found", file=sys.stderr)
return 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether parse/shape guards are currently present.
rg -n "yaml\.safe_load|except yaml\.YAMLError|isinstance\(doc, dict\)|isinstance\(entries, list\)|doc\.get\(\"entries\"\)" eng/semconv/tools/gen-deprecated-diagnostics/gen.py

Repository: Alexander-Nachtmann/qyl

Length of output: 145


Add deterministic error handling for YAML parsing to prevent traceback-style CI failures.

Lines 318-324 lack protection against malformed YAML and incorrect document shape. Parsing errors and shape mismatches currently produce uncontrolled tracebacks instead of tool-level error messages.

Missing guards:

  • Exception handling for yaml.YAMLError
  • Root-level type validation (isinstance(doc, dict))
  • entries shape validation (isinstance(entries, list))

Wrap yaml.safe_load() in try-except, validate that the YAML root is a mapping, and confirm entries is a list before use.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eng/semconv/tools/gen-deprecated-diagnostics/gen.py` around lines 318 - 324,
Wrap the yaml.safe_load call in a try/except that catches yaml.YAMLError and
prints a deterministic error to stderr and returns non-zero; after loading,
validate that doc is a mapping with isinstance(doc, dict) and if not print a
clear error and return non-zero; then obtain entries = doc.get("entries") or []
and validate isinstance(entries, list) (print error and return non-zero if not);
ensure all error messages reference the YAML file context and use the same
non-zero exit code path instead of allowing uncontrolled tracebacks (refer to
yaml.safe_load, yaml.YAMLError, doc, and entries in your changes).

Comment on lines +44 to +46
_INVOCATION = re.compile(
r"\.(?:" + "|".join(TAG_METHODS) + r")\s*\(\s*\"([A-Za-z0-9_.\-]+)\"",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if any C# files use interpolated or verbatim strings with tag-setter methods
rg -n '(SetTag|AddTag|SetAttribute|AddAttribute|SetCustomProperty|SetBaggage)\s*\(\s*(\$|@)"' --type cs 2>/dev/null | head -20

Repository: Alexander-Nachtmann/qyl

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Check repository structure and look for C# files
echo "=== Repository structure ==="
git ls-files | head -30

echo ""
echo "=== Count of C# files ==="
git ls-files --cached | grep -E '\.cs$' | wc -l

echo ""
echo "=== Sample C# files ==="
git ls-files --cached | grep -E '\.cs$' | head -10

Repository: Alexander-Nachtmann/qyl

Length of output: 1151


🏁 Script executed:

#!/bin/bash
# Examine the Python script to understand TAG_METHODS and context
echo "=== Content of __main__.py ==="
cat -n eng/semconv/tools/orphan-scan/__main__.py | head -60

Repository: Alexander-Nachtmann/qyl

Length of output: 2089


🏁 Script executed:

#!/bin/bash
# Search for all tag-setter method calls in C# files (not just interpolated/verbatim)
echo "=== All tag-setter method calls in C# files ==="
rg -n '(SetTag|AddTag|SetAttribute|AddAttribute|SetCustomProperty|SetBaggage|\.Tag)\s*\(' --type cs | head -30

Repository: Alexander-Nachtmann/qyl

Length of output: 5161


🏁 Script executed:

#!/bin/bash
# Comprehensive search for interpolated strings in service and packages directories with tag methods
echo "=== Interpolated strings in tag methods (services/**/*.cs and packages/**/*.cs) ==="
rg -n '\$"[^"]*"' services/ packages/ --type cs -A 1 -B 1 | grep -E '(SetTag|AddTag|SetAttribute|AddAttribute|SetCustomProperty|SetBaggage)' | head -20

echo ""
echo "=== Verbatim strings in tag methods (services/**/*.cs and packages/**/*.cs) ==="
rg -n '@"[^"]*"' services/ packages/ --type cs -A 1 -B 1 | grep -E '(SetTag|AddTag|SetAttribute|AddAttribute|SetCustomProperty|SetBaggage)' | head -20

echo ""
echo "=== Any interpolated strings in services/ ==="
rg '\$"' services/ --type cs | wc -l

echo ""
echo "=== Any verbatim strings in services/ ==="
rg '@"' services/ --type cs | wc -l

echo ""
echo "=== Any interpolated strings in packages/ ==="
rg '\$"' packages/ --type cs | wc -l

echo ""
echo "=== Any verbatim strings in packages/ ==="
rg '@"' packages/ --type cs | wc -l

Repository: Alexander-Nachtmann/qyl

Length of output: 782


🏁 Script executed:

#!/bin/bash
# Check the exact scope of the script - what directories does it scan?
echo "=== Scanning directories mentioned in docstring ==="
sed -n '14,17p' eng/semconv/tools/orphan-scan/__main__.py

Repository: Alexander-Nachtmann/qyl

Length of output: 174


Regex pattern is incomplete for C# string literal variants.

The pattern only matches plain double-quoted string literals ("...") but ignores C# interpolated ($"...") and verbatim (@"...") strings. While no such usage currently exists in services/**/*.cs or packages/**/*.cs, the tool should handle these valid C# 14 patterns to avoid future false negatives if developers use SetTag($"{prefix}.attr", ...) or similar.

Current pattern
_INVOCATION = re.compile(
    r"\.(?:" + "|".join(TAG_METHODS) + r")\s*\(\s*\"([A-Za-z0-9_.\-]+)\"",
)

Update the regex to match interpolated and verbatim string prefixes, or document this constraint in the module docstring.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eng/semconv/tools/orphan-scan/__main__.py` around lines 44 - 46, The current
_INVOCATION regex only matches plain double-quoted literals; update the pattern
in the _INVOCATION variable (which uses TAG_METHODS) to allow optional C# string
prefixes @ and/or $ (in either order) immediately before the opening quote so it
matches plain ("..."), interpolated ($"..."), verbatim (@"...") and combined
($@"..." or @$"...) literals; alternatively, if you prefer not to change
matching behavior, add a clear note to the module docstring near _INVOCATION
explaining the limitation and why interpolated/verbatim forms are not handled.

Comment on lines +66 to +70
try:
with path.open("r", encoding="utf-8") as f:
doc = yaml.safe_load(f)
except (OSError, yaml.YAMLError):
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent error swallowing obscures registry load failures.

YAML parse errors and I/O failures are silently dropped with no diagnostic output. In a build/CI context, a malformed registry YAML would produce zero IDs with no warning, leading to false-positive orphan reports.

Consider emitting a warning to stderr when a file cannot be loaded.

Proposed fix
         except (OSError, yaml.YAMLError):
+            print(f"[orphan-scan] warning: failed to load {path}", file=sys.stderr)
             return 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eng/semconv/tools/orphan-scan/__main__.py` around lines 66 - 70, The except
block that wraps "with path.open(...)" and "yaml.safe_load(f)" is silently
swallowing OSError and yaml.YAMLError; update the exception handler to write a
descriptive warning to stderr (or use the logger) including the filename (path)
and the exception details, e.g. print(f"Warning: failed to load registry {path}:
{err}", file=sys.stderr) or process via logging, then continue returning 0 as
before; reference the try/except around "with path.open" and the call to
"yaml.safe_load" when making the change.

Comment on lines +146 to +148
if literal in registry.ids:
continue
rel = path.relative_to(repo_root).as_posix()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

relative_to raises ValueError if path escapes repo_root.

If discover_cs_files returns a path outside repo_root (e.g., via symlink resolution or misconfiguration), path.relative_to(repo_root) throws an unhandled exception, crashing the scan mid-flight.

Proposed defensive handling
-            rel = path.relative_to(repo_root).as_posix()
+            try:
+                rel = path.relative_to(repo_root).as_posix()
+            except ValueError:
+                rel = str(path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eng/semconv/tools/orphan-scan/__main__.py` around lines 146 - 148, The call
to path.relative_to(repo_root) in the loop that processes results from
discover_cs_files can raise ValueError when a returned path lies outside
repo_root; wrap the relative_to call in a try/except ValueError and fallback to
a safe representation (e.g., path.as_posix() or path.resolve().as_posix()) so
the scan doesn't crash. Specifically, update the block that checks "if literal
in registry.ids: continue" and sets "rel =
path.relative_to(repo_root).as_posix()" to catch ValueError, assign a fallback
to rel, and proceed; keep references to registry.ids, discover_cs_files results,
and repo_root intact.

Comment on lines +213 to +214
if args.limit:
all_hits = all_hits[: args.limit]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

--limit 0 treated as "no limit" due to falsy check.

if args.limit: evaluates to False when args.limit == 0, so --limit 0 won't truncate to zero orphans. Use explicit None comparison.

Proposed fix
-    if args.limit:
+    if args.limit is not None:
         all_hits = all_hits[: args.limit]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if args.limit:
all_hits = all_hits[: args.limit]
if args.limit is not None:
all_hits = all_hits[: args.limit]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eng/semconv/tools/orphan-scan/__main__.py` around lines 213 - 214, The
current falsy check "if args.limit:" treats 0 as no limit; change the condition
to an explicit None check so a provided 0 truncates to an empty list: update the
block that slices all_hits (referencing args.limit and all_hits in __main__.py)
to use "if args.limit is not None" (or equivalent explicit None comparison)
before doing the slice.

Comment on lines +224 to +225
if args.report:
args.report.write_text(text, encoding="utf-8")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Report file write fails if parent directory doesn't exist.

args.report.write_text(...) will raise FileNotFoundError if the parent directory is missing. Build scripts often specify output paths in directories created by prior steps—this is fragile.

Proposed fix
     if args.report:
+        args.report.parent.mkdir(parents=True, exist_ok=True)
         args.report.write_text(text, encoding="utf-8")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if args.report:
args.report.write_text(text, encoding="utf-8")
if args.report:
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(text, encoding="utf-8")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eng/semconv/tools/orphan-scan/__main__.py` around lines 224 - 225, Before
calling args.report.write_text(text, ...), ensure the parent directory exists to
avoid FileNotFoundError: call args.report.parent.mkdir(parents=True,
exist_ok=True) (or convert args.report to a pathlib.Path first if it might be a
string) immediately before the write; update the code near the
args.report.write_text call in __main__.py to create the parent directories when
needed.

Comment on lines +10 to +12
```bash
python3 -m eng.semconv.tools.orphan-scan --repo-root . --report orphans.json
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Documented -m invocation is invalid Python syntax.

eng.semconv.tools.orphan-scan contains a hyphen, which is not a valid Python module identifier. This command will fail with No module named eng.semconv.tools.orphan-scan. The direct __main__.py invocation on line 17 is the only working approach.

Either rename the directory to orphan_scan or remove the broken -m example.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eng/semconv/tools/orphan-scan/README.md` around lines 10 - 12, The README's
suggested python -m invocation uses the module name
"eng.semconv.tools.orphan-scan" which is invalid because of the hyphen; update
the README to either remove the broken `-m` example entirely or rename the
package directory (e.g., to orphan_scan) so the module name becomes valid, and
keep the working direct invocation that runs __main__.py; ensure any references
to the module name in README match the new package name if you choose to rename.

Comment on lines +150 to +161
var project = workspace.AddProject(ProjectInfo.Create(
projectId,
VersionStamp.Default,
"Test",
"Test",
LanguageNames.CSharp,
metadataReferences: new[]
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Attribute).Assembly.Location),
},
compilationOptions: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)));

private static async Task<(Document Document, ImmutableArray<Diagnostic> Diagnostics)> PrepareDocumentAsync(string code)
{
var workspace = new AdhocWorkspace();
Comment on lines +56 to +59
catch
{
// Suppress analyzer exceptions so they don't produce AD0001.
}
Comment on lines +90 to +93
catch
{
// Suppress analyzer exceptions so they don't produce AD0001.
}
Comment on lines +69 to +72
catch
{
// Suppress analyzer exceptions so they don't produce AD0001.
}
Comment on lines +89 to +92
catch
{
// Suppress analyzer exceptions so they don't produce AD0001.
}
Comment on lines +36 to +39
catch
{
// Degrade gracefully if the generated table failed to load.
}
Comment on lines +47 to +50
catch
{
// Never let static field init issues propagate.
}
Comment on lines +99 to +104
foreach (var type in ns.GetTypeMembers())
{
var expr = TryFindConstInType(type, attrId);
if (expr is not null)
return expr;
}
Comment on lines +139 to +144
foreach (var nested in type.GetTypeMembers())
{
var expr = TryFindConstInType(nested, attrId);
if (expr is not null)
return expr;
}
@ANcpLua
ANcpLua merged commit a2f2458 into main Apr 22, 2026
46 checks passed
@ANcpLua
ANcpLua deleted the claude/goofy-cohen-8f4c45 branch April 22, 2026 13:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Roslyn analyzer + codefix package that enforces OpenTelemetry semconv deprecations with one stable diagnostic ID per deprecated entry, plus tooling to generate/validate the registry.

Changes:

  • Introduces Qyl.OpenTelemetry.SemanticConventions.Analyzers (3 analyzers + codefix providers) and wires it into the solution.
  • Adds analyzer/codefix test suite (xUnit v3 MTP) and shared Roslyn test helpers.
  • Adds semconv tooling: deprecated-diagnostics generator + orphan-scan CLI, and checks in the deprecated registry YAML.

Reviewed changes

Copilot reviewed 23 out of 25 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
qyl.slnx Adds the analyzer project + test project to the solution.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Qyl.OpenTelemetry.SemanticConventions.Analyzers.csproj Defines the analyzer NuGet packaging layout and Roslyn dependencies.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/README.md Documents diagnostics, codefix behavior, and regeneration workflow.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/InternalsVisibleTo.cs Exposes internals to the test assembly.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Analyzers/DeprecatedAttributeAnalyzer.cs Analyzer that reports per-entry deprecated semconv diagnostics.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Analyzers/MagicStringAnalyzer.cs Analyzer that flags known-valid semconv IDs used as string literals.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Analyzers/UnknownConventionAnalyzer.cs Analyzer that flags OTel-lookalike typos and unregistered qyl.* IDs.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Analyzers/TagMethodMatcher.cs Shared helper to detect tag-setter invocations and extract key literals.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/CodeFixes/DeprecatedAttributeCodeFixProvider.cs Codefix provider for deprecated IDs (direct/alternative/removal strategies).
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/CodeFixes/MagicStringCodeFixProvider.cs Codefix provider that replaces magic strings with typed constant usage (or TODO fallback).
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Model/DeprecatedDiagnostics.g.cs Generated registry: 245 descriptors + lookup dictionary + mode/kind metadata.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Model/DeprecationIndex.cs Facade exposing deprecated-ID membership checks.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Model/RegistryIndex.cs Index of valid IDs/prefixes derived from replacement targets + well-known floors.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Model/IsExternalInitPolyfill.cs Adds netstandard2.0 polyfill needed for records/init-only support.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Tests/Qyl.OpenTelemetry.SemanticConventions.Analyzers.Tests.csproj Test project configuration and pinned Roslyn/xUnit package versions.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Tests/GlobalUsings.cs Global usings for test compilation.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Tests/RoslynTestHelper.cs Minimal compilation helper to run analyzers in unit tests.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Tests/DeprecatedAttributeAnalyzerTests.cs Tests for per-entry deprecated diagnostics and generated-table invariants.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Tests/MagicStringAnalyzerTests.cs Tests for magic-string diagnostics behavior.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Tests/UnknownConventionAnalyzerTests.cs Tests for unknown/typo and unregistered convention diagnostics.
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Tests/DeprecatedAttributeCodeFixTests.cs Tests codefix registration/application across replacement modes.
eng/semconv/tools/gen-deprecated-diagnostics/gen.py Generator that emits DeprecatedDiagnostics.g.cs from the programmatic YAML.
eng/semconv/deprecated-lookup/master-programmatic.yaml Checked-in upstream-derived deprecated semconv dataset (245 entries).
eng/semconv/tools/orphan-scan/main.py CLI to scan repos for orphaned semconv-like string literals and suggest fixes.
eng/semconv/tools/orphan-scan/README.md Usage docs for the orphan-scan CLI.
Comments suppressed due to low confidence (1)

packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Model/IsExternalInitPolyfill.cs:9

  • internal static class IsExternalInit; is not valid C# (there are no forward declarations). This will prevent the analyzer project from compiling on netstandard2.0. Define the polyfill as a real type with a body (the standard pattern is internal sealed class IsExternalInit { } in System.Runtime.CompilerServices).
namespace System.Runtime.CompilerServices;

internal static class IsExternalInit;


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +26 to +33
private static readonly DiagnosticDescriptor s_otelUnknown = new(
id: OtelUnknownId,
title: "Unknown OTel-namespaced attribute",
messageFormat: "Unknown OTel attribute '{0}'. Did you mean '{1}'?",
category: "QylSemanticConventions",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "The attribute ID starts with a known OTel namespace prefix but is not in the registry. This is likely a typo or a renamed/deprecated attribute.");

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s_otelUnknown messageFormat always includes "Did you mean '{1}'?". Since the analyzer sometimes has no suggestion, this produces awkward output (e.g., "Did you mean '(no close match)'?"). Consider using a different message when no suggestion exists (or separate descriptors) so users don't see a placeholder suggestion.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,74 @@
using Qyl.OpenTelemetry.SemanticConventions.Analyzers.Analyzers;
using Qyl.OpenTelemetry.SemanticConventions.Analyzers.Model;

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using Qyl.OpenTelemetry.SemanticConventions.Analyzers.Model; is unused in this test file. Removing it avoids unnecessary warnings/noise during builds.

Suggested change
using Qyl.OpenTelemetry.SemanticConventions.Analyzers.Model;

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +18
/// <summary>
/// Get a known-valid (replacement) ID from the deprecation index so the test
/// is data-driven and stays correct if the YAML changes.
/// </summary>
private static string AnyValidId()
{
// android.app.state is the replacement for android.state
return "android.app.state";
}

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AnyValidId() XML comment says the test is data-driven and will stay correct if the YAML changes, but the implementation returns a hard-coded value. Either make it actually pull a value from DeprecatedDiagnostics/RegistryIndex, or update the comment to reflect that it’s intentionally fixed.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +15
/// QYL-SEMCONV-003 — Fires when a string literal in a tag-setter is not in any known registry:
/// Warning if it starts with a known OTel prefix (likely a typo/drift), Info otherwise.
/// Suggests the closest valid ID when Levenshtein distance ≤ 3.

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML doc says this analyzer emits "Info otherwise" and labels the rule as QYL-SEMCONV-003, but the implementation only reports warnings (QYLSC003A / QYLSC003B) and is silent for non-OTel/non-qyl prefixes. Update the doc comment to match the actual IDs and behavior so consumers know what to expect.

Suggested change
/// QYL-SEMCONV-003 — Fires when a string literal in a tag-setter is not in any known registry:
/// Warning if it starts with a known OTel prefix (likely a typo/drift), Info otherwise.
/// Suggests the closest valid ID when Levenshtein distance ≤ 3.
/// Reports warnings for unknown semantic-convention IDs used in tag-setters.
/// Emits <c>QYLSC003A</c> for unknown attributes that start with a known OTel prefix,
/// and <c>QYLSC003B</c> for unregistered <c>qyl.</c> attributes.
/// Values outside those prefixes produce no diagnostic. For <c>QYLSC003A</c>, suggests
/// the closest valid ID when Levenshtein distance ≤ 3.

Copilot uses AI. Check for mistakes.

if (field.ConstantValue is string val && string.Equals(val, attrId, StringComparison.Ordinal))
{
var access = $"{type.Name}.{field.Name}";

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a constant match is found, the fix builds an access expression using only type.Name ($"{type.Name}.{field.Name}"). This can generate code that doesn’t compile if the type isn’t in scope or is in a nested namespace. Prefer a fully-qualified reference (e.g., global::...) derived from the symbols.

Suggested change
var access = $"{type.Name}.{field.Name}";
var containingTypeName = field.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var access = $"{containingTypeName}.{field.Name}";

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +16
python3 -m eng.semconv.tools.orphan-scan --repo-root . --report orphans.json
```

or, because the package lives outside a Python import root, the more robust:

```bash

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

python3 -m eng.semconv.tools.orphan-scan ... won’t work (hyphens aren’t valid in module names, and this directory isn’t importable as a package). Consider removing this form and documenting only the direct python3 eng/semconv/tools/orphan-scan/__main__.py ... invocation, or restructure the tool as an importable Python package.

Suggested change
python3 -m eng.semconv.tools.orphan-scan --repo-root . --report orphans.json
```
or, because the package lives outside a Python import root, the more robust:
```bash

Copilot uses AI. Check for mistakes.
correspond to any known OTel semantic-convention id (upstream or qyl-custom).

Run:
python3 -m orphan_scan --repo-root . --report orphans.json

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring suggests python3 -m orphan_scan ..., but there’s no orphan_scan module/package in this repo (the directory is orphan-scan/). Either document running the script by path, or add packaging (__init__.py, rename folder) so the -m invocation is valid.

Suggested change
python3 -m orphan_scan --repo-root . --report orphans.json
python3 eng/semconv/tools/orphan-scan/__main__.py --repo-root . --report orphans.json

Copilot uses AI. Check for mistakes.
"SetBaggage",
)

# Matches invocations like `.SetTag("x.y.z", ...)` — captures the literal text and start column.

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says the _INVOCATION regex "captures the literal text and start column", but the regex only captures the literal text (one capture group). Either update the comment or add a capture group if column data is intended to be reported.

Suggested change
# Matches invocations like `.SetTag("x.y.z", ...)` — captures the literal text and start column.
# Matches invocations like `.SetTag("x.y.z", ...)` — captures the literal text.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +14
| `QYLSC0001`–`QYLSC0245` | Warning | Each deprecated OTel attribute / metric / event / entity / enum member gets its own rule id so severity can be tuned per entry via `.editorconfig`. |
| `QYLSC002` | Info | Magic-string attribute id — suggests replacing with a typed constant. |
| `QYLSC003A` | Warning | String literal that looks OTel-namespaced but isn't in the registry; offers a Levenshtein suggestion. |
| `QYLSC003B` | Warning | Unregistered `qyl.*` attribute — must be registered under `eng/semconv/qyl/model`. |

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The non-deprecated diagnostic IDs (QYLSC002, QYLSC003A/B) are very easy to confuse with the per-entry IDs (QYLSC0002, QYLSC0003, …). Consider clarifying this in the table (or switching these to the same 4-digit format) to avoid misconfiguring .editorconfig severity settings.

Suggested change
| `QYLSC0001``QYLSC0245` | Warning | Each deprecated OTel attribute / metric / event / entity / enum member gets its own rule id so severity can be tuned per entry via `.editorconfig`. |
| `QYLSC002` | Info | Magic-string attribute id — suggests replacing with a typed constant. |
| `QYLSC003A` | Warning | String literal that looks OTel-namespaced but isn't in the registry; offers a Levenshtein suggestion. |
| `QYLSC003B` | Warning | Unregistered `qyl.*` attribute — must be registered under `eng/semconv/qyl/model`. |
| `QYLSC0001``QYLSC0245` | Warning | Each deprecated OTel attribute / metric / event / entity / enum member gets its own 4-digit rule id so severity can be tuned per entry via `.editorconfig`. |
| `QYLSC002` | Info | Standalone analyzer id (not `QYLSC0002`): magic-string attribute id — suggests replacing with a typed constant. |
| `QYLSC003A` | Warning | Standalone analyzer id (not `QYLSC0003`): string literal that looks OTel-namespaced but isn't in the registry; offers a Levenshtein suggestion. |
| `QYLSC003B` | Warning | Standalone analyzer id (not `QYLSC0003`): unregistered `qyl.*` attribute — must be registered under `eng/semconv/qyl/model`. |
> **Note:** `QYLSC002`, `QYLSC003A`, and `QYLSC003B` are separate analyzer ids. They are easy to confuse with the deprecated per-entry ids such as `QYLSC0002` and `QYLSC0003`, but they must be configured exactly as shown in `.editorconfig`.

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +104
[Fact]
public void All_245_rule_ids_are_fixableAsync()
{
// Guarantees that if the diagnostic fires, the codefix at least sees it — even when
// the mode has no auto-action. This is the contract: every deprecated ID is routable.
Assert.Equal(245, s_fix.FixableDiagnosticIds.Length);
foreach (var descriptor in s_analyzer.SupportedDiagnostics)
Assert.Contains(descriptor.Id, s_fix.FixableDiagnosticIds);
}

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test method name ends with Async but it’s not asynchronous (returns void). Renaming it (or making it return Task) will keep naming consistent and avoid implying there’s async behavior here.

Copilot uses AI. Check for mistakes.
ANcpLua added a commit that referenced this pull request Apr 22, 2026
Railway build failed with MSB9008 because the new semconv packages (shipped
in #156/#157 — runtime cutover + analyzer) were ProjectReferenced from
internal/qyl.instrumentation.csproj but the three service Dockerfiles never
COPY'd them into the build context.

Adds to each of services/{qyl.collector,qyl.mcp,qyl.loom}/Dockerfile:
- packages/Qyl.SemanticConventions/
- packages/Qyl.OpenTelemetry.SemanticConventions/
- packages/Qyl.OpenTelemetry.SemanticConventions.Incubating/
- packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/

Both as csproj-only (layer-cached restore) and full-directory copies.

Also wires Qyl.OpenTelemetry.SemanticConventions.Analyzers as an Analyzer
ProjectReference into qyl.collector, qyl.mcp, qyl.loom, and
qyl.instrumentation — the analyzer now fires QYLSC* diagnostics on any
deprecated OTel tag-setter call site in those projects. qyl.loom picks up
the three runtime semconv packages it was missing (needed for direct use
of typed attribute constants).

Housekeeping: drops a stray blank line in QylMcpServerRegistration.cs that
was inside a using block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants