# 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:

Real 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.

Test | Asserts | Why it matters
--- | --- | ---
Panel 7: 5 skills aggregated | fixture row count | jq query groups by skill_name correctly
Panel 7: frontend-design proactive_ratio = 60 | integer division | 3 proactive of 5 total → 60% (not 60.0)
Panel 7: component-search nested_skill = 2 | filter accuracy | nested-skill counts are isolated correctly
Panel 8: top target = CLAUDE.md | sort order | sort_by(-.count) descending works
Panel 8: top count = 3 | aggregation | 3 events for CLAUDE.md collapse to one row
Panel 7: empty file produces [] | graceful fallback | no events → empty array, not crash
Panel 8: empty file produces [] | graceful fallback | same — covers CC < target version case
dream/doctor no lossy decode | regex absence | blocks reintroducing `printf | sed s|-|/|g` pipeline
dream/doctor uses canonical source | regex presence | requires `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" == "[]" ]]