create-qa-architect 5.12.0 → 5.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.github/dependabot.yml +10 -30
  2. package/.github/workflows/claude-md-validation.yml +5 -7
  3. package/.github/workflows/dependabot-auto-merge.yml +1 -0
  4. package/.github/workflows/quality.yml +26 -12
  5. package/.github/workflows/release.yml +2 -1
  6. package/.github/workflows/stale-prs.yml +42 -0
  7. package/.github/workflows/weekly-gitleaks-verification.yml +6 -4
  8. package/LICENSE +3 -3
  9. package/README.md +19 -20
  10. package/config/quality-config.schema.json +1 -1
  11. package/docs/CI-COST-ANALYSIS.md +8 -8
  12. package/docs/DEPLOYMENT.md +1 -1
  13. package/docs/DEVELOPMENT-WORKFLOW.md +2 -2
  14. package/docs/TURBOREPO-SUPPORT.md +3 -3
  15. package/docs/dev_guide/CONVENTIONS.md +132 -0
  16. package/eslint.config.cjs +25 -0
  17. package/lib/blob-storage.js +57 -0
  18. package/lib/commands/analyze-ci.js +267 -27
  19. package/lib/commands/deps.js +5 -5
  20. package/lib/commands/license-commands.js +2 -2
  21. package/lib/commands/maturity-check.js +20 -2
  22. package/lib/dependency-monitoring-basic.js +4 -4
  23. package/lib/dependency-monitoring-premium.js +5 -5
  24. package/lib/license-validator.js +1 -1
  25. package/lib/licensing.js +3 -3
  26. package/lib/smart-strategy-generator.js +1 -1
  27. package/lib/validation/documentation.js +2 -0
  28. package/lib/workflow-config.js +106 -61
  29. package/package.json +51 -21
  30. package/scripts/deploy-consumers.sh +369 -0
  31. package/scripts/pattern-check.sh +607 -0
  32. package/scripts/run-semgrep.sh +244 -0
  33. package/scripts/smart-test-strategy.sh +1 -1
  34. package/setup.js +62 -32
  35. package/templates/CLAUDE_WORKFLOW_POLICY.md +3 -3
  36. package/templates/scripts/smart-test-strategy.sh +1 -1
  37. package/.github/workflows/auto-release.yml +0 -39
