|
| 1 | +# Hash Labeling & Disambiguation Plan |
| 2 | + |
| 3 | +**Targets:** Phase 153 |
| 4 | + |
| 5 | +## Problem Statement |
| 6 | + |
| 7 | +Users encounter unlabeled hashes throughout gitsema output (CLI, MCP, HTTP, HTML) and naturally interpret them as **commit hashes** (the Git-level identifier they understand). In reality, these are **blob hashes** (content-addressed, internal implementation detail). This ambiguity causes confusion, especially when LLMs/Claude read the tool output and misidentify which hash type is which. |
| 8 | + |
| 9 | +### Example |
| 10 | +``` |
| 11 | +0.898 src/auth/login.ts [abc1234] |
| 12 | +``` |
| 13 | + |
| 14 | +A user sees `[abc1234]` and thinks: "This must be a commit hash." It's actually a blob hash. |
| 15 | + |
| 16 | +## Root Cause |
| 17 | + |
| 18 | +`src/core/search/ranking.ts:53` defines `renderResults()`, which outputs: |
| 19 | +```typescript |
| 20 | +let line = `${score} ${path} [${shortHash(r.blobHash)}]` |
| 21 | +``` |
| 22 | + |
| 23 | +This function is used by: |
| 24 | +- CLI: `gitsema search`, `gitsema first-seen`, `gitsema code-search` |
| 25 | +- MCP: `semantic_search`, `search_history`, `code_search` |
| 26 | +- HTTP: `/search`, `/first-seen` (text rendering mode) |
| 27 | + |
| 28 | +The unlabeled hash ripples across all these interfaces. |
| 29 | + |
| 30 | +## Solution Overview |
| 31 | + |
| 32 | +### Scope 1: Core Output Formatters (Highest Impact) |
| 33 | +- Update `renderResults()` to prefix blob hashes with `blob:` |
| 34 | +- Update `renderResultsByLevel()` (delegates to `renderResults()`, so fix cascades) |
| 35 | +- Update all HTML renderers to use explicit type labels |
| 36 | + |
| 37 | +### Scope 2: Documentation & Clarity (Zero Code Impact) |
| 38 | +- Update OpenAPI spec field descriptions to clarify blob vs commit hash |
| 39 | +- Update MCP tool interpretations to guide LLMs |
| 40 | +- Update skill documentation |
| 41 | + |
| 42 | +### Scope 3: Consistency Audit & Future-Proofing |
| 43 | +- Add unit tests for hash labeling |
| 44 | +- Verify all interfaces (CLI, MCP, HTTP, HTML, LSP) |
| 45 | +- Document hash-type conventions for future code |
| 46 | + |
| 47 | +## Changes by Area |
| 48 | + |
| 49 | +### Area 1: `src/core/search/ranking.ts` |
| 50 | + |
| 51 | +**Function: `renderResults()`** (line 53) |
| 52 | + |
| 53 | +Current: |
| 54 | +```typescript |
| 55 | +const hash = shortHash(r.blobHash) |
| 56 | +let line = `${score} ${path} [${hash}]` |
| 57 | +``` |
| 58 | + |
| 59 | +Proposed: |
| 60 | +```typescript |
| 61 | +const hash = shortHash(r.blobHash) |
| 62 | +let line = `${score} ${path} [blob:${hash}]` |
| 63 | +``` |
| 64 | + |
| 65 | +**Why:** Single change fixes CLI, MCP, HTTP text output. |
| 66 | + |
| 67 | +**Function: `renderResultsByLevel()`** (line 85) |
| 68 | + |
| 69 | +Current implementation delegates to `renderResults()`, so no change needed here — fix cascades automatically. |
| 70 | + |
| 71 | +**Function: `renderFirstSeenResults()`** (line 76) |
| 72 | + |
| 73 | +Also delegates to `renderResults()` — cascades automatically. |
| 74 | + |
| 75 | +### Area 2: HTML Renderers |
| 76 | + |
| 77 | +**File: `src/core/viz/htmlRenderer-search.ts`** (line 43, 61) |
| 78 | + |
| 79 | +Current: |
| 80 | +```html |
| 81 | +<th>Hash</th> |
| 82 | +... |
| 83 | +<td class="hash">' + esc(r.blobHash.slice(0,7)) + '</td> |
| 84 | +``` |
| 85 | + |
| 86 | +Proposed: |
| 87 | +```html |
| 88 | +<th>Blob Hash</th> |
| 89 | +... |
| 90 | +<td class="hash">blob:' + esc(r.blobHash.slice(0,7)) + '</td> |
| 91 | +``` |
| 92 | + |
| 93 | +**Why:** Makes it explicit in HTML output; aligns with text output convention. |
| 94 | + |
| 95 | +**File: `src/core/viz/htmlRenderer-evolution.ts`** |
| 96 | + |
| 97 | +Review for consistency; likely shows both `blobHash` and `commitHash`, so verify both are labeled clearly. |
| 98 | + |
| 99 | +**File: `src/core/viz/htmlRenderer-map.ts`** |
| 100 | + |
| 101 | +Review and update as needed. |
| 102 | + |
| 103 | +### Area 3: OpenAPI Documentation |
| 104 | + |
| 105 | +**File: `src/server/routes/openapi.ts`** |
| 106 | + |
| 107 | +Add field descriptions to clarify hash types in JSON responses: |
| 108 | + |
| 109 | +```json |
| 110 | +{ |
| 111 | + "blobHash": { |
| 112 | + "type": "string", |
| 113 | + "description": "Git blob OID (content-addressed hash, 40 hex chars). Identifies blob content uniquely." |
| 114 | + }, |
| 115 | + "firstCommit": { |
| 116 | + "type": "string", |
| 117 | + "description": "Commit hash when this blob was first indexed (40 hex chars). The earliest commit that introduced this blob." |
| 118 | + } |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +### Area 4: MCP Tool Interpretations |
| 123 | + |
| 124 | +**File: `src/core/narrator/interpretations.ts`** |
| 125 | + |
| 126 | +Add guidance for `semantic_search`, `code_search`, and related tools: |
| 127 | + |
| 128 | +``` |
| 129 | +When tools return text with hash values like [blob:abc123], the "blob:" |
| 130 | +prefix indicates a blob hash (content-addressed). If a firstCommit or |
| 131 | +commitHash is present, it's the earliest commit that introduced this blob. |
| 132 | +Do not confuse blob hashes (internal) with commit hashes (user-visible). |
| 133 | +``` |
| 134 | + |
| 135 | +### Area 5: Tests |
| 136 | + |
| 137 | +**File: `tests/ranking.test.ts`** |
| 138 | + |
| 139 | +Add tests: |
| 140 | + |
| 141 | +```typescript |
| 142 | +describe('renderResults hash labeling', () => { |
| 143 | + it('should prefix blob hashes with "blob:" for clarity', () => { |
| 144 | + const results = [{ |
| 145 | + blobHash: 'abcdef0123456789abcdef0123456789abcdef01', |
| 146 | + paths: ['src/auth.ts'], |
| 147 | + score: 0.9 |
| 148 | + }] |
| 149 | + const output = renderResults(results, false) |
| 150 | + expect(output).toContain('[blob:abcdef01]') |
| 151 | + }) |
| 152 | + |
| 153 | + it('should show first-seen results with blob: prefix', () => { |
| 154 | + const results = [{ |
| 155 | + blobHash: 'abc...', |
| 156 | + paths: ['src/foo.ts'], |
| 157 | + score: 0.85, |
| 158 | + firstSeen: 1609459200 |
| 159 | + }] |
| 160 | + const output = renderFirstSeenResults(results, false) |
| 161 | + expect(output).toContain('[blob:abc]') |
| 162 | + }) |
| 163 | +}) |
| 164 | +``` |
| 165 | + |
| 166 | +## Implementation Order |
| 167 | + |
| 168 | +1. **Update `ranking.ts`** → `renderResults()` change (1 line) |
| 169 | +2. **Test** → `tests/ranking.test.ts` (verify all callers) |
| 170 | +3. **Update HTML renderers** → `htmlRenderer-*.ts` (visual consistency) |
| 171 | +4. **Update OpenAPI** → `openapi.ts` (API documentation) |
| 172 | +5. **Update MCP guidance** → `interpretations.ts` (LLM clarity) |
| 173 | +6. **Verify all interfaces** → CLI, MCP, HTTP, HTML, LSP |
| 174 | + |
| 175 | +## Acceptance Criteria |
| 176 | + |
| 177 | +- [ ] CLI: `gitsema search "auth"` output includes `[blob:...]` prefix |
| 178 | +- [ ] CLI: `gitsema first-seen` output includes `[blob:...]` prefix |
| 179 | +- [ ] CLI: `gitsema code-search` output includes `[blob:...]` prefix |
| 180 | +- [ ] MCP: `semantic_search` text output includes `[blob:...]` prefix |
| 181 | +- [ ] MCP: `code_search` text output includes `[blob:...]` prefix |
| 182 | +- [ ] HTTP: `POST /search?rendered=true` text includes `[blob:...]` prefix |
| 183 | +- [ ] HTTP: `POST /first-seen?rendered=true` text includes `[blob:...]` prefix |
| 184 | +- [ ] HTML: Search results show "Blob Hash" column or `blob:` prefix |
| 185 | +- [ ] HTML: Evolution results clearly distinguish blob vs commit hashes |
| 186 | +- [ ] Tests: `tests/ranking.test.ts` passes with new labeling tests |
| 187 | +- [ ] No breaking changes to JSON responses (field names unchanged) |
| 188 | +- [ ] No breaking changes to structured MCP responses (field names unchanged) |
| 189 | + |
| 190 | +## Testing Strategy |
| 191 | + |
| 192 | +### Unit Tests |
| 193 | +- `tests/ranking.test.ts`: Verify `renderResults()` and variants output `[blob:...]` |
| 194 | +- `tests/htmlRenderer.test.ts`: Verify HTML renderers show type labels |
| 195 | + |
| 196 | +### Integration Tests |
| 197 | +- CLI: Run `gitsema search`, `first-seen`, `code-search` and verify output |
| 198 | +- MCP: Verify `semantic_search` and `code_search` outputs via test harness |
| 199 | +- HTTP: Test `/search`, `/first-seen` with `rendered=true` |
| 200 | +- HTML: Generate HTML output and inspect visually |
| 201 | + |
| 202 | +### Backward Compatibility |
| 203 | +- JSON field names (e.g., `blobHash`, `firstCommit`) unchanged — no API break |
| 204 | +- Field descriptions added to OpenAPI (no breaking change) |
| 205 | +- Text output changed but is human-readable only (not parsed by clients) |
| 206 | + |
| 207 | +## Risk Assessment |
| 208 | + |
| 209 | +**Low Risk:** |
| 210 | +- Single-line change to core formatter cascades across all text outputs |
| 211 | +- No JSON/structured response changes, only field descriptions |
| 212 | +- No database schema changes |
| 213 | +- All existing tests should pass (no removal of fields) |
| 214 | + |
| 215 | +**Testing Required:** |
| 216 | +- Verify text output in all interfaces (CLI, MCP, HTTP, HTML) |
| 217 | +- Verify JSON structures unchanged in HTTP responses |
| 218 | +- Verify MCP tools work with new text formatting |
| 219 | + |
| 220 | +## Phasing |
| 221 | + |
| 222 | +This work should be a single phase: |
| 223 | +- **Goal:** Eliminate hash type ambiguity across all user-facing outputs |
| 224 | +- **Changes:** Core formatter, HTML, OpenAPI, tests, documentation |
| 225 | +- **Duration:** ~2–4 hours |
| 226 | +- **Review:** Verify text output consistency across all interfaces |
| 227 | + |
| 228 | +## Future Enhancements (Out of Scope) |
| 229 | + |
| 230 | +- Clickable links to blob/commit viewers in web UI |
| 231 | +- Add hash type to CSV/tabular exports |
| 232 | +- Show full 40-char hash vs abbreviated in different contexts |
| 233 | +- Hash validation/parsing utilities for user input |
0 commit comments