Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
id: bugfix-895
title: vscode-uppercase-area-group-he
protocol: bugfix
phase: pr
plan_phases: []
current_plan_phase: null
gates:
pr:
status: pending
requested_at: '2026-05-28T01:02:24.638Z'
iteration: 1
build_complete: false
history: []
started_at: '2026-05-28T00:56:37.908Z'
updated_at: '2026-05-28T01:02:24.638Z'
pr_ready_for_human: true
15 changes: 15 additions & 0 deletions codev/state/bugfix-895_thread.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# bugfix-895 thread

## Investigate
- Issue #895: Replace per-word capitalization with full uppercase for area group headers in VSCode sidebar.
- Current `formatAreaForDisplay` in `packages/core/src/area-grouping.ts` splits on `[-_\s]+`, capitalizes first char of each word, joins with space. Produces `Cross Cutting` for `cross-cutting`.
- Issue's before/after table shows `Cross-cutting (1)` → `CROSS-CUTTING (1)` (preserves hyphen). The simplest reading that matches the table: replace the entire pipeline with `area.toUpperCase()`. That preserves `-` and `_` and matches every example in the table.
- Plan: rename `formatAreaForDisplay` → `uppercaseAreaName`, implementation becomes `area.toUpperCase()`. Update test + one caller in `area-group-tree-item.ts`.
- Callers: `packages/vscode/src/views/area-group-tree-item.ts` (one site) + tests in `packages/vscode/src/test/area-grouping.test.ts`.

## Fix
- `packages/core/src/area-grouping.ts`: replaced multi-step pipeline with `area.toUpperCase()`; renamed `formatAreaForDisplay` → `uppercaseAreaName`; updated docstring.
- `packages/vscode/src/views/area-group-tree-item.ts`: import + call site renamed; updated docstring comment.
- `packages/vscode/src/test/area-grouping.test.ts`: suite renamed + assertions updated to uppercase expectations. Separator behavior changed: hyphens/underscores are now preserved (e.g. `cross-cutting` → `CROSS-CUTTING`, not `CROSS CUTTING`) — matches the issue's before/after table. The old "consecutive-separator collapsing" test was repurposed to lock in verbatim preservation.
- Verified: `pnpm --filter @cluesmith/codev-core build` (tsc), `pnpm --filter codev-vscode check-types`, `pnpm --filter codev-vscode lint`, `pnpm --filter codev-vscode test:unit` (49 pass), `pnpm --filter codev-vscode compile-tests` all clean.