@@ -0,0 +1,132 @@
1
+ # Dev Guide — QA Architect
2
+
3
+ > Load at session start. Replaces blind codebase exploration.
4
+ > **Last updated:** 2026-03-08
5
+
6
+ ## What This Project Does
7
+
8
+ QA Architect (`create-qa-architect`) is a CLI tool that bootstraps quality automation for JavaScript/TypeScript, Python, and shell script projects. One command installs ESLint, Prettier, Husky, lint-staged, and GitHub Actions. Pro tier adds Gitleaks security scanning, Smart Test Strategy, and multi-language support.
9
+
10
+ **Tech stack:** Node.js (CommonJS, no build step) · Vanilla JS (no framework) · GitHub Actions templates · c8 for coverage · Playwright for E2E
11
+
12
+ **Entry point:** `setup.js` — CLI argument parsing and orchestration. Run as `npx create-qa-architect` or `node setup.js`.
13
+
14
+ **npm package:** `create-qa-architect` v5.13.2 (published via GitHub trusted publishing)
15
+
16
+ ## Directory Structure
17
+
18
+ ```
19
+ qa-architect/
20
+ ├── setup.js # Main CLI entry — arg parsing + orchestration
21
+ ├── lib/ # Business logic modules
22
+ │ ├── licensing.js # Freemium tier system (FREE/PRO), feature gates, usage caps
23
+ │ ├── project-maturity.js# Detects project maturity stage (minimal → production-ready)
24
+ │ ├── workflow-config.js # CI workflow generation + tier transformations
25
+ │ ├── smart-strategy-generator.js # Risk-based test selection (Pro feature)
26
+ │ ├── template-loader.js # Custom template merging
27
+ │ ├── commands/ # Command handlers (validate, deps, analyze-ci)
28
+ │ ├── validation/ # Validators (security, docs, config)
29
+ │ └── interactive/ # TTY prompt system
30
+ ├── templates/ # Config file templates deployed to consumer repos
31
+ │ ├── ci/ # GitHub Actions workflow templates
32
+ │ └── scripts/ # Helper scripts deployed to consumers
33
+ ├── config/ # Language-specific configs (Python, Shell)
34
+ ├── scripts/ # Dev/ops scripts (deploy-consumers, e2e tests, etc.)
35
+ ├── tests/ # 40+ test files (Node's assert module, no test runner)
36
+ ├── docs/ # Dev guides and plans
37
+ │ ├── dev_guide/ # This file and other dev references
38
+ │ └── plans/ # Agent planning docs (/bs:plan output)
39
+ └── .claude/ # Claude Code workspace metadata
40
+ ```
41
+
42
+ ## Key Files
43
+
44
+ | File | Role |
45
+ | --------------------------------------------- | ----------------------------------------------------------- |
46
+ | `setup.js:390-500` | Main entry — arg parsing, interactive mode, command routing |
47
+ | `setup.js:985-2143` | `runMainSetup()` — core setup flow |
48
+ | `lib/licensing.js` | All tier logic, usage caps, feature gates |
49
+ | `lib/project-maturity.js` | Maturity detection algorithm |
50
+ | `lib/workflow-config.js` | CI workflow generation, mode detection, matrix injection |
51
+ | `lib/template-loader.js` | Custom template merging |
52
+ | `config/defaults.js` | Default scripts, dependencies, lint-staged config |
53
+ | `scripts/deploy-consumers.sh` | Auto-discovers + deploys to all consumer repos |
54
+ | `tests/consumer-workflow-integration.test.js` | Gates what can appear in consumer CI output |
55
+
56
+ ## Conventions
57
+
58
+ **Language:** Plain JavaScript (CommonJS). No TypeScript in the main source. `QAA_DEVELOPER=true` env var bypasses license checks in tests.
59
+
60
+ **Naming:** kebab-case files, camelCase functions/vars. Test files: `tests/[feature].test.js`.
61
+
62
+ **Feature addition pattern:**
63
+
64
+ 1. Add feature gate check in `lib/licensing.js` if Pro-only
65
+ 2. Implement in appropriate `lib/` module
66
+ 3. Wire into `setup.js` argument parsing if it needs a CLI flag
67
+ 4. Add test file: `tests/[feature].test.js` using Node `assert` module
68
+ 5. Add to the `npm test` chain in `package.json`
69
+
70
+ **Template-as-Product contract** — `quality.yml` is BOTH qa-architect's own CI AND the template deployed to 15+ consumer repos. Rules:
71
+
72
+ - Never reference `node_modules/create-qa-architect` in templates — consumers use `npx @latest`
73
+ - Never use `\s*` in YAML cleanup regexes — use `[ \t]*` (avoids newline collapse)
74
+ - Conditional content uses `# {{NAME_BEGIN/END}}` section markers, stripped by `stripSection()`
75
+ - `CONSUMER_FORBIDDEN_CONTENT` in `consumer-workflow-integration.test.js` is a hard gate
76
+
77
+ **Workflow tiers:**
78
+
79
+ - Minimal (default): single Node 22, weekly security, path filters (~$0-5/mo)
80
+ - Standard: single Node 22, tests on main only (~$5-10/mo)
81
+ - Comprehensive: matrix every commit (~$100-350/mo)
82
+
83
+ **Testing approach:** Tests use real filesystem with temp directories (no mocks). `createTempGitRepo()` is the standard test setup helper.
84
+
85
+ **Publishing:** Never run `npm publish` manually. GitHub Actions handles publishing via trusted publishing when `package.json` version changes on `main`.
86
+
87
+ ## Running the Project
88
+
89
+ ```bash
90
+ # Install dependencies
91
+ npm install
92
+
93
+ # Run all tests (40+ files, ~2-3 min)
94
+ QAA_DEVELOPER=true npm test
95
+
96
+ # Fast unit tests only
97
+ npm run test:unit
98
+
99
+ # Single test file
100
+ QAA_DEVELOPER=true node tests/licensing.test.js
101
+
102
+ # CLI smoke test (dry run — no changes)
103
+ node setup.js --dry-run
104
+
105
+ # Validate before release
106
+ npm run prerelease
107
+
108
+ # Deploy to consumer repos (after publishing)
109
+ ./scripts/deploy-consumers.sh # validate only
110
+ ./scripts/deploy-consumers.sh --push # regenerate + commit + push
111
+ ```
112
+
113
+ ## Agent Gotchas
114
+
115
+ - **`QAA_DEVELOPER=true`** must be set for most tests — it bypasses license checks. Without it, tests fail with license errors.
116
+ - **Never `npm publish` manually** — GitHub trusted publishing only. Use `npm version patch/minor/major` + push tags.
117
+ - **Template changes affect 15+ consumer repos** — always run `tests/consumer-workflow-integration.test.js` after template edits.
118
+ - **`\s` in YAML regexes will collapse newlines** — use `[ \t]*` for any whitespace-trimming regex on YAML content.
119
+ - **Coverage thresholds:** 75% lines, 70% functions, 65% branches (enforced by `c8`).
120
+ - **No Vitest/Jest** — tests use Node's built-in `assert` module and are run directly with `node tests/*.test.js`.
121
+ - **Pre-push hook** runs `test:patterns`, `test:commands`, `test:changed` — these must all pass before push.
122
+ - **`.claude` directory** already exists (has prior workspace data) — do not overwrite its contents.
123
+
124
+ ## Active Development Areas
125
+
126
+ From recent git log:
127
+
128
+ - Dependency updates (Dependabot active)
129
+ - CI cost optimization (minute budget guardrails, monthly vs weekly security scans)
130
+ - Staged rollout / canary deployment for consumer updates
131
+ - Vercel Blob integration for webhook handler (replacing filesystem storage)
132
+ - Documentation consistency improvements
package/eslint.config.cjs CHANGED
@@ -37,6 +37,11 @@ if (security) {
37
37
 
38
38
  // Base rules configuration
39
39
  const baseRules = {
40
+ // Complexity gates (AI quality)
41
+ complexity: ['warn', 15],
42
+ 'max-depth': ['warn', 4],
43
+ 'max-params': ['warn', 5],
44
+
40
45
  // XSS Prevention patterns - critical for web applications
41
46
  'no-eval': 'error',
42
47
  'no-implied-eval': 'error',
@@ -112,4 +117,24 @@ if (tsPlugin && tsParser) {
112
117
  })
113
118
  }
