PowerShell Script Consolidation
July 16, 2026 · View on GitHub
Scope: Sprint-0006/0007. Inventory of standalone .ps1 scripts that
live outside any PowerShell module, the rationale for consolidating them
into modules, and the rules that govern when a new standalone script is
acceptable vs. when it should become a module function.
Strategy update (sprint-0007 — Immutable Build). The build / pack / publish / promote / Chocolatey / WinGet automation surface is centralized in
ATAP.Utilities.BuildTooling.PowerShellas a set of cmdlets that are called by BuildMaster stages and developer workflows alike. The cmdlets deliberately replace the older "one orchestrator script per repo" pattern (Publish-ATAPUtilities.ps1) — the new functions are listed in BuildMaster-Pipeline-Topology.md §4. The classification rules in §2 still apply, but the new cmdlets all fall into bucket A (module function) and any standalone script that duplicates their behavior is now a candidate for deletion. See §10.4.
Audience: Developers who reach for "I'll just write a quick .ps1",
maintainers planning the sprint-0007 cleanup, anyone trying to discover
"where does the script that does X live?"
Status: Authoritative for sprint-0006. Includes a curated inventory that will go stale; treat the categorization as load-bearing, the file list as a snapshot.
Not in this doc:
- How modules themselves are built / tested / packed → see the four PowerShell module docs.
- C# console apps that wrap PowerShell entry points → out of scope.
- Skills, agents, and the
.claude/folder → governed separately by SharedVSCode.
1. Why scripts proliferate
Loose .ps1 files accumulate in a multi-repo PowerShell-heavy ecosystem
because:
- No bootstrap cost.
Foo.ps1is one file; a module is a folder with a manifest, public/private split, tests, and aversion.json. - Per-repo entry points. Each repo has a few "developer affordances"
that are not really cmdlets —
Publish-ATAPUtilities.ps1at repo root, plus workstation setup tools such assrc/ATAP.Utilities.BuildTooling.PowerShell/tools/Setup-GitHubMCP.ps1andsrc/ATAP.Utilities.BuildTooling.PowerShell/tools/Test-GitHubMCP.ps1. - CI hook scripts.
_generated/Check-ProGetFeeds.ps1,_generated/Fix-PowershellGetFeedNames.ps1get dropped during sprint investigations and never relocated. - Module-internal scripts authored as standalones first. A function
typically exists as
Verb-Noun.ps1inpublic/because the build concatenates them — but a script that pre-dates the module conversion gets stranded outside. - Database helper scripts.
Database/Powershell/public/*.ps1is a PowerShell folder but not a published module — Flyway invocation wrappers, schema parity checks.
The consolidation problem is real but bounded — the inventory in §3 counts ~25 truly-standalone scripts across the four repos.
2. The classification rule
Every .ps1 falls into exactly one of these five buckets:
| Bucket | Definition | Lives where | Versioned? |
|---|---|---|---|
| A. Module function | Becomes part of <Module>.psm1 via Build-PSModulePsm1 | src/<Module>/public/, private/, or lib/ | Yes (NBGV) |
| B. Module test | Pester test for a module function | src/<Module>/tests/ | n/a |
| C. Loose tool | Developer affordance scoped to one repo/workstation (publish, setup, smoke-test) | repo root or src/ATAP.Utilities.BuildTooling.PowerShell/tools/ for shared build/tooling setup | No (git-tracked, but not packaged) |
| D. Generated | Output of a build/diagnostic step | _generated/ (per SC-0033) | No |
| E. Vendor | Imported from a third-party package's distribution | wherever the package put it (e.g. bin/Debug/.playwright/) | n/a |
Bucket E is never consolidated — those files are restored by dotnet restore or npm install and editing them is futile.
Bucket D is never consolidated into source — the scripts under
_generated/ may be deleted at any time. If a _generated/ script proves
useful, it gets promoted to bucket A or C.
Buckets A and B are governed entirely by the Build-Process, Versioning, Pack-and-Publish, and Test-Process docs.
The interesting bucket — and the focus of this doc — is C.
3. Inventory of standalone scripts (sprint-0006)
3.1 ATAP.Utilities
| Path | Bucket | Purpose | Disposition |
|---|---|---|---|
Publish-ATAPUtilities.ps1 | C | Iterate projects and publish to ProGet feeds | Delete — replace with Invoke-DotnetBuildWithRetry / Invoke-ModuleBuildWithRetry |
src/ATAP.Utilities.BuildTooling.PowerShell/tools/Setup-GitHubMCP.ps1 | C | One-time GitHub MCP server setup | Keep as BuildTooling setup tool |
src/ATAP.Utilities.BuildTooling.PowerShell/tools/Test-GitHubMCP.ps1 | C | Smoke-test GitHub MCP after setup | Keep as BuildTooling setup tool |
Database/Powershell/public/Export-RuleToTextFile.ps1 | C/A | Schema rule export | Promote to module (Database utilities) |
Database/Powershell/public/Rebuild-All.ps1 | C | Flyway rebuild orchestrator | Keep — not a function |
Database/Powershell/public/Example-RuleExport.ps1 | C | Demo / docs example | Move to Documentation/ |
Database/Powershell/tests/*.Tests.ps1 | B | Pester tests for the un-modularized DB scripts | Move with the promotion |
OlderDBsForReference/**/*.ps1 | n/a | Archive — pre-Flyway era | Delete in sprint-0007 |
src/.../public/Obsolete/*.ps1 | n/a | Marked obsolete years ago | Delete in sprint-0007 |
3.2 AceCommander
| Path | Bucket | Purpose | Disposition |
|---|---|---|---|
powershell/public/Invoke-AceCommanderTests.ps1 | A | Sole function in an unbuilt module folder | Wire into the module build |
AceCommander.Server.Tests/E2E/UserInformationAndSettings.Tests.ps1 | B | Pester E2E test | Keep — already in correct location |
AceCommander.Server.Tests/bin/.../*.ps1 | E | Playwright vendor scripts | Ignore |
_generated/*.ps1 | D | Sprint-0006 ProGet diagnostics | Delete after sprint closes |
3.3 _Planning — RESOLVED 2026-07-07 (Sprint 0012 Task 12.46.e)
The _Planning\Powershell\ folder was removed. All planning commands live in
ATAP.Utilities.BuildTooling.PowerShell as cmdlets (Save-SprintWorkSession — R-15
now names the BuildTooling path as canonical — Start-PlanningSession,
Complete-PlanningSession, Add-ScopeCreepIdea); the _Planning copy of
Save-SprintWorkSession.ps1 had already been reduced to a thin wrapper and was
deleted rather than promoted to a _Planning.PowerShell module.
The _Planning/Powershell/Public/ folder follows the layout of a
PowerShell module's public directory but lacks a .psd1. It is a
half-modularized state — the cleanup is to add the manifest and
version.json, then run the standard build flow.
3.4 SharedVSCode
No standalone .ps1 files. SharedVSCode hosts shell snippets and skills
in .claude/, not executable PowerShell.
3.5 ATAP.IAC
Most scripts here are infrastructure-as-code (Ansible playbooks
themselves are YAML, but wrapper scripts are PowerShell). Many were
already consolidated into ATAP.Utilities.IAC.Ansible.Powershell.
Remaining stragglers are bucket C (one-off provisioning scripts).
4. The "is this a function or a script" decision tree
When unsure whether a new .ps1 should be a module function or a
standalone, walk this tree:
Is it a verb-noun cmdlet that takes parameters?
├── Yes → does it fit an existing module's purpose?
│ ├── Yes → write Verb-Noun.ps1 in that module's public/
│ └── No → does the new module have ≥ 3 functions today + planned?
│ ├── Yes → create the new module (folder, .psd1, version.json)
│ └── No → write it as a standalone in the appropriate bucket
└── No (it's a procedural orchestrator: "do these N steps in order")
├── Is it the entry point for a developer workflow? → bucket C, repo root or tooling folder
├── Is it a CI/CD step? → bucket C, repo root, tooling folder, or under `build/`
└── Is it a one-time fix? → bucket D, `_generated/`, then delete
5. Promotion procedure: standalone → module function
Steps to promote Foo.ps1 (standalone) into ATAP.Utilities.X.Powershell:
- Rename to
Verb-Noun.ps1if not already cmdlet-named. - Wrap in
function Verb-Noun { ... }with a proper[CmdletBinding()]block, parameter validation, and PSFramework logging (per.claude/rules/PowerShell.md). - Move to
src/<Module>/public/(orprivate/if not exported). - Add a Pester test at
src/<Module>/tests/Unit/Verb-Noun.Tests.ps1. - Update
FunctionsToExportin the source.psd1template — or leave it to be auto-populated by the build (preferred). - Delete the original standalone.
- Search the codebase for callers (
Grep-able) and update them toImport-Module <Module>; Verb-Noun ...instead of dot-sourcing the old path.
The cleanup commit message convention is
refactor(<module>): promote Foo.ps1 to Verb-Noun in <Module>.
6. Why loose scripts (bucket C) stay loose
It is tempting to consolidate every script into a module. Resist this for bucket C because:
- No version semantics.
Publish-ATAPUtilities.ps1is a developer convenience, not an API. Versioning it makes the next ProGet pipeline weirder, not safer. - Repository context must be explicit. Many bucket-C scripts depend on
git rev-parse --show-toplevel; if relocated into a tooling folder, they must resolve the repository root from their own path or from Git. - Developer discoverability. Repo-specific entry points can stay at the
repo root. Shared workstation/build setup helpers should live in the
BuildTooling
tools/folder and be linked from the setup runbooks. - Breakage radius is local. A bug in
Setup-GitHubMCP.ps1affects one developer's workstation; a bug in a cmdlet that gets imported everywhere causes wider damage.
The rule is: a bucket-C script may grow up to ~150 lines. Beyond that, extract the core logic into a module function and keep the script as a thin wrapper.
7. The _generated/ discipline (bucket D)
Scripts under _generated/ are throw-away. They:
- Are produced by an investigation, prototype, or sprint-scoped fix.
- Are not git-ignored by default (so the user can review and decide).
- Should be deleted at sprint close as part of housekeeping.
- Must never be referenced by import paths from anywhere outside
_generated/.
If a _generated/ script proves load-bearing for more than one sprint,
that is the trigger to either promote it (§5) or move it to bucket C.
Per SC-0033, no agent or script should ever write outputs anywhere
other than _generated/. The corollary: every .ps1 you find under
_generated/ is a candidate for deletion the moment it is no longer
useful.
8. Boundary between modules and the build-tooling module
ATAP.Utilities.BuildTooling.PowerShell is a special case: it consolidates
~70 build/test/publish helpers under one module. The temptation is to
treat it as a junk drawer for any script related to "the build."
Resist by applying these rules:
Build-*,Pack-*,Publish-*verbs → belong here.Confirm-*,Test-*(workstation health) → belong here.Invoke-Git*Hook→ belong here (used by all repos via the.claude/junction).- One-shot
Fix-*from a single sprint → bucket D, then delete. - Repo-specific publish wrappers → bucket C in that repo, not here.
The 5-tier publish cmdlets (Publish-PSModuleToProGetFeed,
Get-PSModuleVersionFromNBGV, Compress-PSModuleArtifacts,
Test-CodeCoverageGate) all correctly live in this module — they are
ecosystem-wide and tier-aware.
9. Cross-repo invocation (the .claude/ junction)
The .claude/ folder at each repo root is an NTFS junction to
SharedVSCode/.claude/. Scripts there (skills, agent prompts, hooks)
appear in every repo simultaneously.
Rules:
- A script under
.claude/may invoke any module function from any repo, but it should not assume the module is imported — it mustImport-Module <Module> -ErrorAction Stopfirst. - A script under
.claude/must not write outside_generated/of the invoking repo. - Editing a file under
.claude/from any of the four repos changes behavior in all four — confirm with the user before doing so.
10. Known drift and gaps (sprint-0006)
-
Database/Powershell/lacks a manifest. It looks like a module, it haspublic/,tests/, but no.psd1. Either promote to a real module or rename toDatabase/scripts/. -
_Planning/Powershell/Public/is similarly half-modularized. Tracked for sprint-0007. -
OlderDBsForReference/and*/Obsolete/are dead weight. Multiple.ps1files marked obsolete years ago. Slated for deletion. -
Both copies of
Publish-ATAPUtilities.ps1are slated for deletion. The repo-root script (Publish-ATAPUtilities.ps1) and the module function (BuildTooling/public/Publish-ATAPUtilities.ps1) will both be removed. Callers that publish C# libraries should callInvoke-DotnetBuildWithRetryinstead. Callers that publish PowerShell modules should callInvoke-ModuleBuildWithRetry, which was added in sprint-0006 and orchestratesmodule.build.ps1viaInvoke-Buildwith NBGV-derived tier resolution and automatic retry. -
No automated lint for "this
.ps1should be a function". A PSScriptAnalyzer custom rule could flag standalone scripts that definefunction Verb-Nounand recommend module promotion. -
_generated/cleanup is automated.Clear-SprintGeneratedArtifactsinATAP.Utilities.BuildTooling.PowerShellis called by SprintEndAgent Step 10.7 with-SprintNumber $closedSprintNumber. It removes all contents of every sprint worktree's_generated/directory. Pass-WhatIfto preview and-Forceto suppress the confirmation prompt. (Implemented in sprint-0006; SC-0033 task 7.4-2.)
11. Quick reference
Find all standalone .ps1 files in this repo (excludes module sources,
tests, vendor, generated):
Get-ChildItem -Path . -Filter '*.ps1' -Recurse -File |
Where-Object {
$p = $_.FullName
$p -notmatch '\\(public|private|lib|tests)\\' -and
$p -notmatch '\\bin\\' -and
$p -notmatch '\\obj\\' -and
$p -notmatch '\\_generated\\' -and
$p -notmatch '\\node_modules\\'
} |
Select-Object -ExpandProperty FullName
Promote one script (mechanical scaffold):
$src = './Publish-Foo.ps1'
$module = './src/ATAP.Utilities.X.Powershell'
$verbNoun = 'Publish-Foo'
Copy-Item $src "$module/public/$verbNoun.ps1"
# manually wrap in function Verb-Noun { ... } and add CmdletBinding
New-Item -ItemType Directory "$module/tests/Unit" -Force | Out-Null
# author $module/tests/Unit/$verbNoun.Tests.ps1
git rm $src
Delete sprint-end _generated/ cruft (review before running):
Get-ChildItem ./_generated -Filter '*.ps1' | Remove-Item -WhatIf
Related Documents
- Production-and-Tooling-Overview.md — index.
- PowerShell-Modules-Build-Process.md — how a promoted script becomes part of a module.
- PowerShell-Modules-Test-Process.md — Pester test that the promotion must add.
- PowerShell-Modules-Pack-and-Publish.md — how the consolidated module ships.
- PowerShell-Modules-Versioning.md — version semantics that bucket-C scripts deliberately avoid.