23 changes: 10 additions & 13 deletions packages/core/src/area-grouping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,21 @@ import { UNCATEGORIZED_AREA } from './constants.js';
* `UNCATEGORIZED_AREA` sentinel) MUST continue to be used for
* bucketing (`groupByArea`), id stability, and `===` matchers.
*
* Rule: split on `-`, `_`, and whitespace; capitalize the first
* character of each word; rejoin with a single space. Examples:
* 'vscode' -> 'Vscode'
* 'cross-cutting' -> 'Cross Cutting'
* 'front_end' -> 'Front End'
* 'Uncategorized' -> 'Uncategorized' (no-op; sentinel is already
* single-word, first-char-upper)
* Rule: uppercase the entire string. Separators (`-`, `_`) are
* preserved as-is so the header reads as a single visual token
* matching VSCode's own container-label convention (EXPLORER,
* SOURCE CONTROL, etc.). Examples:
* 'vscode' -> 'VSCODE'
* 'cross-cutting' -> 'CROSS-CUTTING'
* 'front_end' -> 'FRONT_END'
* 'Uncategorized' -> 'UNCATEGORIZED'
*
* Purely structural: no hardcoded list of "known acronyms". Teams
* using Codev decide their own labeling semantics; a per-repo
* override map can be layered on top as a non-breaking extension.
*/
export function formatAreaForDisplay(area: string): string {
return area
.split(/[-_\s]+/)
.filter(w => w.length > 0)
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
.join(' ');
export function uppercaseAreaName(area: string): string {
return area.toUpperCase();
}

/**
Expand Down
37 changes: 18 additions & 19 deletions packages/vscode/src/test/area-grouping.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as assert from 'assert';
import { groupByArea, formatAreaForDisplay } from '@cluesmith/codev-core/area-grouping';
import { groupByArea, uppercaseAreaName } from '@cluesmith/codev-core/area-grouping';

interface AreaItem {
id: string;
Expand Down Expand Up @@ -81,36 +81,35 @@ suite('groupByArea', () => {
});
});

suite('formatAreaForDisplay', () => {
test('lowercase single word -> capitalized first char', () => {
assert.strictEqual(formatAreaForDisplay('vscode'), 'Vscode');
assert.strictEqual(formatAreaForDisplay('tower'), 'Tower');
assert.strictEqual(formatAreaForDisplay('porch'), 'Porch');
suite('uppercaseAreaName', () => {
test('lowercase single word -> full uppercase', () => {
assert.strictEqual(uppercaseAreaName('vscode'), 'VSCODE');
assert.strictEqual(uppercaseAreaName('tower'), 'TOWER');
assert.strictEqual(uppercaseAreaName('porch'), 'PORCH');
});

test('hyphenated -> separator replaced with space, each word capitalized', () => {
assert.strictEqual(formatAreaForDisplay('cross-cutting'), 'Cross Cutting');
test('hyphenated -> uppercase with hyphen preserved', () => {
assert.strictEqual(uppercaseAreaName('cross-cutting'), 'CROSS-CUTTING');
});

test('underscored -> separator replaced with space, each word capitalized', () => {
assert.strictEqual(formatAreaForDisplay('front_end'), 'Front End');
test('underscored -> uppercase with underscore preserved', () => {
assert.strictEqual(uppercaseAreaName('front_end'), 'FRONT_END');
});

test('mixed separators and >2 words', () => {
assert.strictEqual(formatAreaForDisplay('front-end_ui'), 'Front End Ui');
test('mixed separators preserved verbatim', () => {
assert.strictEqual(uppercaseAreaName('front-end_ui'), 'FRONT-END_UI');
});

test('Uncategorized sentinel is a no-op (single word, first char already upper)', () => {
assert.strictEqual(formatAreaForDisplay('Uncategorized'), 'Uncategorized');
test('Uncategorized sentinel uppercases to UNCATEGORIZED', () => {
assert.strictEqual(uppercaseAreaName('Uncategorized'), 'UNCATEGORIZED');
});

test('empty string -> empty string (defensive)', () => {
assert.strictEqual(formatAreaForDisplay(''), '');
assert.strictEqual(uppercaseAreaName(''), '');
});

test('collapses multiple consecutive separators', () => {
// Pathological but cheap to lock — no accidental double-spacing.
assert.strictEqual(formatAreaForDisplay('a--b'), 'A B');
assert.strictEqual(formatAreaForDisplay('a__b'), 'A B');
test('consecutive separators preserved verbatim (no collapsing)', () => {
assert.strictEqual(uppercaseAreaName('a--b'), 'A--B');
assert.strictEqual(uppercaseAreaName('a__b'), 'A__B');
});
});
8 changes: 4 additions & 4 deletions packages/vscode/src/views/area-group-tree-item.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as vscode from 'vscode';
import { formatAreaForDisplay } from '@cluesmith/codev-core/area-grouping';
import { uppercaseAreaName } from '@cluesmith/codev-core/area-grouping';

export type AreaGroupKind = 'backlog' | 'builder';

Expand All @@ -17,8 +17,8 @@ export type AreaGroupKind = 'backlog' | 'builder';
* `areaName`, `id`, and `contextValue` all use the raw wire value so
* expansion-state persistence and `areaName === ...` matchers in the
* per-view providers keep working. Only the human-visible label is
* passed through `formatAreaForDisplay` (title-case + separator-to-
* space, see #885).
* passed through `uppercaseAreaName`, matching VSCode's own
* container-label convention (EXPLORER, SOURCE CONTROL, etc.).
*/
export class AreaGroupTreeItem extends vscode.TreeItem {
constructor(
Expand All @@ -27,7 +27,7 @@ export class AreaGroupTreeItem extends vscode.TreeItem {
count: number,
collapsibleState: vscode.TreeItemCollapsibleState,
) {
super(`${formatAreaForDisplay(areaName)} (${count})`, collapsibleState);
super(`${uppercaseAreaName(areaName)} (${count})`, collapsibleState);
this.id = `${kind}-group:${areaName}`;
this.contextValue = `${kind}-group`;
}
Expand Down
Loading