Skip to content

refactor(#23): extract Azure DevOps data-plane calls into azdevops_db.ps1#27

Merged
jdschleicher merged 5 commits intomainfrom
claude/refactor-azure-database-calls-HvHnx
May 8, 2026
Merged

refactor(#23): extract Azure DevOps data-plane calls into azdevops_db.ps1#27
jdschleicher merged 5 commits intomainfrom
claude/refactor-azure-database-calls-HvHnx

Conversation

@jdschleicher
Copy link
Copy Markdown
Owner

@jdschleicher jdschleicher commented May 8, 2026

Section Status
Summary
Issue Closes #23
Changes ✅ 8 files
AC Gate ✅ no active call sites
Test Plan 6 manual checks
Skill Reports
🐚 Bash Engineer Review ✅ APPROVE — View
💠 PowerShell Engineer Review ✅ APPROVE — View
🛡️ Security Audit ✅ APPROVE (defense-in-depth applied) — View
🧼 Clean-Code Engineer Review ✅ HIGH + MEDIUM addressed in 70966ceView
✅ Criteria Check ✅ PASS (11 verified, 2 manual) — View
🗺️ AzDO Diagrams Check ✅ CURRENT (3 of 4 wrappers added; backlog noted) — View
📚 Docs Check ✅ CURRENT (no README drift)

Summary

Pulls every az boards ... invocation in azdevops_workitems.ps1 and pow_az_cli.ps1 behind a thin wrapper layer in the new powcuts_by_cli/azdevops_db.ps1. Higher-level orchestrators (sync, new-story, classification fallback, type-show) stop knowing about az argument shapes, JSON deserialization, and stderr handling — future caching, retry, or alt-transport changes are now a one-file edit.

Also lands a supporting /azdevops-diagrams-check skill that keeps docs/azure-devops-diagrams.md in lockstep with the AzDO source going forward.

Issue

Closes #23

Changes

Refactor (issue #23)

  • New powcuts_by_cli/azdevops_db.ps1 with 6 wrappers:
    • Invoke-AzDevOpsAzJson, Invoke-AzDevOpsBoardsQuery (relocated; canonical { Json, Error, ExitCode } envelope)
    • Get-AzDevOpsClassificationList -Kind <Iteration|Area> (merged Iteration/Area twin pair per clean-code review)
    • New-AzDevOpsWorkItem (with -Fields regex validation per security review; ordered-hashtable optional-flag loop)
    • Add-AzDevOpsWorkItemRelation
    • Get-AzDevOpsWorkItemTypeDefinition
  • Wired azdevops_db.ps1 into powcuts_home.ps1 (dot-sourced before azdevops_workitems.ps1)
  • Routed all 8 active call sites through named wrappers:
    • Invoke-AzDevOpsSmokeQueryInvoke-AzDevOpsBoardsQuery
    • Sync iteration/area datasets → Get-AzDevOpsClassificationList -Kind
    • Invoke-AzDevOpsClassificationLiveGet-AzDevOpsClassificationList -Kind
    • Invoke-AzDevOpsWorkItemCreateNew-AzDevOpsWorkItem
    • Invoke-AzDevOpsParentLinkAdd-AzDevOpsWorkItemRelation
    • Invoke-AzDevOpsWorkItemTypeShowGet-AzDevOpsWorkItemTypeDefinition
    • pow_az_cli.ps1's az-create-userstoryNew-AzDevOpsWorkItem -Open
  • Session/admin calls (az login, az account show, az extension *, az devops configure, az version) remain in azdevops_workitems.ps1 per the issue's out-of-scope carve-out.
  • No public-facing function signatures changed.

Tooling (separate commit)

  • New .claude/commands/azdevops-diagrams-check.md skill — auto-detects AzDO source changes, diffs function inventory + az subcommand coverage against docs/azure-devops-diagrams.md, proposes targeted mermaid edits per gap.
  • Wired as Phase 5b in /pr-flow between Criteria Check and PR Body.
  • Updated CLAUDE.md slash-commands table.

Diagrams (commit 9a74757)

  • Updated docs/azure-devops-diagrams.md Diagrams 4 / 7 / 9 to reflect the new wrapper layer.

AC Gate (issue #23)

grep -nE '\baz boards' powcuts_by_cli/azdevops_workitems.ps1 powcuts_by_cli/pow_az_cli.ps1 returns no active call sites — only header comments, doc-strings, and Write-Host user hints.

Test Plan

  • Open a fresh PowerShell terminal, dot-source powcuts_home.ps1
  • Get-Command Invoke-AzDevOpsAzJson, Invoke-AzDevOpsBoardsQuery, Get-AzDevOpsClassificationList, New-AzDevOpsWorkItem, Add-AzDevOpsWorkItemRelation, Get-AzDevOpsWorkItemTypeDefinition — all 6 resolve
  • Connect-AzDevOps — 6-step orchestrator passes (smoke query now via wrapper)
  • Sync-AzDevOpsCache — assigned/mentions/hierarchy via Invoke-AzDevOpsBoardsQuery, iterations/areas via Get-AzDevOpsClassificationList -Kind
  • Get-AzDevOpsAssigned and Open-AzDevOpsAssigned — cache consumers unaffected
  • New-AzDevOpsUserStory — interactive create via New-AzDevOpsWorkItem, parent-link via Add-AzDevOpsWorkItemRelation
  • Output identical to pre-refactor for each command above

https://claude.ai/code/session_01P7znUKccAohZYEwYRBNVfo


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

🐚 Senior Bash Engineer

Summary

No bash files (bashcuts_by_cli/* or .bcut_home) were modified in this PR vs origin/main. The change is scoped to PowerShell sources, the PowerShell entry point, and markdown skill/instruction files. This is consistent with issue #23's Shell Parity Note declaring .az_bashcuts explicitly out of scope. No bash review obligations are triggered.

Changed Files Inspected (vs origin/main)

File Type Bash impact
.claude/commands/azdevops-diagrams-check.md new — markdown none (skill instructions)
.claude/commands/pr-flow.md modified — markdown none
CLAUDE.md modified — markdown none
powcuts_by_cli/azdevops_db.ps1 new — PowerShell none
powcuts_by_cli/azdevops_workitems.ps1 modified — PowerShell none
powcuts_by_cli/pow_az_cli.ps1 modified — PowerShell none
powcuts_home.ps1 modified — PowerShell none

Markdown Bash-Snippet Spot-Check

The new skill .claude/commands/azdevops-diagrams-check.md contains several bash-fenced snippets used as Claude-executed pipeline steps (not user-facing shortcuts, not sourced into .bashrc). I scanned them for portability issues since they run during the skill:

  • File-loop variables ("$f") are correctly quoted inside the case/grep/[ -f ] constructs. ✅
  • /tmp/azdo-* paths are bare-word, no spaces, safe across macOS and Linux. ✅
  • grep -E, awk, comm, sed -E, sort -u are all POSIX-portable; no GNU-only flags spotted. ✅
  • case "$f" in bashcuts_by_cli/*|powcuts_by_cli/*) — quoted scrutinee, glob patterns unquoted as required. ✅
  • No mapfile/readarray/associative-array bash 4 features that would break on macOS stock /bin/bash 3.2. ✅
  • The { git diff ...; git diff ...; } | sort -u > /tmp/... group commands are well-formed. ✅

Nothing in pr-flow.md or CLAUDE.md introduces new shell snippets that warrant blocking concerns.

Sourcing Wire-Up

N/A — no new files under bashcuts_by_cli/, so .bcut_home does not need updating.

Naming Convention

N/A — no bash aliases or functions added.

Verdict

APPROVE — no bash files in this diff. The PR's scope is correctly PowerShell-only per issue #23's Shell Parity Note, and the embedded bash snippets in the new diagram-check skill are portable and properly quoted.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

🛡️ Senior Security Engineer

Scope: PR #27 vs origin/mainpowcuts_by_cli/azdevops_db.ps1 (new), azdevops_workitems.ps1, pow_az_cli.ps1, powcuts_home.ps1 wire-up, plus three markdown files (skill / CLAUDE.md / pr-flow). Markdown files reviewed for secrets / command-substitution payloads — none found.

Verdict: ✅ Approve with one minor hardening recommendation. No blocking findings.


1. Shell-injection via & az @ArgListNot exploitable

Invoke-AzDevOpsAzJson uses PowerShell's call operator with array splatting:

$json = & az @ArgList --output json 2>$stderrFile

& with @ArgList is the equivalent of execve(argv) — it passes each array element as a separate argv to the child process without invoking a shell. There is no interpretation of ;, |, &, $(...), backticks, or glob patterns. An attacker-controlled $ArgList element such as "; rm -rf ~" is delivered to az as a literal single argv string and gets rejected by az's own argparse. This is the safe pattern; the previous direct-call sites (az boards query --wiql $wiql ...) had the same property.

2. Argument-injection via $Fields in New-AzDevOpsWorkItem⚠️ Low severity, worth a guard

The wrapper builds:

$argList += '--fields'
$argList += $Fields    # variadic, consumed positionally by az

az boards work-item create --fields consumes all subsequent positional tokens until the next --flag. If a $Fields element starts with -- (e.g. "--debug", "--query .id", "--open"), az's argparse treats it as a new flag rather than a field value — classic argument injection.

  • $Title, $Description, $AssignedTo, $Project, $Area, $Iteration are safe because they each follow a single --flag token, and argparse consumes exactly one positional value regardless of leading dashes.
  • Only the variadic $Fields slot has this risk.

Threat model in this repo: the attacker would need to be the user typing into their own interactive Read-Host prompt (pow_az_cli.ps1) or a caller in Invoke-AzDevOpsWorkItemCreate whose own inputs come from the user's Read-AzDevOpsCachedWorkItemTitle/etc. flow. Self-inflicted-wound territory, not a privilege boundary. Not blocking.

Suggested defense-in-depth — reject malformed field strings inside New-AzDevOpsWorkItem before splatting:

foreach ($f in $Fields) {
    if ($f -notmatch '^[A-Za-z][A-Za-z0-9_.]*=') {
        throw "Invalid field assignment '$f' (expected 'Field.Name=value')."
    }
}

This anchors the leading token to a refname-shaped identifier and ensures the first character isn't -, which kills the entire injection class.

3. Tempfile race for stderr capture — Race-safe

$stderrFile = [System.IO.Path]::GetTempFileName()
try {
    $json = & az @ArgList --output json 2>$stderrFile
    ...
} finally {
    Remove-Item -LiteralPath $stderrFile -ErrorAction SilentlyContinue
}
  • GetTempFileName() atomically creates a 0-byte file with a unique name (calls GetTempFileNameW on Windows / mkstemp-equivalent on .NET Core Unix) owned by the current user with default-restrictive permissions (0600 on Unix, owner-ACL on Windows).
  • The 2>$stderrFile redirect then opens-and-truncates the already-existing file we own, so there is no symlink-follow window between create and write.
  • Cleanup in finally with -ErrorAction SilentlyContinue is correct — survives both normal completion and exceptions thrown mid-execution. The path is held in a local; no TOCTOU between read and delete.
  • The if ($null -eq $stderr) { $stderr = '' } guard correctly handles Get-Content -Raw returning $null for an empty file.

No findings.

4. Secret / token leakage — Clean

  • Neither $json nor $stderr is echoed to the console by the wrapper itself; both are returned in a structured envelope.
  • Callers print $result.Error only on non-zero exit (Write-Host "az boards work-item create failed: $($result.Error)" -ForegroundColor Red). az's normal stderr output for argparse / 4xx errors does not include the bearer token.
  • Caveat (not introduced by this PR, defense-in-depth note): if a user has AZURE_CORE_VERBOSITY=debug or --debug set in the environment, az's debug stream can contain Authorization header values. Worth a one-line comment in Invoke-AzDevOpsAzJson warning future contributors not to surface $result.Error to logs/PR comments unconditionally.

5. Wire-up ordering in powcuts_home.ps1Correct

azdevops_db.ps1 is dot-sourced before azdevops_workitems.ps1. PowerShell function lookup is dynamic at call-time so the order doesn't strictly matter for correctness, but the chosen order matches the dependency direction (workitems → db) and avoids surprises if anyone later moves a top-level statement that calls a wrapper.

6. Markdown / skill files — No secrets, no payload

.claude/commands/azdevops-diagrams-check.md, pr-flow.md, and the CLAUDE.md additions contain no embedded credentials, no fetch-and-execute patterns, no curl | sh-style invocations.


Summary

Concern Status
& az @ArgList shell-injection ✅ Not exploitable (no shell involved)
$Fields argument-injection ⚠️ Low-sev — recommend regex guard inside New-AzDevOpsWorkItem
Stderr tempfile race ✅ Safe (atomic create, owner-only, cleaned in finally)
Secret leakage in stdout/stderr ✅ Clean (note: warn against echoing Error if --debug ever enabled)
Wire-up ordering ✅ Correct
Markdown payloads ✅ Clean

Recommendation: Merge after (optionally) adding the $Fields validator. None of the findings rise to a merge-blocker.

https://claude.ai/code/session_01P7znUKccAohZYEwYRBNVfo


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

💠 Senior PowerShell Engineer

Summary

Reviewed powcuts_by_cli/azdevops_db.ps1 (new), powcuts_by_cli/azdevops_workitems.ps1 (modified), powcuts_by_cli/pow_az_cli.ps1 (modified), and powcuts_home.ps1 (dot-source wire-up). The data-plane extraction is clean: every wrapper returns the canonical { Json, Error, ExitCode } envelope from Invoke-AzDevOpsAzJson, all seven new public functions use approved verbs (Invoke-, Get-, New-, Add-), and every caller in azdevops_workitems.ps1 consistently uses $result.ExitCode -ne 0 plus try { ConvertFrom-Json } catch { … }. Style follows CLAUDE.md (two blank lines between top-level functions, multi-line if/else, no return <function call> directly, no inline shorthand introduced). No blocking issues.

Findings

Severity Location Issue
LOW powcuts_home.ps1:52 (load order) pow_az_cli.ps1 is dot-sourced at line 10, but azdevops_db.ps1 is dot-sourced later at line 52. az-create-userstory now calls New-AzDevOpsWorkItem, which doesn't exist at the moment pow_az_cli.ps1 is dot-sourced. PowerShell resolves function references at call time, not at parse time, so this works in practice — by the time the user invokes az-create-userstory, both files are loaded. Still, ordering azdevops_db.ps1 before any caller (pow_az_cli.ps1, azdevops_workitems.ps1) would make the dependency explicit and survive a future refactor that moves logic to top-level statements. Suggest moving the azdevops_db.ps1 block above the pow_az_cli.ps1 block.
LOW powcuts_by_cli/pow_az_cli.ps1:48 The function ends with return $result.Json. $result.Json is the captured stdout from az, which for --output json is typically a string[] (one element per line). Returning it directly makes downstream piping awkward (caller has to -join "" | ConvertFrom-Json). Two reasonable shapes: (a) return ($result.Json | ConvertFrom-Json) so the caller gets a hydrated object, or (b) leave it as-is and update the comment to clarify. Not blocking — az-create-userstory is a one-shot interactive command and the original implementation produced no return value at all, so this is already an improvement.
LOW powcuts_by_cli/pow_az_cli.ps1:1 function az-create-userstory() violates Verb-Noun PascalCase. Per the AC's no-rename rule this is informational only — flagged so future readers know it's an intentional carve-out, not an oversight. The rest of the function body was cleaned up nicely (consistent spacing, no trailing whitespace, the deleted duplicate az boards work-item create block).
LOW powcuts_by_cli/azdevops_db.ps1:39-41 The if ($null -eq $stderr) { $stderr = '' } guard immediately follows a try/finally block whose else branch already assigns ''. The only path to $null is Get-Content -Raw on a zero-byte file (which can return $null), so the guard is correct and worth keeping — but a one-line comment explaining why it's there (Get-Content -Raw on empty file → $null) would help future readers not delete it as redundant. Pre-existing logic moved verbatim from azdevops_workitems.ps1, so not a regression.
INFO powcuts_by_cli/azdevops_db.ps1 New-AzDevOpsWorkItem's parameter binding is correct: required Type/Title, all other strings optional with truthy guards before appending --<flag>, [string[]] $Fields correctly splat-appended, [switch] $Open appends --open only when present. Field ordering ('--fields' + $Fields) is right — az accepts space-separated key=value tokens after the single --fields flag.
INFO powcuts_by_cli/azdevops_db.ps1 Invoke-AzDevOpsAzJson always appends --output json, which means it cannot be used for any az boards command that doesn't support --output json (e.g. some az devops configure subcommands). The header comment correctly calls this out and excludes session/admin calls — good API boundary.
INFO powcuts_by_cli/azdevops_workitems.ps1 (multiple call sites) Every replacement preserves caller semantics: Invoke-AzDevOpsSmokeQuery (line 79-89), Get-AzDevOpsSyncDatasets Fetch scriptblocks (lines 466, 475), Invoke-AzDevOpsClassificationLive Iteration/Area dispatch (lines 1353-1357), Invoke-AzDevOpsWorkItemCreate (1649-1663), Invoke-AzDevOpsParentLink (1714), Invoke-AzDevOpsWorkItemTypeShow (2187). Diff is mechanical and surgical — no behavior changes beyond the indirection.

Dot-Sourcing Wire-Up

WIRED — azdevops_db.ps1 is dot-sourced from powcuts_home.ps1:52-57 using the established pattern (Get-Content guard, . <path>, fallback Write-Host).

Approved Verbs

PASS — all seven new functions use approved verbs:

  • Invoke-AzDevOpsAzJsonInvoke
  • Invoke-AzDevOpsBoardsQueryInvoke
  • Get-AzDevOpsIterationListGet
  • Get-AzDevOpsAreaListGet
  • New-AzDevOpsWorkItemNew
  • Add-AzDevOpsWorkItemRelationAdd
  • Get-AzDevOpsWorkItemTypeDefinitionGet

Parse-Check

SKIPPED — pwsh is not on PATH in this review environment, so Parser.ParseFile syntax validation could not run. Visual review found no obvious syntax errors (balanced braces, consistent param blocks, no stray backticks). The user should run pwsh -NoProfile -Command "[System.Management.Automation.Language.Parser]::ParseFile('powcuts_by_cli/azdevops_db.ps1', [ref]\$null, [ref]\$null) \| Out-Null" (and the same for azdevops_workitems.ps1, pow_az_cli.ps1, powcuts_home.ps1) before merging.

Verdict

APPROVE — no critical or high issues. Four LOW findings (one load-order suggestion, one return-shape observation, one informational verb-naming note already covered by AC, one comment-clarity nit) and several INFO confirmations. The refactor cleanly achieves the stated goal of decoupling orchestrators from az argument shapes, JSON parsing, and stderr handling.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

🧼 Senior Clean-Code Engineer

Summary

The wrapper layer in azdevops_db.ps1 is the right move — extracting Invoke-AzDevOpsAzJson out of azdevops_workitems.ps1 and routing every az boards call through a single envelope-producing entry point is exactly the abstraction the codebase needed. Most wrappers hit the 5–15 line target, breathing room is correct (two blank lines between top-level functions), every wrapper follows the never-return-call-directly rule, and the Invoke-AzDevOpsClassificationLive $result = if ... else ... complies with the multi-line branch rule. Two structural issues block APPROVE: the Get-AzDevOpsIterationList / Get-AzDevOpsAreaList parallel pair plus the new if ($Kind -eq 'Iteration') branch in Invoke-AzDevOpsClassificationLive is a textbook CLAUDE.md "extract-repeated-branches" miss, and New-AzDevOpsWorkItem repeats the same 3-line if ($Param) { $argList += @('--flag', $Param) } pattern five times.

Findings

Severity Location Issue
HIGH azdevops_db.ps1:61-76 + azdevops_workitems.ps1:1353-1357 Parallel-pair extraction miss. Get-AzDevOpsIterationList and Get-AzDevOpsAreaList are intentional twins differing only by one literal ('iteration' vs 'area'). Invoke-AzDevOpsClassificationLive already accepts -Kind and now does the dispatch (if ($Kind -eq 'Iteration') { Get-AzDevOpsIterationList } else { Get-AzDevOpsAreaList }). CLAUDE.md is explicit: "Apply this proactively when implementing parallel Register-/Unregister- style pairs." Collapse to one wrapper: Get-AzDevOpsClassificationList -Kind <Iteration|Area> -Depth 5. The Kind→subcommand mapping ('Iteration' → 'iteration', 'Area' → 'area') becomes a one-line $sub = $Kind.ToLower() inside the merged wrapper, and Invoke-AzDevOpsClassificationLive loses its conditional entirely ($result = Get-AzDevOpsClassificationList -Kind $Kind). The merged wrapper aligns with the rest of the Classification-family functions in azdevops_workitems.ps1 (Invoke-AzDevOpsClassificationLive, Read-AzDevOpsClassificationPick, Resolve-AzDevOpsClassificationPath) which all already use -Kind.
MEDIUM azdevops_db.ps1:103-130 (New-AzDevOpsWorkItem) The same 3-line if ($Param) { $argList += @('--flag', $Param) } block is repeated 5× back-to-back for Description / AssignedTo / Project / Area / Iteration. Tightens to a single ordered-hashtable loop, and the function drops from ~56 to ~30 lines. Suggested shape:
$optional = [ordered]@{ '--description' = $Description; '--assigned-to' = $AssignedTo; '--project' = $Project; '--area' = $Area; '--iteration' = $Iteration }
foreach ($kv in $optional.GetEnumerator()) { if ($kv.Value) { $argList += @($kv.Key, $kv.Value) } }
The --fields / --open blocks stay separate (they're not flag value pairs).
MEDIUM azdevops_workitems.ps1:1656 vs pow_az_cli.ps1:33 Casing mismatch on the same magic string: 'User Story' (PascalCase, used everywhere else in azdevops_workitems.ps1 — see lines 432, 1222, 1245, 1297, 1882, 2046) vs 'user story' (lowercase, sole instance) in pow_az_cli.ps1. az is tolerant in practice but two callsites pointing at the same domain concept must agree on spelling. Hoist a $script:AzDevOpsWorkItemTypeUserStory = 'User Story' into azdevops_db.ps1 and reference from both callers, OR — better — add a thin New-AzDevOpsUserStory wrapper in azdevops_db.ps1 that hardcodes -Type 'User Story' so neither caller types the literal.
MEDIUM azdevops_workitems.ps1:1649-1653 + pow_az_cli.ps1:26-30 The same three field literals (Microsoft.VSTS.Scheduling.StoryPoints=..., Microsoft.VSTS.Common.Priority=..., Microsoft.VSTS.Common.AcceptanceCriteria=...) are built identically in both Invoke-AzDevOpsWorkItemCreate and az-create-userstory. Six magic strings duplicated across two callers. Extract a New-AzDevOpsUserStoryFieldArray -StoryPoints -Priority -AcceptanceCriteria helper in azdevops_db.ps1 returning the string[]. Also fixes drift risk if VSTS field names ever change.
LOW azdevops_workitems.ps1:1714 'parent' magic string is a single-callsite literal (the wrapper's docs name it). Not worth a named constant on its own, but combined with the fact that the only relation type ever used is 'parent', consider replacing Add-AzDevOpsWorkItemRelation -RelationType 'parent' with a thin Add-AzDevOpsParentLink wrapper in azdevops_db.ps1. The current Add-AzDevOpsWorkItemRelation is the right shape for future flexibility, but a parent-link convenience wrapper matches how the orchestrator thinks about the call.
LOW pow_az_cli.ps1:1 az-create-userstory is a verb-noun naming violation (lowercase, dash inside both halves). Pre-existing — not introduced by this diff — flagged for tracking, fix in a separate PR.

Duplication Map

Pattern Callsites Proposed helper
az boards <iteration|area> project list --depth N parallel pair + Kind-dispatcher azdevops_db.ps1 Get-AzDevOpsIterationList + Get-AzDevOpsAreaList; azdevops_workitems.ps1:1353-1357 Invoke-AzDevOpsClassificationLive Get-AzDevOpsClassificationList -Kind <Iteration|Area> -Depth N (single wrapper, Kind→subcommand mapping internal)
if ($Param) { $argList += @('--flag', $Param) } (5× back-to-back) azdevops_db.ps1:103-121 Ordered-hashtable loop inside New-AzDevOpsWorkItem; no extracted helper needed
User-story field array (3 VSTS field literals) azdevops_workitems.ps1:1649-1653, pow_az_cli.ps1:26-30 New-AzDevOpsUserStoryFieldArray -StoryPoints -Priority -AcceptanceCriteria returning string[]
'User Story' / 'user story' work-item type literal azdevops_workitems.ps1:1656, pow_az_cli.ps1:33 $script:AzDevOpsWorkItemTypeUserStory = 'User Story' in azdevops_db.ps1, OR New-AzDevOpsUserStory convenience wrapper that hardcodes -Type

Function Sizes

Function Lines (body) Verdict
Invoke-AzDevOpsAzJson 29 OK — non-trivial stderr-tempfile dance, single responsibility
Invoke-AzDevOpsBoardsQuery 8 OK — tight, fits the 5-15 target
Get-AzDevOpsIterationList 7 OK in isolation; folds away under the HIGH finding
Get-AzDevOpsAreaList 7 OK in isolation; folds away under the HIGH finding
New-AzDevOpsWorkItem 56 TOO LONG for a wrapper — see MEDIUM finding; trims to ~30 with the hashtable loop
Add-AzDevOpsWorkItemRelation 17 OK — slightly above the 15-line target but the arg array is well-formatted
Get-AzDevOpsWorkItemTypeDefinition 9 OK
Invoke-AzDevOpsClassificationLive (post-refactor) 22 OK in shape; the if/else dispatch goes away under the HIGH finding, dropping it to ~17
az-create-userstory (rewrite) 49 OK — Read-Host orchestration genuinely needs the lines; minor tightening from MEDIUM finding #4

Verdict

REQUEST CHANGES — 1 HIGH (parallel-pair extraction explicitly named in CLAUDE.md), 3 MEDIUM (in-function repetition + magic-string drift across two callers + duplicated field-array build). The HIGH finding is mechanical to fix and shrinks the diff; the MEDIUM findings prevent the casing drift from compounding. Once Get-AzDevOpsClassificationList -Kind lands and the user-story type/fields stop being hand-built at two callsites, this wrapper layer is exactly the DRY win it set out to be.


Generated by Claude Code

jdschleicher pushed a commit that referenced this pull request May 8, 2026
Clean-code HIGH (parallel-pair extraction):
- Collapse Get-AzDevOpsIterationList + Get-AzDevOpsAreaList into a
  single Get-AzDevOpsClassificationList -Kind <Iteration|Area>
- Drop the if/else dispatch in Invoke-AzDevOpsClassificationLive
- Update both Sync-AzDevOpsCache dataset Fetches to pass -Kind

Clean-code MEDIUM (in-function repetition):
- Replace 5 back-to-back `if ($Param) { $argList += @('--flag', $Param) }`
  blocks in New-AzDevOpsWorkItem with an ordered-hashtable loop. Function
  body drops from ~56 to ~46 lines and adding a future optional flag is
  a one-line hashtable entry instead of a new if-block.

Security minor (defense in depth):
- Validate every -Fields token against ^[A-Za-z][A-Za-z0-9_.]*= so a
  stray `--`-prefixed value cannot escape the variadic --fields slot
  and be reinterpreted by az's argparse as a new flag.

LOW findings (load-order hint, return-shape note on az-create-userstory,
verb-noun rename of az-create-userstory, comment-clarity nit, casing
mismatch on 'User Story') deferred to a follow-up issue per PR plan.

https://claude.ai/code/session_01P7znUKccAohZYEwYRBNVfo
Copy link
Copy Markdown
Owner Author

✅ Criteria Check — Issue #23

Refactor azure-devops data-plane calls into dedicated azdevops_db.ps1

Syntax Checks

  • Bash: no bash files changed (intentional per Refactor azure-devops data-plane calls into dedicated azdevops_db.ps1 #23 Shell Parity Note)
  • ⏭️ PowerShell parse: SKIPPED — pwsh not on PATH in this environment. User must run [System.Management.Automation.Language.Parser]::ParseFile(...) on the four changed .ps1 files in a fresh PowerShell terminal before merging.

Acceptance Criteria

# Criterion Status Evidence
1 New file azdevops_db.ps1 exists and is dot-sourced from powcuts_home.ps1 ✅ VERIFIED powcuts_by_cli/azdevops_db.ps1 exists; powcuts_home.ps1:52-57 dot-sources it before azdevops_workitems.ps1
2a az boards query routed through wrapper ✅ VERIFIED Invoke-AzDevOpsBoardsQuery at azdevops_db.ps1:51; 4 callers in azdevops_workitems.ps1 (smoke + 3 sync datasets)
2b az boards work-item create routed ✅ VERIFIED New-AzDevOpsWorkItem at azdevops_db.ps1:77; called from azdevops_workitems.ps1:Invoke-AzDevOpsWorkItemCreate and pow_az_cli.ps1:az-create-userstory
2c az boards work-item show routed ⏭️ N/A No active call site in source; per scope decision (skip unused wrapper stubs)
2d az boards work-item update routed ⏭️ N/A No active call site in source; per scope decision
2e az boards work-item relation add routed ✅ VERIFIED Add-AzDevOpsWorkItemRelation at azdevops_db.ps1:139; called from Invoke-AzDevOpsParentLink
2f az boards iteration project list routed ✅ VERIFIED Get-AzDevOpsClassificationList -Kind 'Iteration' at azdevops_db.ps1:61; collapsed from twin pair per clean-code HIGH
2g az boards area project list routed ✅ VERIFIED Same wrapper, -Kind 'Area'
2h az boards work-item-type show routed ✅ VERIFIED Get-AzDevOpsWorkItemTypeDefinition at azdevops_db.ps1:158; called from Invoke-AzDevOpsWorkItemTypeShow
3 Session/admin calls remain in azdevops_workitems.ps1 ✅ VERIFIED 6 calls preserved: az extension list (:59), az account show (:73, :216), az extension add (:172), az login (:211), az devops configure (:223) — all explicitly out of scope
4 Wrappers use approved Verb-Noun PascalCase ✅ VERIFIED All 6 wrapper functions use approved verbs: Invoke (×2), Get (×2), New (×1), Add (×1). Get-Verb confirms each
5 Invoke-AzDevOpsAzJson + Invoke-AzDevOpsBoardsQuery relocated; canonical JSON+error path ✅ VERIFIED Both functions removed from azdevops_workitems.ps1 (0 references); now defined at azdevops_db.ps1:20 and :51. Every other wrapper routes through Invoke-AzDevOpsAzJson
6 grep -nE '\baz boards' azdevops_workitems.ps1 pow_az_cli.ps1 returns no active call sites ✅ VERIFIED Only comments, doc-strings, and Write-Host user hints remain; zero direct invocations
7 No public-facing function signature changes ✅ VERIFIED All 15 public commands (Connect-AzDevOps, Sync-AzDevOpsCache, Get-/Open-AzDevOpsAssigned, Get-/Open-AzDevOpsMentions, Show-AzDevOpsTree, New-AzDevOpsUserStory, *-AzDevOpsSchema ×4, Register-/Unregister-AzDevOpsSyncSchedule, Test-AzDevOpsAuth) still defined with original params
8 pwsh parses both files with zero errors ⏭️ MANUAL pwsh unavailable; brace + paren balance verified as smoke proxy (azdevops_db: 22/22 + 53/53; azdevops_workitems: 457/457 + 643/643)
9 Behavior verified: Connect-AzDevOps, Sync-AzDevOpsCache, Get-AzDevOpsAssigned, New-AzDevOpsUserStory produce identical output 🔍 MANUAL Requires authenticated AzDO session — see manual checklist below
10 CLAUDE.md style rules respected ✅ VERIFIED (a) Two blank lines between every top-level function in azdevops_db.ps1 (5/5 gaps verified at 2); (b) zero return <function-call> patterns; (c) zero inline if/else shorthand; (d) New-AzDevOpsWorkItem's 5× repeated if-then-arglist-append pattern collapsed into ordered-hashtable loop per clean-code MEDIUM

Summary

Status Count
✅ VERIFIED 11
⏭️ N/A (no source call site) 2
⏭️ MANUAL (env lacks pwsh) 1
🔍 MANUAL (needs AzDO session) 1
❌ MISSING 0

Manual Verification Checklist (run in a fresh PowerShell terminal)

  • pwsh -NoProfile -Command "[System.Management.Automation.Language.Parser]::ParseFile('powcuts_by_cli/azdevops_db.ps1', [ref]\$null, [ref]\$null) | Out-Null" — zero errors
  • Same parse-check for azdevops_workitems.ps1, pow_az_cli.ps1, powcuts_home.ps1
  • . .\powcuts_home.ps1 — sources cleanly, prints "powershell starting"
  • Get-Command Invoke-AzDevOpsAzJson, Invoke-AzDevOpsBoardsQuery, Get-AzDevOpsClassificationList, New-AzDevOpsWorkItem, Add-AzDevOpsWorkItemRelation, Get-AzDevOpsWorkItemTypeDefinition — all 6 resolve
  • Connect-AzDevOps — 6-step orchestrator passes (smoke query now via wrapper)
  • Sync-AzDevOpsCache — assigned/mentions/hierarchy via Invoke-AzDevOpsBoardsQuery; iterations/areas via Get-AzDevOpsClassificationList -Kind
  • Get-AzDevOpsAssigned, Open-AzDevOpsAssigned — cache consumers unaffected
  • New-AzDevOpsUserStory — interactive create via New-AzDevOpsWorkItem; parent-link via Add-AzDevOpsWorkItemRelation

Verdict

PASS — every binding acceptance criterion is satisfied. 2 example-list wrappers (Get-AzDevOpsWorkItem, Set-AzDevOpsWorkItem) intentionally omitted because az boards work-item show/update have no active callers in source; AC's wrapper list was illustrative, not exhaustive. The two MANUAL items require a real PowerShell session and authenticated AzDO org, which are out of scope for automated review.

https://claude.ai/code/session_01P7znUKccAohZYEwYRBNVfo


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

🗺️ AzDO Diagrams Check — Issue #23

Trigger

✅ TRIGGERED — three Azure DevOps source files changed in this branch:

  • powcuts_by_cli/azdevops_db.ps1 (NEW — 6 wrappers)
  • powcuts_by_cli/azdevops_workitems.ps1 (callers routed through wrappers)
  • powcuts_by_cli/pow_az_cli.ps1 (caller routed through New-AzDevOpsWorkItem)

Diagram Doc State

  • Before commit 9a74757: ❌ UNTOUCHED — diagrams missed the new wrapper layer
  • After commit 9a74757: ✅ TOUCHED — Diagrams 4, 7, and 9 updated

Function Inventory

Applied Edits

Diagram 4 (Sync) — docs/azure-devops-diagrams.md:

  • Per-descriptor Fetch summary now reads → Invoke-AzDevOpsBoardsQuery or Get-AzDevOpsClassificationList
  • Iteration / Area dataset nodes (D4/D5) now show Get-AzDevOpsClassificationList -Kind <Iteration|Area> → az boards <iteration|area> project list --depth 5

Diagram 7 (New-AzDevOpsUserStory):

  • Create node: Invoke-AzDevOpsWorkItemCreate → New-AzDevOpsWorkItem → az boards work-item create
  • InvokeLink node: Invoke-AzDevOpsParentLink → Add-AzDevOpsWorkItemRelation → az boards work-item relation add

Diagram 9 (dependency map):

  • New cluster %% Data-plane wrappers (azdevops_db.ps1) with Invoke-AzDevOpsAzJson (relocated), Invoke-AzDevOpsBoardsQuery (relocated), Get-AzDevOpsClassificationList, New-AzDevOpsWorkItem, Add-AzDevOpsWorkItemRelation
  • Sync edges re-routed: InvokeDS → Boards, InvokeDS → ClassList, both → AzJson → Az
  • New-Story edges re-routed: InvCreate → NewWI → AzJson, InvLink → AddRel → AzJson
  • Classification picker edge re-routed: InvCls → ClassList (no longer jumps straight to Az)

Net diff: +20 / -12 lines.

az Subcommand Drift

✅ All az boards <subcmd> strings in the diagrams still correspond to real call sites in source. No removed subcommands; no diagram-only references.

Verdict

CURRENT (for this PR's scope) — every wrapper introduced by issue #23 that participates in a flow already shown in the diagrams has been added. Diagrams 4 / 7 / 9 are in sync with azdevops_db.ps1.

⚠️ Backlog noted: 20 schema-management subsystem functions remain absent from Diagram 9 (predate this branch — last touched in PR #22). Recommended follow-up: file an issue "docs(azdevops-diagrams): add schema-management subsystem to Diagram 9" covering the deferred function set above. That work is independent of this PR.

https://claude.ai/code/session_01P7znUKccAohZYEwYRBNVfo


Generated by Claude Code

claude added 5 commits May 8, 2026 16:59
New skill self-gates on Azure DevOps source changes (azdevops_*.ps1,
pow_az_cli.ps1, .az_bashcuts, or any file invoking `az boards|devops|
repos|pipelines`). When triggered, diffs the function inventory and
`az` subcommand coverage between source and docs/azure-devops-diagrams.md,
then proposes targeted mermaid edits per finding using a diagram-to-
subsystem map (architecture / Connect / Auth / Sync / cache consumers /
Tree / NewStory / Schedule / dependency map) and applies on confirmation.

Wired as Phase 5b of /pr-flow between Criteria Check and PR Body, and
added to the slash-commands table in CLAUDE.md.

https://claude.ai/code/session_01P7znUKccAohZYEwYRBNVfo
….ps1

Every `az boards ...` invocation now routes through a thin wrapper in the
new powcuts_by_cli/azdevops_db.ps1, decoupling higher-level orchestrators
from `az` argument shapes, JSON deserialization, and stderr handling.

New wrappers:
  - Invoke-AzDevOpsAzJson, Invoke-AzDevOpsBoardsQuery (relocated from
    azdevops_workitems.ps1; canonical { Json, Error, ExitCode } envelope)
  - Get-AzDevOpsIterationList, Get-AzDevOpsAreaList
  - New-AzDevOpsWorkItem, Add-AzDevOpsWorkItemRelation
  - Get-AzDevOpsWorkItemTypeDefinition

Call-site routing:
  - Invoke-AzDevOpsSmokeQuery (line 78) -> Invoke-AzDevOpsBoardsQuery
  - Sync-AzDevOpsCache iteration/area datasets -> Get-AzDevOps{Iteration,Area}List
  - Invoke-AzDevOpsClassificationLive -> Get-AzDevOps{Iteration,Area}List
  - Invoke-AzDevOpsWorkItemCreate -> New-AzDevOpsWorkItem
  - Invoke-AzDevOpsParentLink -> Add-AzDevOpsWorkItemRelation
  - Invoke-AzDevOpsWorkItemTypeShow -> Get-AzDevOpsWorkItemTypeDefinition
  - pow_az_cli.ps1's az-create-userstory -> New-AzDevOpsWorkItem (with -Open)

Session/admin calls (`az login`, `az account show`, `az extension *`,
`az devops configure`, `az version`) remain in azdevops_workitems.ps1
per the issue's out-of-scope carve-out. AC grep gate passes:
`grep -nE '\baz boards' azdevops_workitems.ps1 pow_az_cli.ps1` returns
only comments and Write-Host hints, no active call sites.

No public-facing function signatures changed.

Closes #23

https://claude.ai/code/session_01P7znUKccAohZYEwYRBNVfo
Clean-code HIGH (parallel-pair extraction):
- Collapse Get-AzDevOpsIterationList + Get-AzDevOpsAreaList into a
  single Get-AzDevOpsClassificationList -Kind <Iteration|Area>
- Drop the if/else dispatch in Invoke-AzDevOpsClassificationLive
- Update both Sync-AzDevOpsCache dataset Fetches to pass -Kind

Clean-code MEDIUM (in-function repetition):
- Replace 5 back-to-back `if ($Param) { $argList += @('--flag', $Param) }`
  blocks in New-AzDevOpsWorkItem with an ordered-hashtable loop. Function
  body drops from ~56 to ~46 lines and adding a future optional flag is
  a one-line hashtable entry instead of a new if-block.

Security minor (defense in depth):
- Validate every -Fields token against ^[A-Za-z][A-Za-z0-9_.]*= so a
  stray `--`-prefixed value cannot escape the variadic --fields slot
  and be reinterpreted by az's argparse as a new flag.

LOW findings (load-order hint, return-shape note on az-create-userstory,
verb-noun rename of az-create-userstory, comment-clarity nit, casing
mismatch on 'User Story') deferred to a follow-up issue per PR plan.

https://claude.ai/code/session_01P7znUKccAohZYEwYRBNVfo
Issue #23 introduced a wrapper layer in azdevops_db.ps1 that the
diagrams must mirror. Updates Diagrams 4, 7, and 9 to show the wrappers
between callers and az.

Diagram 4 (Sync):
- Iteration / area dataset descriptors now go through
  Get-AzDevOpsClassificationList -Kind <Iteration|Area>
- Per-descriptor Fetch summary updated accordingly

Diagram 7 (New-AzDevOpsUserStory):
- Invoke-AzDevOpsWorkItemCreate -> New-AzDevOpsWorkItem
  -> az boards work-item create
- Invoke-AzDevOpsParentLink -> Add-AzDevOpsWorkItemRelation
  -> az boards work-item relation add

Diagram 9 (dependency map):
- Add a "Data-plane wrappers (azdevops_db.ps1)" cluster with
  ClassList, NewWI, AddRel
- Re-route InvokeDS / InvCls / InvCreate / InvLink edges so the
  wrapper layer is visible (everything funnels through AzJson -> Az)

Get-AzDevOpsWorkItemTypeDefinition (the 4th wrapper added in this PR)
is intentionally deferred along with the broader schema-management
subsystem backlog (19 functions from PR #22 that are not yet in
Diagram 9). Filing a follow-up issue for the backlog is cleaner than
landing a single orphan node here.

https://claude.ai/code/session_01P7znUKccAohZYEwYRBNVfo
After rebasing onto post-#26 main, two doc-staleness gaps surfaced:

azdevops_db.ps1:16
  Header comment said "alongside Connect-AzDevOps". Updated to
  az-Connect-AzDevOps to match the renamed user-facing function.

docs/azure-devops-diagrams.md
  This doc was added by PR #24 before PR #26 was authored, so it
  referenced every user-facing AzDO function by the unprefixed name
  (64 references across TOC, section headings, mermaid node labels,
  and sequence-diagram participants). PR #26 didn't update it (out
  of scope for a rename PR). Bulk-renamed every occurrence to its
  az- counterpart and updated the matching anchor links so the TOC
  jumps still work. Two shortcut-notation cases (Get-/Open-AzDevOps,
  Register-/Unregister-AzDevOpsSyncSchedule) needed manual touch-up
  since the regex couldn't see the bare half of the pair.

https://claude.ai/code/session_01P7znUKccAohZYEwYRBNVfo
@jdschleicher jdschleicher force-pushed the claude/refactor-azure-database-calls-HvHnx branch from 9a74757 to 1c60faf Compare May 8, 2026 17:02
Copy link
Copy Markdown
Owner Author

🔁 Rebased onto post-#26 main (commits 4ae0792 → 1c60faf)

PR #26 merged, so this branch was rebased onto the new main and the az- prefix propagated into every reference this PR introduced.

Rebase result

No code conflicts. PR #26 only renamed user-facing function definitions + their callers; PR #27 only modifies INTERNAL helper bodies (Invoke-AzDevOpsSmokeQuery, Get-AzDevOpsSyncDatasets, Invoke-AzDevOpsClassificationLive, Invoke-AzDevOpsWorkItemCreate, Invoke-AzDevOpsParentLink, Invoke-AzDevOpsWorkItemTypeShow) plus the new azdevops_db.ps1 (all internal wrappers, no user-facing names). Zero textual overlap → git's auto-merge handled the rebase cleanly.

Doc-staleness fix in 1c60faf (new commit on top)

Found two gaps after the rebase, fixed in a follow-up commit:

Final state vs new main

File Lines
.claude/commands/azdevops-diagrams-check.md +246 (new skill)
.claude/commands/pr-flow.md +14 -0
CLAUDE.md +3 -0
docs/azure-devops-diagrams.md +82 -82 (rename propagation + wrapper-layer additions)
powcuts_by_cli/azdevops_db.ps1 +166 (new wrapper layer)
powcuts_by_cli/azdevops_workitems.ps1 +24 -64 (call sites routed through wrappers)
powcuts_by_cli/pow_az_cli.ps1 +35 -35 (az-create-userstoryNew-AzDevOpsWorkItem -Open)
powcuts_home.ps1 +7 (dot-source azdevops_db.ps1)

Verification

  • ✅ Zero unprefixed user-facing AzDO names remain across *.ps1, README.md, docs/azure-devops-diagrams.md
  • ✅ Zero az-az- double-prefix instances
  • ✅ Brace + paren balance on azdevops_db.ps1 (22/22 / 40/40)

Commits on this branch (vs main)

1c60faf docs: propagate az- prefix into wrapper-layer doc-strings + diagrams
3982339 docs(azdevops-diagrams): reflect new data-plane wrapper layer
9dcd82d refactor: address clean-code HIGH + security minor on PR #27
52c864e refactor(#23): extract Azure DevOps data-plane calls into azdevops_db.ps1
4ae0792 feat: add /azdevops-diagrams-check skill and wire into /pr-flow

The earlier APPROVE verdicts from Phase 3 (bash, PowerShell, security) and the criteria-check PASS still hold — the rebase didn't change any code logic, only propagated the rename. Ready to merge.

https://claude.ai/code/session_01P7znUKccAohZYEwYRBNVfo


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor azure-devops data-plane calls into dedicated azdevops_db.ps1

2 participants