114
119
 
120
+ // Import verification (eslint-plugin-n)
121
+ let nPlugin = null
122
+ try {
123
+ nPlugin = require('eslint-plugin-n')
124
+ } catch {
125
+ // eslint-plugin-n not installed
126
+ }
127
+
128
+ if (nPlugin) {
129
+ configs.push({
130
+ files: ['**/*.{js,mjs,cjs}'],
131
+ plugins: { n: nPlugin },
132
+ rules: {
133
+ 'n/no-missing-require': 'error',
134
+ 'n/no-missing-import': 'off', // Often handled by bundlers
135
+ 'n/no-unpublished-require': 'off',
136
+ },
137
+ })
138
+ }
139
+
115
140
  module.exports = configs
@@ -0,0 +1,57 @@
1
+ 'use strict'
2
+
3
+ const { put, head } = require('@vercel/blob')
4
+
5
+ const BLOB_PREFIX = 'licenses/'
6
+
7
+ const BLOB_PATHS = {
8
+ private: `${BLOB_PREFIX}legitimate-licenses.json`,
9
+ public: `${BLOB_PREFIX}legitimate-licenses.public.json`,
10
+ }
11
+
12
+ /**
13
+ * Load JSON from a Vercel Blob path.
14
+ * Returns null ONLY if the blob does not exist (first-run).
15
+ * Throws on infrastructure errors so callers can distinguish
16
+ * "empty" from "broken".
17
+ */
18
+ async function loadBlob(blobPath) {
19
+ let metadata
20
+ try {
21
+ metadata = await head(blobPath)
22
+ } catch (error) {
23
+ if (error.code === 'blob_not_found' || error.name === 'BlobNotFoundError') {
24
+ return null
25
+ }
26
+ throw new Error(`Blob head failed for ${blobPath}: ${error.message}`)
27
+ }
28
+
29
+ const response = await fetch(metadata.url)
30
+ if (!response.ok) {
31
+ throw new Error(
32
+ `Blob fetch failed for ${blobPath}: HTTP ${response.status}`
33
+ )
34
+ }
35
+
36
+ try {
37
+ return await response.json()
38
+ } catch (error) {
39
+ throw new Error(`Blob JSON parse failed for ${blobPath}: ${error.message}`)
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Save JSON to a Vercel Blob path.
45
+ * Throws on failure so callers know the write did not persist.
46
+ */
47
+ async function saveBlob(blobPath, data) {
48
+ const content = JSON.stringify(data, null, 2)
49
+ return put(blobPath, content, {
50
+ access: /** @type {const} */ ('public'),
51
+ addRandomSuffix: false,
52
+ allowOverwrite: true,
53
+ contentType: 'application/json',
54
+ })
55
+ }
56
+
57
+ module.exports = { loadBlob, saveBlob, BLOB_PATHS }
@@ -13,6 +13,11 @@ const { execSync } = require('child_process')
13
13
  const yaml = require('js-yaml')
14
14
  const { showProgress } = require('../ui-helpers')
15
15
 
16
+ const DAYS_PER_MONTH = 30
17
+ const DEFAULT_PULL_REQUEST_FACTOR = 0.8
18
+ const DEFAULT_MANUAL_RUNS_PER_MONTH = 1
19
+ const DEFAULT_RELEASE_RUNS_PER_MONTH = 1
20
+
16
21
  /**
17
22
  * Discover all GitHub Actions workflow files in the project
18
23
  * @param {string} projectPath - Root path of the project
@@ -151,25 +156,238 @@ function getCommitFrequency(projectPath, days = 30) {
151
156
  }
152
157
  }
153
158
 
159
+ /**
160
+ * Normalize a GitHub Actions `on` declaration into a trigger object.
161
+ * @param {string|string[]|object} onConfig - Workflow `on` section
162
+ * @returns {object} Normalized trigger object
163
+ */
164
+ function normalizeTriggers(onConfig) {
165
+ if (!onConfig) return {}
166
+ if (typeof onConfig === 'string') return { [onConfig]: true }
167
+ if (Array.isArray(onConfig)) {
168
+ return onConfig.reduce((acc, eventName) => {
169
+ if (typeof eventName === 'string') {
170
+ acc[eventName] = true
171
+ }
172
+ return acc
173
+ }, {})
174
+ }
175
+ if (typeof onConfig === 'object') return onConfig
176
+ return {}
177
+ }
178
+
179
+ /**
180
+ * Count cron field slots for rough monthly frequency estimation.
181
+ * @param {string} field - Cron field expression
182
+ * @param {number} maxSlots - Max slots in field (minute=60, hour=24)
183
+ * @returns {number} Estimated slot count
184
+ */
185
+ function countCronFieldSlots(field, maxSlots) {
186
+ if (!field || field === '*') return 1
187
+
188
+ if (field.includes(',')) {
189
+ return field
190
+ .split(',')
191
+ .map(part => part.trim())
192
+ .reduce((sum, part) => sum + countCronFieldSlots(part, maxSlots), 0)
193
+ }
194
+
195
+ if (field.includes('/')) {
196
+ const [base, stepRaw] = field.split('/')
197
+ const step = Number(stepRaw)
198
+ if (!Number.isFinite(step) || step <= 0) return 1
199
+
200
+ if (!base || base === '*') {
201
+ return Math.max(1, Math.ceil(maxSlots / step))
202
+ }
203
+
204
+ if (base.includes('-')) {
205
+ const [startRaw, endRaw] = base.split('-')
206
+ const start = Number(startRaw)
207
+ const end = Number(endRaw)
208
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) {
209
+ return 1
210
+ }
211
+ return Math.max(1, Math.ceil((end - start + 1) / step))
212
+ }
213
+ }
214
+
215
+ if (field.includes('-')) {
216
+ const [startRaw, endRaw] = field.split('-')
217
+ const start = Number(startRaw)
218
+ const end = Number(endRaw)
219
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) {
220
+ return 1
221
+ }
222
+ return Math.max(1, end - start + 1)
223
+ }
224
+
225
+ return 1
226
+ }
227
+
228
+ /**
229
+ * Estimate monthly runs from schedule cron expressions.
230
+ * @param {Array|object} scheduleConfig - Workflow schedule config
231
+ * @returns {number} Estimated runs per month
232
+ */
233
+ function estimateScheduleRunsPerMonth(scheduleConfig) {
234
+ const schedules = Array.isArray(scheduleConfig)
235
+ ? scheduleConfig
236
+ : scheduleConfig
237
+ ? [scheduleConfig]
238
+ : []
239
+
240
+ if (schedules.length === 0) return 0
241
+
242
+ return schedules.reduce((total, entry) => {
243
+ const cron =
244
+ entry && typeof entry.cron === 'string' ? entry.cron.trim() : ''
245
+ const parts = cron.split(/\s+/)
246
+ if (parts.length !== 5) {
247
+ return total + 4
248
+ }
249
+
250
+ const [
251
+ minuteField,
252
+ hourField,
253
+ dayOfMonthField,
254
+ monthField,
255
+ dayOfWeekField,
256
+ ] = parts
257
+
258
+ const minuteSlots = countCronFieldSlots(minuteField, 60)
259
+ const hourSlots = countCronFieldSlots(hourField, 24)
260
+ const timeSlots = Math.max(1, minuteSlots * hourSlots)
261
+
262
+ let baseRuns = DAYS_PER_MONTH
263
+ if (dayOfWeekField !== '*') {
264
+ const dowSlots = countCronFieldSlots(dayOfWeekField, 7)
265
+ baseRuns = Math.ceil(dowSlots * 4.3) // ~4.3 weeks/month
266
+ } else if (dayOfMonthField !== '*') {
267
+ baseRuns = 1
268
+ } else if (monthField !== '*') {
269
+ baseRuns = 1
270
+ }
271
+
272
+ return total + Math.max(1, Math.ceil(baseRuns * timeSlots))
273
+ }, 0)
274
+ }
275
+
276
+ /**
277
+ * Estimate workflow runs/month from trigger type.
278
+ * @param {object} workflow - Parsed workflow object
279
+ * @param {number} commitsPerDay - Average commits/day
280
+ * @param {object} [options={}] - Estimation tuning
281
+ * @returns {number} Estimated runs per month
282
+ */
283
+ function estimateWorkflowRunsPerMonth(workflow, commitsPerDay, options = {}) {
284
+ const triggers = normalizeTriggers(workflow && workflow.on)
285
+ const commitsPerMonth = Math.ceil(commitsPerDay * DAYS_PER_MONTH)
286
+ const pullRequestFactor =
287
+ options.pullRequestFactor ?? DEFAULT_PULL_REQUEST_FACTOR
288
+ const manualRunsPerMonth =
289
+ options.manualRunsPerMonth ?? DEFAULT_MANUAL_RUNS_PER_MONTH
290
+ const releaseRunsPerMonth =
291
+ options.releaseRunsPerMonth ?? DEFAULT_RELEASE_RUNS_PER_MONTH
292
+
293
+ const hasPush = Object.prototype.hasOwnProperty.call(triggers, 'push')
294
+ const hasPullRequest = Object.prototype.hasOwnProperty.call(
295
+ triggers,
296
+ 'pull_request'
297
+ )
298
+ const hasSchedule = Object.prototype.hasOwnProperty.call(triggers, 'schedule')
299
+ const hasWorkflowDispatch = Object.prototype.hasOwnProperty.call(
300
+ triggers,
301
+ 'workflow_dispatch'
302
+ )
303
+ const hasRelease = Object.prototype.hasOwnProperty.call(triggers, 'release')
304
+ const hasCreate = Object.prototype.hasOwnProperty.call(triggers, 'create')
305
+
306
+ const pushConfig =
307
+ hasPush && typeof triggers.push === 'object' ? triggers.push : null
308
+ const pushIsTagOnly =
309
+ !!pushConfig &&
310
+ Array.isArray(pushConfig.tags) &&
311
+ pushConfig.tags.length > 0 &&
312
+ !pushConfig.branches &&
313
+ !pushConfig['branches-ignore']
314
+ const hasCommitPush = hasPush && !pushIsTagOnly
315
+ const hasTagPush = hasPush && pushIsTagOnly
316
+
317
+ let runsPerMonth = 0
318
+
319
+ if (hasCommitPush) {
320
+ runsPerMonth += commitsPerMonth
321
+ }
322
+ if (hasPullRequest) {
323
+ runsPerMonth += Math.ceil(commitsPerMonth * pullRequestFactor)
324
+ }
325
+ if (hasSchedule) {
326
+ runsPerMonth += estimateScheduleRunsPerMonth(triggers.schedule)
327
+ }
328
+ if (hasTagPush || hasRelease || hasCreate) {
329
+ runsPerMonth += releaseRunsPerMonth
330
+ }
331
+
332
+ const hasOnlyManualTrigger =
333
+ hasWorkflowDispatch &&
334
+ !hasCommitPush &&
335
+ !hasPullRequest &&
336
+ !hasSchedule &&
337
+ !hasTagPush &&
338
+ !hasRelease &&
339
+ !hasCreate
340
+ if (hasOnlyManualTrigger) {
341
+ runsPerMonth += manualRunsPerMonth
342
+ }
343
+
344
+ // Fallback for unusual trigger configurations.
345
+ if (runsPerMonth === 0) {
346
+ runsPerMonth = commitsPerMonth
347
+ }
348
+
349
+ return Math.max(1, Math.ceil(runsPerMonth))
350
+ }
351
+
154
352
  /**
155
353
  * Calculate monthly CI costs based on workflow usage
156
354
  * @param {Array} workflows - Array of workflow analysis results
157
355
  * @param {number} commitsPerDay - Average commits per day
356
+ * @param {object} [options={}] - Estimation tuning options
158
357
  * @returns {object} Cost breakdown and recommendations
159
358
  */
