|
| 1 | +# Metrics Processing Fix Implementation Plan |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. |
| 4 | +
|
| 5 | +**Goal:** Fix component usage metrics to produce reliable counts: stabilize the CLI CSV column order upstream, and make `process-ds-components.js` robust to column order regardless. |
| 6 | + |
| 7 | +**Architecture:** Two complementary fixes. (1) In `component-library`, fix the `yarn report:csv` CLI command to sort application columns alphabetically and remove the pre-calculated `total` column — this gives us a stable, predictable CSV schema. (2) In this repo, update `process-ds-components.js` to identify application columns by exclusion (`not date/component_name/uswds`) rather than by position — this is correct behavior and a good defensive fallback. The `uswds` column is excluded from summation since the USWDS v3 migration is complete; it is no longer meaningful to count separately. The `combineComponentVariants` logic (merging `va-alert` + `VaAlert` → `Alert`, etc.) is **intentional and correct** — React wrapper usages are real DS component usages. |
| 8 | + |
| 9 | +**Tech Stack:** Node.js (built-in modules only), GitHub CLI (`gh`) |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## Context: How the workflow is structured |
| 14 | + |
| 15 | +The `yarn forms` and `yarn report:csv` CLI commands live in `component-library/packages/design-system-dashboard-cli`. They used to run in a workflow inside component-library and check results into that repo. **What moved to this repo** is where those commands are invoked: `.github/workflows/metrics-dashboard.yml` now checks out `component-library` as a sibling directory and runs the CLI from there, so all metrics updates stay within this repo. The CLI source itself still lives in component-library. |
| 16 | + |
| 17 | +## Context: Why the Numbers Were Wrong |
| 18 | + |
| 19 | +The CLI appends new application columns to the CSV as new VA apps get tracked. Column order is **not stable** between runs — `uswds` appeared at index 2 in older files but drifted to a later position in the 2026-03-06 and 2026-03-13 runs. `process-ds-components.js` used `uswdsIndex + 1` as the start of application columns, so everything before `uswds` was silently dropped. A pre-calculated `total` column in the middle of the header was also being summed, double-counting every component. Result: `Link` showing 88 usages when the raw CSV shows `va-link` alone at 1163. |
| 20 | + |
| 21 | +**Why `combineComponentVariants` must stay:** React wrappers (`VaAlert`, `VaButton`, etc.) are real usages of DS components. Counting them separately would undercount component adoption. They are correctly merged with their `va-*` web component counterparts. |
| 22 | + |
| 23 | +**What we're NOT doing in this plan:** v1 component tracking, imposter metrics changes, or va-file-input specifics. Those are deferred pending team input. |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## Files |
| 28 | + |
| 29 | +| Repo | File | Action | |
| 30 | +|---|---|---| |
| 31 | +| `component-library` | `packages/design-system-dashboard-cli/src/commands/report.js` (or equivalent) | Modify — sort app columns, drop `total` column | |
| 32 | +| `vets-design-system-documentation` | `scripts/process-ds-components.js` | Modify — exclusion-based column detection, drop uswds from sum | |
| 33 | +| `vets-design-system-documentation` | `src/assets/data/metrics/component-usage.json` | Regenerated | |
| 34 | +| `vets-design-system-documentation` | `src/_data/metrics/component-usage.json` | Regenerated | |
| 35 | + |
| 36 | +--- |
| 37 | + |
| 38 | +## Task 1: Fix the CLI CSV output in `component-library` |
| 39 | + |
| 40 | +**Repo:** `department-of-veterans-affairs/component-library` |
| 41 | +**Goal:** Stable column order so the CSV schema doesn't change between runs. |
| 42 | + |
| 43 | +- [ ] **Step 1: Find the forms/report CLI source** |
| 44 | + |
| 45 | +```bash |
| 46 | +find component-library/packages/design-system-dashboard-cli/src -name "*.js" | xargs grep -l "csv\|uswds\|component_name" 2>/dev/null |
| 47 | +``` |
| 48 | + |
| 49 | +Also check: |
| 50 | +```bash |
| 51 | +cat component-library/packages/design-system-dashboard-cli/package.json | grep -A5 '"scripts"' |
| 52 | +``` |
| 53 | + |
| 54 | +- [ ] **Step 2: Locate where the CSV header is built** |
| 55 | + |
| 56 | +Look for where column headers are assembled. The fix is: |
| 57 | +1. Put metadata columns first in a fixed order: `date`, `component_name`, `uswds` |
| 58 | +2. Sort all application/repo columns alphabetically after that |
| 59 | +3. **Remove the pre-calculated `total` column** — it is redundant (our script calculates totals) and causes double-counting when included in sums |
| 60 | + |
| 61 | +- [ ] **Step 3: Apply the sort** |
| 62 | + |
| 63 | +The change should look roughly like: |
| 64 | +```js |
| 65 | +// BEFORE (unstable — insertion order depends on when apps were added) |
| 66 | +const headers = ['date', 'component_name', 'uswds', ...appColumns, 'total']; |
| 67 | + |
| 68 | +// AFTER (stable — app columns sorted, total removed) |
| 69 | +const METADATA = ['date', 'component_name', 'uswds']; |
| 70 | +const sortedAppColumns = [...appColumns].sort(); |
| 71 | +const headers = [...METADATA, ...sortedAppColumns]; |
| 72 | +``` |
| 73 | + |
| 74 | +- [ ] **Step 4: Run the CLI locally to verify output** |
| 75 | + |
| 76 | +```bash |
| 77 | +cd component-library/packages/design-system-dashboard-cli |
| 78 | +yarn report:csv --output output/test-stable-columns.csv |
| 79 | +head -1 output/test-stable-columns.csv | tr ',' '\n' | head -10 |
| 80 | +``` |
| 81 | + |
| 82 | +Expected: `date`, `component_name`, `uswds` as the first three columns; app columns alphabetically after; no `total` at the end. |
| 83 | + |
| 84 | +- [ ] **Step 5: Commit in component-library** |
| 85 | + |
| 86 | +```bash |
| 87 | +git add packages/design-system-dashboard-cli/src/... |
| 88 | +git commit -m "fix: sort CSV app columns alphabetically, remove pre-calculated total column |
| 89 | +
|
| 90 | +Column order was non-deterministic, causing downstream processing in |
| 91 | +vets-design-system-documentation to misidentify application columns. |
| 92 | +The 'total' column was also redundant and caused double-counting. |
| 93 | +
|
| 94 | +Related: vets-design-system-documentation#4903" |
| 95 | +``` |
| 96 | + |
| 97 | +--- |
| 98 | + |
| 99 | +## Task 2: Fix `process-ds-components.js` to use exclusion-based column detection |
| 100 | + |
| 101 | +**Repo:** `vets-design-system-documentation` |
| 102 | +**Files:** `scripts/process-ds-components.js` |
| 103 | + |
| 104 | +This fix is correct and necessary regardless of the CLI fix in Task 1 — it makes the script robust to any future column order drift. |
| 105 | + |
| 106 | +### The bug (lines ~78–132) |
| 107 | + |
| 108 | +```js |
| 109 | +// Breaks when uswds is not at index 2 |
| 110 | +const uswdsIndex = headers.findIndex(h => h.toLowerCase() === 'uswds'); |
| 111 | +const applicationStartIndex = Math.max(uswdsIndex + 1, 3); |
| 112 | +const applicationColumns = headers.slice(applicationStartIndex); |
| 113 | + |
| 114 | +// ...in row loop: |
| 115 | +for (let j = applicationStartIndex; j < Math.min(values.length, headers.length); j++) { |
| 116 | + applicationUsage[headers[j]] = parseInt(values[j]) || 0; |
| 117 | + totalUsage += parseInt(values[j]) || 0; |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +### The fix |
| 122 | + |
| 123 | +- [ ] **Step 1: Replace the column detection block** (around lines 78–90) |
| 124 | + |
| 125 | +Find: |
| 126 | +```js |
| 127 | + // Find column indices |
| 128 | + const dateIndex = headers.findIndex(h => h.toLowerCase() === 'date'); |
| 129 | + const componentIndex = headers.findIndex(h => h.toLowerCase() === 'component_name'); |
| 130 | + const uswdsIndex = headers.findIndex(h => h.toLowerCase() === 'uswds'); |
| 131 | + |
| 132 | + if (dateIndex === -1 || componentIndex === -1) { |
| 133 | + throw new Error('Required columns (date, component_name) not found in CSV'); |
| 134 | + } |
| 135 | + |
| 136 | + // Application columns start after the standard columns (date, component_name, uswds) |
| 137 | + const applicationStartIndex = Math.max(uswdsIndex + 1, 3); |
| 138 | + const applicationColumns = headers.slice(applicationStartIndex); |
| 139 | + |
| 140 | + console.log(`Processing ${lines.length - 1} component records...`); |
| 141 | + console.log(`Application columns: ${applicationColumns.length} (${applicationColumns.slice(0, 3).join(', ')}...)`); |
| 142 | +``` |
| 143 | + |
| 144 | +Replace with: |
| 145 | +```js |
| 146 | + // Find column indices |
| 147 | + const dateIndex = headers.findIndex(h => h.toLowerCase() === 'date'); |
| 148 | + const componentIndex = headers.findIndex(h => h.toLowerCase() === 'component_name'); |
| 149 | + |
| 150 | + if (dateIndex === -1 || componentIndex === -1) { |
| 151 | + throw new Error('Required columns (date, component_name) not found in CSV'); |
| 152 | + } |
| 153 | + |
| 154 | + // Application columns: everything except known metadata and pre-calculated totals. |
| 155 | + // Using exclusion (not position) so column order in the CSV doesn't matter. |
| 156 | + // 'uswds' is excluded — the USWDS v3 migration is complete and that count is no |
| 157 | + // longer meaningful. 'total' is excluded to prevent double-counting. |
| 158 | + const EXCLUDED_COLUMNS = new Set(['date', 'component_name', 'uswds', 'total']); |
| 159 | + const applicationColumns = headers.filter(h => !EXCLUDED_COLUMNS.has(h.toLowerCase())); |
| 160 | + const applicationColumnIndices = applicationColumns.map(col => headers.indexOf(col)); |
| 161 | + |
| 162 | + console.log(`Processing ${lines.length - 1} component records...`); |
| 163 | + console.log(`Application columns: ${applicationColumns.length} (${applicationColumns.slice(0, 3).join(', ')}...)`); |
| 164 | +``` |
| 165 | + |
| 166 | +- [ ] **Step 2: Replace the row validation and summation loop** (around lines 95–132) |
| 167 | + |
| 168 | +Find: |
| 169 | +```js |
| 170 | + // Improved column count validation - handle both too few and too many columns |
| 171 | + if (values.length < Math.min(headers.length, applicationStartIndex + 1)) { |
| 172 | + console.warn(`Row ${i + 1} has ${values.length} values but expected at least ${applicationStartIndex + 1} (${headers.length} total columns), skipping`); |
| 173 | + continue; |
| 174 | + } |
| 175 | + |
| 176 | + // Handle rows with more columns than headers (pad headers or truncate values) |
| 177 | + if (values.length > headers.length) { |
| 178 | + console.warn(`Row ${i + 1} has ${values.length} values but only ${headers.length} headers, truncating extra values`); |
| 179 | + values.length = headers.length; // Truncate extra values |
| 180 | + } |
| 181 | + |
| 182 | + const date = values[dateIndex]; |
| 183 | + const componentName = values[componentIndex]; |
| 184 | + |
| 185 | + // Set report date from first row |
| 186 | + if (!reportDate) { |
| 187 | + reportDate = date; |
| 188 | + } |
| 189 | + |
| 190 | + if (!componentName || componentName.trim() === '') { |
| 191 | + continue; |
| 192 | + } |
| 193 | + |
| 194 | + // Calculate total usage across all applications (ignoring USWDS column) |
| 195 | + let totalUsage = 0; |
| 196 | + const applicationUsage = {}; |
| 197 | + |
| 198 | + for (let j = applicationStartIndex; j < Math.min(values.length, headers.length); j++) { |
| 199 | + const appName = headers[j]; |
| 200 | + const usageCount = parseInt(values[j]) || 0; |
| 201 | + applicationUsage[appName] = usageCount; |
| 202 | + totalUsage += usageCount; |
| 203 | + } |
| 204 | +``` |
| 205 | + |
| 206 | +Replace with: |
| 207 | +```js |
| 208 | + // Handle rows with more columns than headers |
| 209 | + if (values.length > headers.length) { |
| 210 | + console.warn(`Row ${i + 1} has ${values.length} values but only ${headers.length} headers, truncating extra values`); |
| 211 | + values.length = headers.length; |
| 212 | + } |
| 213 | + |
| 214 | + const date = values[dateIndex]; |
| 215 | + const componentName = values[componentIndex]; |
| 216 | + |
| 217 | + // Set report date from first row |
| 218 | + if (!reportDate) { |
| 219 | + reportDate = date; |
| 220 | + } |
| 221 | + |
| 222 | + if (!componentName || componentName.trim() === '') { |
| 223 | + continue; |
| 224 | + } |
| 225 | + |
| 226 | + // Sum only application columns. Iterating by pre-built index list means |
| 227 | + // column order in the CSV doesn't affect which columns are counted. |
| 228 | + let totalUsage = 0; |
| 229 | + const applicationUsage = {}; |
| 230 | + |
| 231 | + for (let k = 0; k < applicationColumnIndices.length; k++) { |
| 232 | + const colIndex = applicationColumnIndices[k]; |
| 233 | + const appName = applicationColumns[k]; |
| 234 | + const usageCount = parseInt(values[colIndex]) || 0; |
| 235 | + applicationUsage[appName] = usageCount; |
| 236 | + totalUsage += usageCount; |
| 237 | + } |
| 238 | +``` |
| 239 | + |
| 240 | +- [ ] **Step 3: Verify no leftover references to the old variable names** |
| 241 | + |
| 242 | +```bash |
| 243 | +grep -n "applicationStartIndex\|uswdsIndex" scripts/process-ds-components.js |
| 244 | +``` |
| 245 | + |
| 246 | +Expected: no output. |
| 247 | + |
| 248 | +- [ ] **Step 4: Commit** |
| 249 | + |
| 250 | +```bash |
| 251 | +git add scripts/process-ds-components.js |
| 252 | +git commit -m "fix(metrics): detect ds-components app columns by exclusion, not position |
| 253 | +
|
| 254 | +The CLI does not guarantee a stable column order — 'uswds' drifted past |
| 255 | +column 2 in recent runs, causing applicationStartIndex to skip many real |
| 256 | +app columns and severely undercount component usage. |
| 257 | +
|
| 258 | +Also explicitly exclude 'uswds' (USWDS v3 migration is complete, no |
| 259 | +longer meaningful to count separately) and 'total' (pre-calculated sum |
| 260 | +that caused double-counting). |
| 261 | +
|
| 262 | +The combineComponentVariants logic is unchanged — React wrappers are |
| 263 | +real DS component usages and should be merged with their va-* counterparts. |
| 264 | +
|
| 265 | +Closes Copilot comments on #5902" |
| 266 | +``` |
| 267 | + |
| 268 | +--- |
| 269 | + |
| 270 | +## Task 3: Regenerate component-usage.json |
| 271 | + |
| 272 | +- [ ] **Step 1: Run the fixed script** |
| 273 | + |
| 274 | +```bash |
| 275 | +node scripts/process-ds-components.js |
| 276 | +``` |
| 277 | + |
| 278 | +- [ ] **Step 2: Sanity-check the output** |
| 279 | + |
| 280 | +```bash |
| 281 | +node -e " |
| 282 | +const d = require('./src/assets/data/metrics/component-usage.json'); |
| 283 | +console.log('total_usages:', d.summary_stats.total_usages); |
| 284 | +console.log('applications_tracked:', d.summary_stats.applications_tracked); |
| 285 | +console.log('Top 5:'); |
| 286 | +d.top_components_overall.slice(0,5).forEach((c,i) => console.log(i+1, c.name, c.usage_count)); |
| 287 | +" |
| 288 | +``` |
| 289 | + |
| 290 | +Expected: `total_usages` in the tens of thousands (raw CSV shows va-telephone at ~810, va-link at ~1163, many more). `applications_tracked` should be ~140. If totals are still small (~459), stop and debug before committing. |
| 291 | + |
| 292 | +- [ ] **Step 3: Commit the regenerated data** |
| 293 | + |
| 294 | +```bash |
| 295 | +git add src/assets/data/metrics/component-usage.json src/_data/metrics/component-usage.json |
| 296 | +git commit -m "data: regenerate component-usage.json with correct column detection" |
| 297 | +``` |
| 298 | + |
| 299 | +--- |
| 300 | + |
| 301 | +## Task 4: Resolve PR #5902 and issue #4903 |
| 302 | + |
| 303 | +- [ ] **Step 1: Comment on PR #5902 explaining the resolution** |
| 304 | + |
| 305 | +```bash |
| 306 | +gh pr comment 5902 \ |
| 307 | + --repo department-of-veterans-affairs/vets-design-system-documentation \ |
| 308 | + --body "Addressing Copilot's review comments here. |
| 309 | +
|
| 310 | +The undercounted component usage (Link: 87 vs CSV: 1221) was caused by a column-order bug in \`process-ds-components.js\` — it used \`uswds\` column position to find where application columns start. When the CLI moved \`uswds\` past column 2 in recent runs, all earlier app columns were silently dropped. |
| 311 | +
|
| 312 | +Fixed in [link to your PR] by switching to exclusion-based column detection: all columns except \`date\`, \`component_name\`, \`uswds\`, and \`total\` are treated as application columns regardless of position. |
| 313 | +
|
| 314 | +The \`total\` column is also now excluded to prevent double-counting. |
| 315 | +
|
| 316 | +The \`combineComponentVariants\` behavior (merging \`va-link\` + \`VaLink\` → \`Link\`) is intentional — React wrapper usages are real DS component usages. |
| 317 | +
|
| 318 | +The 10-10d form-apps comment is an upstream CLI formatting issue tracked in #4903." |
| 319 | +``` |
| 320 | + |
| 321 | +- [ ] **Step 2: Close PR #5902** (it contains stale/incorrect data; the next weekly run will generate a fresh PR with the fixed script in place) |
| 322 | + |
| 323 | +```bash |
| 324 | +gh pr close 5902 \ |
| 325 | + --repo department-of-veterans-affairs/vets-design-system-documentation |
| 326 | +``` |
| 327 | + |
| 328 | +- [ ] **Step 3: Comment on issue #4903** |
| 329 | + |
| 330 | +```bash |
| 331 | +gh issue comment 4903 \ |
| 332 | + --repo department-of-veterans-affairs/vets-design-system-documentation \ |
| 333 | + --body "Partial progress: The column-order bug in \`process-ds-components.js\` that caused severely undercounted component usage is fixed. The form-apps CSV now has a header row in current output. |
| 334 | +
|
| 335 | +Remaining: form numbers are still inconsistent in the CLI output (e.g. \`10D\` instead of \`10-10D\`). That fix belongs in the \`design-system-dashboard-cli\` in component-library — filing there separately." |
| 336 | +``` |
| 337 | + |
| 338 | +--- |
| 339 | + |
| 340 | +## Summary |
| 341 | + |
| 342 | +| Problem | Fix location | Task | |
| 343 | +|---|---|---| |
| 344 | +| Unstable CSV column order | component-library CLI | Task 1 | |
| 345 | +| Exclusion-based column detection | `process-ds-components.js` | Task 2 | |
| 346 | +| Wrong `component-usage.json` counts | Regenerate after script fix | Task 3 | |
| 347 | +| PR #5902 Copilot comments | Explain + close stale PR | Task 4 | |
| 348 | +| Issue #4903 status | Comment with progress | Task 4 | |
| 349 | +| v1 component / imposter tracking | **Deferred** — pending team input on va-file-input specifics | — | |
0 commit comments