bmad-plus 0.19.0 → 0.20.0
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.
- package/CHANGELOG.md +14 -0
- package/README.md +14 -14
- package/package.json +1 -1
- package/readme-international/README.de.md +14 -14
- package/readme-international/README.es.md +14 -14
- package/readme-international/README.fr.md +14 -14
- package/src/bmad-plus/agents/agent-quality/SKILL.md +1 -1
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +18 -2
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-results.schema.json +1 -0
- package/tools/cli/bmad-plus-cli.js +16 -4
- package/tools/cli/commands/review.js +129 -20
- package/tools/cli/commands/uat.js +16 -3
- package/tools/cli/lib/glob.js +73 -0
- package/tools/cli/lib/packs.js +1 -1
- package/tools/cli/lib/redact.js +116 -0
- package/tools/cli/lib/review-rules.js +156 -0
- package/tools/cli/lib/review.js +138 -26
- package/tools/cli/lib/uat.js +16 -0
- package/tools/cli/review-rules/ci-workflows.md +7 -0
- package/tools/cli/review-rules/configuration.md +5 -0
- package/tools/cli/review-rules/containers.md +6 -0
- package/tools/cli/review-rules/general.md +10 -0
- package/tools/cli/review-rules/index.yaml +36 -0
- package/tools/cli/review-rules/javascript-typescript.md +7 -0
- package/tools/cli/review-rules/python.md +6 -0
- package/tools/cli/review-rules/shell.md +6 -0
- package/tools/cli/review-rules/sql-and-migrations.md +6 -0
package/tools/cli/lib/review.js
CHANGED
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
* derives the verdict from coverage — never from how many findings were written.
|
|
4
4
|
*
|
|
5
5
|
* The host agent reviews; this module only establishes what must be reviewed, where each
|
|
6
|
-
* finding really is, and whether every selected file was accounted
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* disposition. No model call, no network, no write outside the review folder.
|
|
6
|
+
* finding really is, which checklists apply, and whether every selected file was accounted
|
|
7
|
+
* for. Written for BMAD+ after studying open-code-review (docs/research/open-code-review-2026-09-25)
|
|
8
|
+
* for ideas only. No model call, no network, no write outside the review folder.
|
|
10
9
|
*/
|
|
11
10
|
'use strict';
|
|
12
11
|
|
|
@@ -14,6 +13,9 @@ const fs = require('node:fs');
|
|
|
14
13
|
const path = require('node:path');
|
|
15
14
|
const crypto = require('node:crypto');
|
|
16
15
|
const { spawnSync } = require('node:child_process');
|
|
16
|
+
const { matchesAny } = require('./glob');
|
|
17
|
+
const { redactFields } = require('./redact');
|
|
18
|
+
const rules = require('./review-rules');
|
|
17
19
|
|
|
18
20
|
const SCOPE_SCHEMA = 'bmad-plus/review-scope/1';
|
|
19
21
|
const FINDINGS_SCHEMA = 'bmad-plus/review-findings/1';
|
|
@@ -72,24 +74,30 @@ const GENERATED_PATTERNS = [
|
|
|
72
74
|
];
|
|
73
75
|
const UNIT_LIMITS = { files: 8, lines: 400 };
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
/**
|
|
78
|
+
* How much reviewing a change deserves. A pass is a complete read of the scope; later
|
|
79
|
+
* passes look for what the earlier ones missed and stop early when a pass adds nothing.
|
|
80
|
+
*/
|
|
81
|
+
const EFFORTS = {
|
|
82
|
+
low: { passes: 1, refute: false },
|
|
83
|
+
medium: { passes: 2, refute: true },
|
|
84
|
+
high: { passes: 3, refute: true },
|
|
85
|
+
};
|
|
86
|
+
const CHURN = { plan: 150, split: 400 };
|
|
76
87
|
|
|
77
|
-
/**
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
else if (c === '?') source += '[^/]';
|
|
88
|
-
else source += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
89
|
-
}
|
|
90
|
-
return new RegExp(`^${source}$`);
|
|
88
|
+
/** The reviewing plan follows the effort chosen and the measured size of the change. */
|
|
89
|
+
function reviewPlan(effort, lines, units) {
|
|
90
|
+
const { passes, refute } = EFFORTS[effort];
|
|
91
|
+
return {
|
|
92
|
+
effort,
|
|
93
|
+
passes,
|
|
94
|
+
refute,
|
|
95
|
+
planFirst: lines >= CHURN.plan,
|
|
96
|
+
parallelUnits: units > 1 && lines >= CHURN.split,
|
|
97
|
+
};
|
|
91
98
|
}
|
|
92
|
-
|
|
99
|
+
|
|
100
|
+
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
|
93
101
|
|
|
94
102
|
function git(projectDir, args) {
|
|
95
103
|
const result = spawnSync('git', args, {
|
|
@@ -251,6 +259,10 @@ function buildScope(projectDir, options = {}) {
|
|
|
251
259
|
...(entry.oldPath ? { oldPath: entry.oldPath } : {}),
|
|
252
260
|
});
|
|
253
261
|
}
|
|
262
|
+
const effort = options.effort || 'medium';
|
|
263
|
+
if (!EFFORTS[effort]) throw new Error(`effort must be one of ${Object.keys(EFFORTS).join('|')}`);
|
|
264
|
+
const ruleset = options.ruleset || rules.loadRuleset(projectDir);
|
|
265
|
+
for (const item of selected) item.rules = rules.rulesFor(ruleset, item.path);
|
|
254
266
|
const identity = {
|
|
255
267
|
base,
|
|
256
268
|
head: head || 'WORKTREE',
|
|
@@ -258,10 +270,16 @@ function buildScope(projectDir, options = {}) {
|
|
|
258
270
|
workspace: Boolean(options.workspace),
|
|
259
271
|
include,
|
|
260
272
|
exclude,
|
|
273
|
+
effort,
|
|
274
|
+
rulesSha256: ruleset.sha256,
|
|
261
275
|
};
|
|
262
|
-
const units = planUnits(selected, options.unitLimits)
|
|
276
|
+
const units = planUnits(selected, options.unitLimits).map((unit) => ({
|
|
277
|
+
...unit,
|
|
278
|
+
rules: [...new Set(unit.paths.flatMap((p) => selected.find((s) => s.path === p).rules))],
|
|
279
|
+
}));
|
|
263
280
|
const lines = selected.reduce((n, item) => n + item.added + item.removed, 0);
|
|
264
|
-
const
|
|
281
|
+
const plan = reviewPlan(effort, lines, units.length);
|
|
282
|
+
const body = { identity, selected, excluded, units, plan };
|
|
265
283
|
return {
|
|
266
284
|
schema: SCOPE_SCHEMA,
|
|
267
285
|
id: options.id,
|
|
@@ -292,6 +310,7 @@ const FINDING_KEYS = [
|
|
|
292
310
|
'evidence',
|
|
293
311
|
'refutation',
|
|
294
312
|
'fix',
|
|
313
|
+
'rule',
|
|
295
314
|
];
|
|
296
315
|
|
|
297
316
|
/** Unknown values are errors, never coerced: a wrong enum is a wrong finding. */
|
|
@@ -332,8 +351,12 @@ function validateFindings(doc, scope) {
|
|
|
332
351
|
errors.push(`${at}: a refuted finding keeps its refutation — the quoted ground`);
|
|
333
352
|
if (finding.disposition === 'confirmed' && !String(finding.evidence || '').trim())
|
|
334
353
|
errors.push(`${at}: a confirmed finding needs evidence`);
|
|
335
|
-
|
|
354
|
+
const entry = scope && scope.selected.find((item) => item.path === finding.path);
|
|
355
|
+
if (scope && finding.path && !entry)
|
|
336
356
|
errors.push(`${at}: ${finding.path} is not in the review scope`);
|
|
357
|
+
// A finding may name the checklist rule that led to it; the rule must apply to that file.
|
|
358
|
+
if (finding.rule !== undefined && entry && !(entry.rules || []).includes(finding.rule))
|
|
359
|
+
errors.push(`${at}: rule "${finding.rule}" does not apply to ${finding.path}`);
|
|
337
360
|
}
|
|
338
361
|
return errors;
|
|
339
362
|
}
|
|
@@ -396,11 +419,27 @@ function anchorFindings(projectDir, scope, doc) {
|
|
|
396
419
|
location: { status: 'unlocated', reason: 'the quoted code is not in the file' },
|
|
397
420
|
};
|
|
398
421
|
});
|
|
399
|
-
const counts = { located: 0, ambiguous: 0, unlocated: 0 };
|
|
400
|
-
for (const finding of anchored)
|
|
422
|
+
const counts = { located: 0, ambiguous: 0, unlocated: 0, redactions: 0 };
|
|
423
|
+
for (const finding of anchored) {
|
|
424
|
+
counts[finding.location.status] += 1;
|
|
425
|
+
// Located first, redacted second: the anchor needs the quote, the written record must not carry a secret.
|
|
426
|
+
const redacted = redactFields(finding, REDACTED_FIELDS);
|
|
427
|
+
if (redacted) finding.redactions = redacted;
|
|
428
|
+
counts.redactions += redacted;
|
|
429
|
+
}
|
|
401
430
|
return { findings: anchored, counts };
|
|
402
431
|
}
|
|
403
432
|
|
|
433
|
+
const REDACTED_FIELDS = [
|
|
434
|
+
'existing_code',
|
|
435
|
+
'content',
|
|
436
|
+
'trigger',
|
|
437
|
+
'consequence',
|
|
438
|
+
'evidence',
|
|
439
|
+
'refutation',
|
|
440
|
+
'fix',
|
|
441
|
+
];
|
|
442
|
+
|
|
404
443
|
// ── Coverage and gate ─────────────────────────────────────────────────────────
|
|
405
444
|
|
|
406
445
|
function validateCoverage(doc, scope) {
|
|
@@ -486,6 +525,73 @@ function reviewGate({ scope, findings, coverage, anchored }) {
|
|
|
486
525
|
};
|
|
487
526
|
}
|
|
488
527
|
|
|
528
|
+
// ── Comparing two reviews ─────────────────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
const COMPARE_SCHEMA = 'bmad-plus/review-compare/1';
|
|
531
|
+
|
|
532
|
+
/** A finding's identity survives moved lines and reindentation: path, category, normalised quote. */
|
|
533
|
+
function findingKey(file, finding) {
|
|
534
|
+
const code = String(finding.existing_code || '')
|
|
535
|
+
.split('\n')
|
|
536
|
+
.map(normalizeLine)
|
|
537
|
+
.filter(Boolean)
|
|
538
|
+
.join('\n');
|
|
539
|
+
return JSON.stringify([file, finding.category, code]);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* What became of the open findings of an earlier review. `resolved` needs the later review
|
|
544
|
+
* to have completed that file; otherwise the finding is `not_reviewed`, never assumed fixed.
|
|
545
|
+
* Renames recorded in the later scope carry findings to the new path.
|
|
546
|
+
*/
|
|
547
|
+
function compareReviews(before, after) {
|
|
548
|
+
const renamed = new Map(
|
|
549
|
+
after.scope.selected.filter((item) => item.oldPath).map((item) => [item.oldPath, item.path])
|
|
550
|
+
);
|
|
551
|
+
const completed = new Set(
|
|
552
|
+
((after.coverage && after.coverage.items) || [])
|
|
553
|
+
.filter((item) => item.outcome === 'completed')
|
|
554
|
+
.map((item) => item.path)
|
|
555
|
+
);
|
|
556
|
+
const isOpen = (finding) => finding.disposition !== 'refuted';
|
|
557
|
+
const afterByKey = new Map(after.findings.findings.map((f) => [findingKey(f.path, f), f]));
|
|
558
|
+
const beforeKeys = new Set();
|
|
559
|
+
const buckets = { new: [], persisting: [], resolved: [], refuted: [], not_reviewed: [] };
|
|
560
|
+
|
|
561
|
+
for (const finding of before.findings.findings.filter(isOpen)) {
|
|
562
|
+
const file = renamed.get(finding.path) || finding.path;
|
|
563
|
+
const key = findingKey(file, finding);
|
|
564
|
+
beforeKeys.add(key);
|
|
565
|
+
const match = afterByKey.get(key);
|
|
566
|
+
const entry = {
|
|
567
|
+
before: finding.id,
|
|
568
|
+
path: file,
|
|
569
|
+
category: finding.category,
|
|
570
|
+
severity: finding.severity,
|
|
571
|
+
};
|
|
572
|
+
if (match && isOpen(match)) buckets.persisting.push({ ...entry, after: match.id });
|
|
573
|
+
else if (match) buckets.refuted.push({ ...entry, after: match.id });
|
|
574
|
+
else if (completed.has(file)) buckets.resolved.push(entry);
|
|
575
|
+
else buckets.not_reviewed.push(entry);
|
|
576
|
+
}
|
|
577
|
+
for (const finding of after.findings.findings.filter(isOpen)) {
|
|
578
|
+
if (!beforeKeys.has(findingKey(finding.path, finding)))
|
|
579
|
+
buckets.new.push({
|
|
580
|
+
after: finding.id,
|
|
581
|
+
path: finding.path,
|
|
582
|
+
category: finding.category,
|
|
583
|
+
severity: finding.severity,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
schema: COMPARE_SCHEMA,
|
|
588
|
+
before: { id: before.scope.id, scopeSha256: before.scope.sha256 },
|
|
589
|
+
after: { id: after.scope.id, scopeSha256: after.scope.sha256 },
|
|
590
|
+
counts: Object.fromEntries(Object.entries(buckets).map(([name, list]) => [name, list.length])),
|
|
591
|
+
buckets,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
489
595
|
function layout(projectDir, dir = DEFAULT_DIR, id) {
|
|
490
596
|
const root = path.resolve(projectDir, dir, id);
|
|
491
597
|
return {
|
|
@@ -494,6 +600,8 @@ function layout(projectDir, dir = DEFAULT_DIR, id) {
|
|
|
494
600
|
findings: path.join(root, 'findings.json'),
|
|
495
601
|
anchored: path.join(root, 'findings.anchored.json'),
|
|
496
602
|
coverage: path.join(root, 'coverage.json'),
|
|
603
|
+
checklist: path.join(root, 'checklist.md'),
|
|
604
|
+
compare: path.join(root, 'compare.json'),
|
|
497
605
|
};
|
|
498
606
|
}
|
|
499
607
|
|
|
@@ -501,7 +609,9 @@ module.exports = {
|
|
|
501
609
|
SCOPE_SCHEMA,
|
|
502
610
|
FINDINGS_SCHEMA,
|
|
503
611
|
COVERAGE_SCHEMA,
|
|
612
|
+
COMPARE_SCHEMA,
|
|
504
613
|
DEFAULT_DIR,
|
|
614
|
+
EFFORTS,
|
|
505
615
|
SEVERITIES,
|
|
506
616
|
CATEGORIES,
|
|
507
617
|
DISPOSITIONS,
|
|
@@ -509,7 +619,6 @@ module.exports = {
|
|
|
509
619
|
OUTCOMES,
|
|
510
620
|
SECRET_PATTERNS,
|
|
511
621
|
GENERATED_PATTERNS,
|
|
512
|
-
globToRegExp,
|
|
513
622
|
parseNumstat,
|
|
514
623
|
planUnits,
|
|
515
624
|
buildScope,
|
|
@@ -518,5 +627,8 @@ module.exports = {
|
|
|
518
627
|
anchorFindings,
|
|
519
628
|
validateCoverage,
|
|
520
629
|
reviewGate,
|
|
630
|
+
reviewPlan,
|
|
631
|
+
findingKey,
|
|
632
|
+
compareReviews,
|
|
521
633
|
layout,
|
|
522
634
|
};
|
package/tools/cli/lib/uat.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const crypto = require('node:crypto');
|
|
7
|
+
const { redactFields } = require('./redact');
|
|
7
8
|
|
|
8
9
|
const SPEC_SCHEMA = 'bmad-plus/uat-spec/2';
|
|
9
10
|
const RESULTS_SCHEMA = 'bmad-plus/uat-results/2';
|
|
@@ -714,6 +715,20 @@ function normalizeRun(doc, spec) {
|
|
|
714
715
|
return run;
|
|
715
716
|
}
|
|
716
717
|
|
|
718
|
+
/**
|
|
719
|
+
* Tester notes are free text, and testers paste terminal output into them: credentials go
|
|
720
|
+
* before the run is written. Returns the number of replacements, recorded on the run.
|
|
721
|
+
*/
|
|
722
|
+
function redactRun(run) {
|
|
723
|
+
let count = redactFields(run, ['overallNote']);
|
|
724
|
+
for (const step of Object.values(run.steps || {})) {
|
|
725
|
+
count += redactFields(step, ['note']);
|
|
726
|
+
for (const expect of Object.values(step.expect || {})) count += redactFields(expect, ['note']);
|
|
727
|
+
}
|
|
728
|
+
if (count) run.redactions = (run.redactions || 0) + count;
|
|
729
|
+
return count;
|
|
730
|
+
}
|
|
731
|
+
|
|
717
732
|
function readRuns(dir, spec) {
|
|
718
733
|
const runs = [];
|
|
719
734
|
const seen = new Set();
|
|
@@ -1013,6 +1028,7 @@ module.exports = {
|
|
|
1013
1028
|
pageGuarantees,
|
|
1014
1029
|
languageCode,
|
|
1015
1030
|
normalizeRun,
|
|
1031
|
+
redactRun,
|
|
1016
1032
|
readRuns,
|
|
1017
1033
|
stepState,
|
|
1018
1034
|
failures,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
- **Untrusted triggers.** `pull_request_target`, `workflow_run` or `issue_comment` jobs that check out or execute code from a pull request with secrets or a write token available.
|
|
2
|
+
- **Script injection.** Expressions such as `${{ github.event.* }}`, branch names, titles or commit messages interpolated directly into `run:`; pass them through `env:` and quote them.
|
|
3
|
+
- **Permissions.** No top-level `permissions:` (defaults may be broad), or `write` scopes a job does not need; `id-token: write` on jobs that do not publish.
|
|
4
|
+
- **Pinning.** Third-party actions referenced by a moving tag or branch instead of a full commit SHA; container images without a digest.
|
|
5
|
+
- **Secrets.** Secrets echoed, written to artifacts or caches, or exposed to steps that run third-party code; `ACTIONS_STEP_DEBUG` left on.
|
|
6
|
+
- **Runners.** Self-hosted runners on public repositories; a job that assumes a tool preinstalled on GitHub-hosted images (`node`, `python`, `jq`) before setting it up.
|
|
7
|
+
- **Gates.** A step that should block marked `continue-on-error`; a required check that can be skipped by a path filter; a release job that does not depend on the test jobs.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
- **Secrets.** A real credential, token, private URL or personal data committed in a configuration file or an example file; an example file whose placeholder looks real.
|
|
2
|
+
- **Dependencies.** A new dependency: is it maintained, necessary, and from the expected publisher (typosquatting)? A version range widened to accept a major; a lockfile out of step with its manifest; install scripts from a new package.
|
|
3
|
+
- **Defaults.** A security-relevant setting weakened (TLS verification, CORS origins, cookie flags, debug mode, log level exposing data) or a default that now differs between environments.
|
|
4
|
+
- **Consistency.** A key renamed in one environment file and not the others; a value duplicated where one source of truth exists; a feature flag left on.
|
|
5
|
+
- **Parsing.** YAML values that change type silently (`no`, `on`, `0755`, unquoted dates and versions); JSON with duplicate keys.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
- **Privilege.** Containers running as root without need; `privileged: true`, added capabilities, host network or PID namespace; the Docker socket mounted into a container (root on the host).
|
|
2
|
+
- **Images.** Base images on `latest` or an unpinned tag; packages installed without cleanup or version pins where reproducibility matters; secrets passed as build arguments or copied into a layer.
|
|
3
|
+
- **Exposure.** Ports published on `0.0.0.0` for services that should stay internal (databases, caches, admin APIs); missing network separation between stacks.
|
|
4
|
+
- **Health and restart.** No health check for a service others depend on; `depends_on` without a health condition; restart policies that loop a crashing container.
|
|
5
|
+
- **Data.** Volumes for stateful services missing or anonymous; host paths mounted read-write where read-only suffices.
|
|
6
|
+
- **Resources.** No memory or CPU limits on a shared host; Kubernetes pods without requests/limits, probes or a `securityContext` (`runAsNonRoot`, `readOnlyRootFilesystem`).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Look for what the diff changes in behaviour, not how it reads.
|
|
2
|
+
|
|
3
|
+
- **Callers.** Search every caller of a changed function, export, route, command or schema. A signature, return shape or error type that changed is a defect at the caller that was not updated.
|
|
4
|
+
- **Removed behaviour.** Read what the diff deletes: a check, a branch, a cleanup, a log that an operator relies on. Deleted protection is the most often missed defect.
|
|
5
|
+
- **Boundaries.** Empty input, one element, the last element, the maximum size, a missing optional field, a concurrent second call, a retry after a partial failure.
|
|
6
|
+
- **Errors.** An error swallowed, logged and ignored, or turned into a success value. A failure path that leaves state half-written.
|
|
7
|
+
- **Trust.** Data that crosses a boundary (request, file, environment, another service, a model's output) and reaches a query, a command, a path, HTML or a permission decision without validation.
|
|
8
|
+
- **Tests.** A changed behaviour whose test still passes because it mocks the changed path, asserts nothing about the new case, or is skipped.
|
|
9
|
+
|
|
10
|
+
Do not report formatting, naming or style a linter enforces, and do not propose rewrites the change did not need.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Built-in review checklists, applied by path. Every matching rule applies to a file.
|
|
2
|
+
# A project adds, replaces (same id) or disables rules in _bmad/review-rules.yaml.
|
|
3
|
+
schema: bmad-plus/review-rules/1
|
|
4
|
+
rules:
|
|
5
|
+
- id: general
|
|
6
|
+
title: Every change
|
|
7
|
+
globs: ['**']
|
|
8
|
+
doc: general.md
|
|
9
|
+
- id: javascript-typescript
|
|
10
|
+
title: JavaScript and TypeScript
|
|
11
|
+
globs: ['**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}', '**/*.{vue,svelte}']
|
|
12
|
+
doc: javascript-typescript.md
|
|
13
|
+
- id: python
|
|
14
|
+
title: Python
|
|
15
|
+
globs: ['**/*.{py,pyi}']
|
|
16
|
+
doc: python.md
|
|
17
|
+
- id: sql-and-migrations
|
|
18
|
+
title: SQL, queries and migrations
|
|
19
|
+
globs: ['**/*.sql', '**/{migrations,migrate,db/migrate}/**', '**/*.prisma', '**/schema.rb']
|
|
20
|
+
doc: sql-and-migrations.md
|
|
21
|
+
- id: shell
|
|
22
|
+
title: Shell and PowerShell scripts
|
|
23
|
+
globs: ['**/*.{sh,bash,zsh,ps1,psm1}', '**/{Makefile,makefile}']
|
|
24
|
+
doc: shell.md
|
|
25
|
+
- id: ci-workflows
|
|
26
|
+
title: CI workflows
|
|
27
|
+
globs: ['.github/workflows/**/*.{yml,yaml}', '**/.gitlab-ci.yml', '**/{bitbucket-pipelines.yml,azure-pipelines.yml}']
|
|
28
|
+
doc: ci-workflows.md
|
|
29
|
+
- id: containers
|
|
30
|
+
title: Containers and deployment manifests
|
|
31
|
+
globs: ['**/{Dockerfile,Containerfile}', '**/*.dockerfile', '**/{docker-,}compose*.{yml,yaml}', '**/{k8s,kubernetes,helm,charts}/**/*.{yml,yaml}']
|
|
32
|
+
doc: containers.md
|
|
33
|
+
- id: configuration
|
|
34
|
+
title: Configuration and dependency manifests
|
|
35
|
+
globs: ['**/*.{json,yml,yaml,toml,ini,properties}', '**/.env.example', '**/{package.json,pyproject.toml,requirements*.txt,go.mod,Cargo.toml,composer.json}']
|
|
36
|
+
doc: configuration.md
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
- **Async.** A promise that is created but not awaited or returned; `forEach` with an async callback; an `await` inside a loop that should run in parallel, or `Promise.all` where one failure must not cancel the others (`allSettled`). Unhandled rejections in event handlers and timers.
|
|
2
|
+
- **Nullish values.** Property access on a value that can be `undefined` after an `await`, a `find`, a map lookup or an optional API field; `||` used where `0`, `''` or `false` are valid values and `??` was meant.
|
|
3
|
+
- **Equality and numbers.** Loose equality across types; floating-point money; `parseInt` without a radix or on user input without a `NaN` check; array index arithmetic off by one.
|
|
4
|
+
- **Mutation.** A function that mutates its argument or a shared default object; sorting or splicing an array the caller still uses; state mutated in a React render or a reducer.
|
|
5
|
+
- **Injection.** `innerHTML`, `dangerouslySetInnerHTML`, `v-html`, template literals building SQL, shell commands through `exec` or `shell: true`, `eval`/`new Function`, a user-controlled path reaching `fs` without confinement, a user-controlled URL fetched by the server (SSRF), `Object.assign`/spread of untrusted keys (`__proto__`).
|
|
6
|
+
- **Types that lie.** `as` casts, non-null assertions (`!`) and `any` that hide a real `undefined` or a wrong shape at runtime.
|
|
7
|
+
- **Resources.** Listeners, intervals, streams, file handles or database connections opened without a matching close on every path, including errors.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
- **Defaults and scope.** Mutable default arguments; a loop variable captured late by a closure; a module-level object mutated per request.
|
|
2
|
+
- **Exceptions.** Bare `except:` or `except Exception` that swallows and continues; `raise` without `from` that loses the cause; cleanup that is not in `finally` or a context manager.
|
|
3
|
+
- **Resources.** Files, sockets, database cursors and locks not managed by `with`; a subprocess whose output is never drained.
|
|
4
|
+
- **Injection.** SQL built with f-strings or `%`; `subprocess` with `shell=True` or a string command built from input; `pickle`, `yaml.load` without a safe loader, `eval`/`exec` on untrusted data; `os.path.join` with an absolute user path; `tarfile`/`zipfile` extraction without member path checks.
|
|
5
|
+
- **Types and values.** `None` returned on one path and used unconditionally; integer division where a float was meant; naive and aware datetimes compared; timezones assumed.
|
|
6
|
+
- **Concurrency.** Shared state touched from threads or async tasks without a lock; a blocking call inside an `async def`.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
- **Failure handling.** Bash without `set -euo pipefail` (or an explicit reason); a failing command in a pipeline hidden by the last stage; PowerShell without `$ErrorActionPreference = 'Stop'` where errors must stop the script; native exit codes (`$LASTEXITCODE`) not checked.
|
|
2
|
+
- **Quoting.** Unquoted variables and command substitutions that split on spaces or expand globs; `eval` or `Invoke-Expression` on data.
|
|
3
|
+
- **Destructive commands.** `rm -rf`, `Remove-Item -Recurse`, `git reset --hard`, `DROP` or deploy commands on a path or target built from a variable that can be empty or wrong.
|
|
4
|
+
- **Secrets.** Tokens passed on the command line (visible in process lists and logs), echoed, or written to world-readable files; `set -x` in a script that handles credentials.
|
|
5
|
+
- **Portability.** Bash-only syntax under `#!/bin/sh`; GNU-only flags on macOS; PowerShell 7 syntax (`&&`, `??`, ternary) in scripts run by Windows PowerShell 5.1; CRLF line endings in a script run by Linux.
|
|
6
|
+
- **Temporary files.** Predictable names in shared directories instead of `mktemp`/`New-TemporaryFile`, and no cleanup trap.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
- **Parameters.** Any value reaching a query must be bound, never concatenated; identifiers (table, column, sort order) chosen from an allow-list.
|
|
2
|
+
- **Scope of writes.** An `UPDATE` or `DELETE` whose `WHERE` can match more rows than intended, or none at all silently; a missing tenant or owner filter.
|
|
3
|
+
- **Migrations.** A migration that locks a large table, drops or renames a column still read by the running version, adds a `NOT NULL` column without a default or backfill, or cannot be rolled back. Data migrations must be idempotent and safe to rerun.
|
|
4
|
+
- **Transactions.** Several writes that must succeed together outside a transaction; a transaction held open across a network call.
|
|
5
|
+
- **Performance.** A new filter or join on a column without an index; a query inside a loop (N+1); an unbounded result set returned to a caller.
|
|
6
|
+
- **Nulls.** Comparisons with `NULL` using `=`; aggregates or `NOT IN` over nullable columns.
|