160
- function calculateMonthlyCosts(workflows, commitsPerDay) {
161
- const minutesPerWorkflow = workflows.reduce(
162
- (total, wf) => total + wf.estimatedDuration,
359
+ function calculateMonthlyCosts(workflows, commitsPerDay, options = {}) {
360
+ const enrichedWorkflows = workflows.map(wf => {
361
+ const runsPerMonth = estimateWorkflowRunsPerMonth(
362
+ wf.parsed || wf,
363
+ commitsPerDay,
364
+ options
365
+ )
366
+ const minutesPerMonth = Math.ceil(wf.estimatedDuration * runsPerMonth)
367
+ return {
368
+ ...wf,
369
+ runsPerMonth,
370
+ minutesPerMonth,
371
+ }
372
+ })
373
+
374
+ const totalWorkflowRunsPerMonth = enrichedWorkflows.reduce(
375
+ (total, wf) => total + wf.runsPerMonth,
376
+ 0
377
+ )
378
+ const minutesPerMonth = enrichedWorkflows.reduce(
379
+ (total, wf) => total + wf.minutesPerMonth,
163
380
  0
164
381
  )
165
- const workflowRunsPerDay = commitsPerDay * workflows.length
166
- const minutesPerDay = minutesPerWorkflow * commitsPerDay
167
- const minutesPerMonth = Math.ceil(minutesPerDay * 30)
382
+ const minutesPerDay = minutesPerMonth / DAYS_PER_MONTH
383
+ const workflowRunsPerDay = totalWorkflowRunsPerMonth / DAYS_PER_MONTH
168
384
 
169
385
  // GitHub Actions pricing (as of 2024)
170
386
  const FREE_TIER_MINUTES = 2000 // Free tier monthly limit
171
387
  const TEAM_TIER_MINUTES = 3000 // Team tier monthly limit
172
388
  const COST_PER_MINUTE = 0.008 // $0.008/min for private repos
389
+ const TARGET_BUDGET_MINUTES = 1000
390
+ const STRETCH_BUDGET_MINUTES = 1500
173
391
 
174
392
  const freeOverage = Math.max(0, minutesPerMonth - FREE_TIER_MINUTES)
175
393
  const teamOverage = Math.max(0, minutesPerMonth - TEAM_TIER_MINUTES)
@@ -181,12 +399,18 @@ function calculateMonthlyCosts(workflows, commitsPerDay) {
181
399
  minutesPerMonth,
182
400
  minutesPerDay,
183
401
  workflowRunsPerDay,
184
- breakdown: workflows.map(wf => ({
402
+ breakdown: enrichedWorkflows.map(wf => ({
185
403
  name: wf.name,
186
404
  minutesPerRun: wf.estimatedDuration,
187
- runsPerMonth: Math.ceil(commitsPerDay * 30),
188
- minutesPerMonth: Math.ceil(wf.estimatedDuration * commitsPerDay * 30),
405
+ runsPerMonth: wf.runsPerMonth,
406
+ minutesPerMonth: wf.minutesPerMonth,
189
407
  })),
408
+ budgets: {
409
+ target: TARGET_BUDGET_MINUTES,
410
+ stretch: STRETCH_BUDGET_MINUTES,
411
+ withinTarget: minutesPerMonth <= TARGET_BUDGET_MINUTES,
412
+ withinStretch: minutesPerMonth <= STRETCH_BUDGET_MINUTES,
413
+ },
190
414
  tiers: {
191
415
  free: {
192
416
  limit: FREE_TIER_MINUTES,
@@ -217,6 +441,7 @@ function analyzeOptimizations(workflows, commitsPerDay) {
217
441
  for (const wf of workflows) {
218
442
  const workflow = wf.parsed
219
443
  const workflowName = wf.name
444
+ const runsPerMonth = estimateWorkflowRunsPerMonth(workflow, commitsPerDay)
220
445
 
221
446
  if (!workflow.jobs) continue
222
447
 
@@ -243,7 +468,7 @@ function analyzeOptimizations(workflows, commitsPerDay) {
243
468
  if (hasInstall && !hasCaching) {
244
469
  // Estimate 2-5 min savings per run
245
470
  const savingsPerRun = 3
246
- const savingsPerMonth = Math.ceil(savingsPerRun * commitsPerDay * 30)
471
+ const savingsPerMonth = Math.ceil(savingsPerRun * runsPerMonth)
247
472
 
248
473
  recommendations.push({
249
474
  type: 'caching',
@@ -267,7 +492,7 @@ function analyzeOptimizations(workflows, commitsPerDay) {
267
492
  const currentMinutes = wf.estimatedDuration
268
493
  const reductionFactor = 0.5
269
494
  const savingsPerMonth = Math.ceil(
270
- currentMinutes * reductionFactor * commitsPerDay * 30
495
+ currentMinutes * reductionFactor * runsPerMonth
271
496
  )
272
497
 
273
498
  recommendations.push({
@@ -287,14 +512,18 @@ function analyzeOptimizations(workflows, commitsPerDay) {
287
512
 
288
513
  // 3. Detect high-frequency scheduled workflows
289
514
  if (workflow.on) {
290
- const triggers = Array.isArray(workflow.on) ? workflow.on : [workflow.on]
291
- const hasSchedule =
292
- triggers.includes('schedule') ||
293
- (typeof workflow.on === 'object' && workflow.on.schedule)
294
-
295
- if (hasSchedule && workflowName.includes('nightly')) {
296
- const currentRuns = 30 // Daily = 30 runs/month
297
- const proposedRuns = 4 // Weekly = 4 runs/month
515
+ const triggers = normalizeTriggers(workflow.on)
516
+ const hasSchedule = Object.prototype.hasOwnProperty.call(
517
+ triggers,
518
+ 'schedule'
519
+ )
520
+ const scheduledRuns = hasSchedule
521
+ ? estimateScheduleRunsPerMonth(triggers.schedule)
522
+ : 0
523
+
524
+ if (scheduledRuns >= 20) {
525
+ const currentRuns = scheduledRuns
526
+ const proposedRuns = 4 // Weekly
298
527
  const savingsPerMonth = Math.ceil(
299
528
  wf.estimatedDuration * (currentRuns - proposedRuns)
300
529
  )
@@ -303,7 +532,7 @@ function analyzeOptimizations(workflows, commitsPerDay) {
303
532
  type: 'frequency',
304
533
  workflow: workflowName,
305
534
  title: 'Reduce schedule frequency',
306
- description: `"${workflowName}" runs nightly (30x/month)`,
535
+ description: `"${workflowName}" runs about ${currentRuns}x/month`,
307
536
  action: 'Change to weekly schedule (4x/month)',
308
537
  potentialSavings: savingsPerMonth,
309
538
  savingsPerRun: 0,
@@ -311,8 +540,8 @@ function analyzeOptimizations(workflows, commitsPerDay) {
311
540
  })
312
541
  }
313
542
 
314
- if (hasSchedule && workflowName.includes('weekly')) {
315
- const currentRuns = 4 // Weekly = 4 runs/month
543
+ if (scheduledRuns >= 4 && scheduledRuns < 20) {
544
+ const currentRuns = scheduledRuns
316
545
  const proposedRuns = 1 // Monthly = 1 run/month
317
546
  const savingsPerMonth = Math.ceil(
318
547
  wf.estimatedDuration * (currentRuns - proposedRuns)
@@ -323,7 +552,7 @@ function analyzeOptimizations(workflows, commitsPerDay) {
323
552
  type: 'frequency',
324
553
  workflow: workflowName,
325
554
  title: 'Reduce schedule frequency',
326
- description: `"${workflowName}" runs weekly (4x/month)`,
555
+ description: `"${workflowName}" runs about ${currentRuns}x/month`,
327
556
  action: 'Change to monthly schedule (1x/month)',
328
557
  potentialSavings: savingsPerMonth,
329
558
  savingsPerRun: 0,
@@ -337,12 +566,15 @@ function analyzeOptimizations(workflows, commitsPerDay) {
337
566
  if (workflow.on && typeof workflow.on === 'object') {
338
567
  const hasPush = workflow.on.push || workflow.on.pull_request
339
568
  const hasPathFilter =
340
- (workflow.on.push && workflow.on.push.paths) ||
341
- (workflow.on.pull_request && workflow.on.pull_request.paths)
569
+ (workflow.on.push &&
570
+ (workflow.on.push.paths || workflow.on.push['paths-ignore'])) ||
571
+ (workflow.on.pull_request &&
572
+ (workflow.on.pull_request.paths ||
573
+ workflow.on.pull_request['paths-ignore']))
342
574
 
343
575
  if (hasPush && !hasPathFilter && !workflowName.includes('release')) {
344
- // Estimate 20% of commits are docs-only
345
- const wastedRuns = commitsPerDay * 0.2 * 30
576
+ // Estimate 20% of runs are docs-only/config-only changes
577
+ const wastedRuns = runsPerMonth * 0.2
346
578
  const savingsPerMonth = Math.ceil(wf.estimatedDuration * wastedRuns)
347
579
 
348
580
  if (savingsPerMonth > 50) {
@@ -401,6 +633,12 @@ function generateReport(analysis) {
401
633
  ` Commit frequency: ~${commitStats.commitsPerDay.toFixed(1)} commits/day`
402
634
  )
403
635
  console.log(` Workflows detected: ${workflows.length}`)
636
+ console.log(
637
+ ` Budget target (<${costs.budgets.target} min): ${costs.budgets.withinTarget ? '✅' : '⚠️'}`
638
+ )
639
+ console.log(
640
+ ` Stretch budget (<${costs.budgets.stretch} min): ${costs.budgets.withinStretch ? '✅' : '⚠️'}`
641
+ )
404
642
  console.log('')
405
643
 
406
644
  // Workflow breakdown
@@ -613,6 +851,8 @@ module.exports = {
613
851
  handleAnalyzeCi,
614
852
  discoverWorkflows,
615
853
  estimateWorkflowDuration,
854
+ estimateScheduleRunsPerMonth,
855
+ estimateWorkflowRunsPerMonth,
616
856
  getCommitFrequency,
617
857
  calculateMonthlyCosts,
618
858
  analyzeOptimizations,
@@ -95,7 +95,7 @@ async function handleDependencyMonitoring() {
95
95
  if (!capCheck.allowed) {
96
96
  console.error(`❌ ${capCheck.reason}`)
97
97
  console.error(
98
- ' Upgrade to Pro, Team, or Enterprise for unlimited runs: https://vibebuildlab.com/qa-architect'
98
+ ' Upgrade to Pro, Team, or Enterprise for unlimited runs: https://buildproven.ai/qa-architect'
99
99
  )
100
100
  process.exit(1)
101
101
  }
@@ -126,7 +126,7 @@ async function handleDependencyMonitoring() {
126
126
 
127
127
  const configData = generatePremiumDependabotConfig({
128
128
  projectPath,
129
- schedule: 'weekly',
129
+ schedule: 'monthly',
130
130
  })
131
131
 
132
132
  if (configData) {
@@ -173,7 +173,7 @@ async function handleDependencyMonitoring() {
173
173
 
174
174
  const dependabotConfig = generateBasicDependabotConfig({
175
175
  projectPath,
176
- schedule: 'weekly',
176
+ schedule: 'monthly',
177
177
  })
178
178
 
179
179
  if (dependabotConfig) {
@@ -184,7 +184,7 @@ async function handleDependencyMonitoring() {
184
184
  console.log('\n🎉 Basic dependency monitoring setup complete!')
185
185
  console.log('\n📋 What was added (Free Tier):')
186
186
  console.log(' • Basic Dependabot configuration for npm packages')
187
- console.log(' • Weekly dependency updates on Monday 9am')
187
+ console.log(' • Monthly dependency updates')
188
188
  console.log(' • GitHub Actions dependency monitoring')
189
189
 
190
190
  // Show upgrade message for premium features
@@ -271,7 +271,7 @@ async function handleDependencyMonitoring() {
271
271
  console.log(' 2. Enable "Dependabot alerts"')
272
272
  console.log(' 3. Enable "Dependabot security updates"')
273
273
  console.log(
274
- `\n • Report issue: https://github.com/vibebuildlab/qa-architect/issues/new?title=${errorId}`
274
+ `\n • Report issue: https://github.com/buildproven/qa-architect/issues/new?title=${errorId}`
275
275
  )
276
276
  }
277
277
 
@@ -39,11 +39,11 @@ async function handleLicenseActivation() {
39
39
  console.log('\n❌ License activation failed.')
40
40
  console.log('• Check your license key format (QAA-XXXX-XXXX-XXXX-XXXX)')
41
41
  console.log('• Verify your email address')
42
- console.log('• Contact support: support@vibebuildlab.com')
42
+ console.log('• Contact support: support@buildproven.ai')
43
43
  }
44
44
  } catch (error) {
45
45
  console.error('\n❌ License activation error:', error.message)
46
- console.log('Contact support for assistance: support@vibebuildlab.com')
46
+ console.log('Contact support for assistance: support@buildproven.ai')
47
47
  }
48
48
 
49
49
  process.exit(0)