M129 — lossy decode + OTEL fixture tests

Cleanup of two M128-adjacent items surfaced by /ork:assess. Closes #1587.
Issue #1587 Milestone M129 Predecessor M128 / #1585

1. Why sed 's|-|/|g' is unfixable

CC encodes project paths under ~/.claude/projects/ by collapsing both / and . to -. The collapse is lossy — three different real paths produce the same encoded directory name. There is no decoder that can recover the original path from the encoding alone. Try a path:

Encoded form (lossy)

// Stored as:

Naive sed 's|-|/|g' decode

// Returns (one of many possibilities):

All paths that produce the same encoding

2. The proper source — claude project purge --dry-run --all

CC stores canonical paths in ~/.claude.json and emits them verbatim in the --dry-run --all output. Parsing that is lossless.

# Stale-project detection — used by /ork:doctor Category 14 + /ork:dream STEP 8
claude project purge --dry-run --all 2>/dev/null \
  | grep -oE 'projects\["[^"]+"\]' \
  | sed -E 's/^projects\["//; s/"\]$//' \
  | while IFS= read -r p; do
      [ -n "$p" ] && [ ! -d "$p" ] && echo "$p"
    done

3. Fixture tests — what's now covered

tests/integration/test-otel-panels.sh ships 11 assertions across 5 categories. Wired into npm test via tests/run-all-tests.sh WITHOUT || true, so a regression in any documented jq query fails CI.

TestAssertsWhy it matters
Panel 7: 5 skills aggregatedfixture row countjq query groups by skill_name correctly
Panel 7: frontend-design proactive_ratio = 60integer division3 proactive of 5 total → 60% (not 60.0)
Panel 7: component-search nested_skill = 2filter accuracynested-skill counts are isolated correctly
Panel 8: top target = CLAUDE.mdsort ordersort_by(-.count) descending works
Panel 8: top count = 3aggregation3 events for CLAUDE.md collapse to one row
Panel 7: empty file produces []graceful fallbackno events → empty array, not crash
Panel 8: empty file produces []graceful fallbacksame — covers CC < target version case
dream/doctor no lossy decoderegex absenceblocks reintroducing printf | sed s|-|/|g pipeline
dream/doctor uses canonical sourceregex presencerequires claude project purge --dry-run --all

4. Why no # silent: known-noise tags

First draft used jq … 2>/dev/null || echo "[]" in tests, requiring a # silent: known-noise tag for the silent-failure check hook. That masks real query syntax errors. Replaced with the empty-file case — fixture exists, has zero matching events — so jq runs normally and any real error surfaces. Same coverage, no suppression.

Before (masking)

# needs # silent: known-noise tag
out=$(jq … 2>/dev/null \
  || echo "[]")

After (clean)

# empty file, no suppression
: > empty.jsonl
out=$(jq … empty.jsonl)
[[ "$out" == "[]" ]]