pyossmtool 0.0.1__py3-none-any.whl
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.
- pyossmtool/__init__.py +3 -0
- pyossmtool/bundle/catalog/checks/bandit.scan.yaml +13 -0
- pyossmtool/bundle/catalog/checks/codespell.spelling.yaml +12 -0
- pyossmtool/bundle/catalog/checks/deadcode.scan.yaml +13 -0
- pyossmtool/bundle/catalog/checks/gate.acronym-allowlist.yaml +14 -0
- pyossmtool/bundle/catalog/checks/gate.folder-breadth.yaml +14 -0
- pyossmtool/bundle/catalog/checks/gate.module-private-vars.yaml +14 -0
- pyossmtool/bundle/catalog/checks/gate.module-size.yaml +14 -0
- pyossmtool/bundle/catalog/checks/gitleaks.secrets.yaml +21 -0
- pyossmtool/bundle/catalog/checks/hadolint.dockerfile.yaml +16 -0
- pyossmtool/bundle/catalog/checks/jscpd.duplication.yaml +19 -0
- pyossmtool/bundle/catalog/checks/markdownlint.check.yaml +17 -0
- pyossmtool/bundle/catalog/checks/mdformat.apply.yaml +10 -0
- pyossmtool/bundle/catalog/checks/mdformat.format.yaml +14 -0
- pyossmtool/bundle/catalog/checks/mutmut.run.yaml +13 -0
- pyossmtool/bundle/catalog/checks/pydeps.cycles.yaml +14 -0
- pyossmtool/bundle/catalog/checks/pytest.coverage.yaml +19 -0
- pyossmtool/bundle/catalog/checks/pytest.test.yaml +14 -0
- pyossmtool/bundle/catalog/checks/radon.cc.yaml +13 -0
- pyossmtool/bundle/catalog/checks/radon.mi.yaml +13 -0
- pyossmtool/bundle/catalog/checks/ruff.format.apply.yaml +11 -0
- pyossmtool/bundle/catalog/checks/ruff.format.yaml +14 -0
- pyossmtool/bundle/catalog/checks/ruff.lint.yaml +15 -0
- pyossmtool/bundle/catalog/checks/ruff.unused.yaml +17 -0
- pyossmtool/bundle/catalog/checks/semgrep.scan.yaml +18 -0
- pyossmtool/bundle/catalog/checks/shellcheck.yaml +13 -0
- pyossmtool/bundle/catalog/checks/shfmt.apply.yaml +11 -0
- pyossmtool/bundle/catalog/checks/shfmt.format.yaml +14 -0
- pyossmtool/bundle/catalog/checks/sourcery.review.yaml +14 -0
- pyossmtool/bundle/catalog/checks/ty.check.yaml +15 -0
- pyossmtool/bundle/catalog/checks/vulture.scan.yaml +16 -0
- pyossmtool/bundle/catalog/checks/yamlfmt.apply.yaml +10 -0
- pyossmtool/bundle/catalog/checks/yamlfmt.format.yaml +14 -0
- pyossmtool/bundle/catalog/checks/yamllint.check.yaml +16 -0
- pyossmtool/bundle/catalog/tools/bandit.yaml +20 -0
- pyossmtool/bundle/catalog/tools/codespell.yaml +17 -0
- pyossmtool/bundle/catalog/tools/deadcode.yaml +13 -0
- pyossmtool/bundle/catalog/tools/gitleaks.yaml +9 -0
- pyossmtool/bundle/catalog/tools/hadolint.yaml +11 -0
- pyossmtool/bundle/catalog/tools/jscpd.yaml +15 -0
- pyossmtool/bundle/catalog/tools/markdownlint.yaml +18 -0
- pyossmtool/bundle/catalog/tools/mdformat.yaml +13 -0
- pyossmtool/bundle/catalog/tools/mutmut.yaml +10 -0
- pyossmtool/bundle/catalog/tools/pydeps.yaml +10 -0
- pyossmtool/bundle/catalog/tools/pytest.yaml +12 -0
- pyossmtool/bundle/catalog/tools/radon.yaml +10 -0
- pyossmtool/bundle/catalog/tools/ruff.yaml +26 -0
- pyossmtool/bundle/catalog/tools/script.yaml +8 -0
- pyossmtool/bundle/catalog/tools/semgrep.yaml +21 -0
- pyossmtool/bundle/catalog/tools/shellcheck.yaml +10 -0
- pyossmtool/bundle/catalog/tools/shfmt.yaml +13 -0
- pyossmtool/bundle/catalog/tools/sourcery.yaml +10 -0
- pyossmtool/bundle/catalog/tools/ty.yaml +20 -0
- pyossmtool/bundle/catalog/tools/vulture.yaml +13 -0
- pyossmtool/bundle/catalog/tools/yamlfmt.yaml +24 -0
- pyossmtool/bundle/catalog/tools/yamllint.yaml +24 -0
- pyossmtool/bundle/defaults/allowlists/acronyms.yaml +10 -0
- pyossmtool/bundle/defaults/allowlists/folder-breadth.txt +1 -0
- pyossmtool/bundle/defaults/allowlists/module-private-vars.txt +1 -0
- pyossmtool/bundle/defaults/allowlists/module-size.txt +1 -0
- pyossmtool/bundle/defaults/configs/bandit.yaml +5 -0
- pyossmtool/bundle/defaults/configs/gates/acronym-allowlist.yaml +4 -0
- pyossmtool/bundle/defaults/configs/gates/folder-breadth.yaml +12 -0
- pyossmtool/bundle/defaults/configs/gates/module-private-vars.yaml +4 -0
- pyossmtool/bundle/defaults/configs/gates/module-size.yaml +6 -0
- pyossmtool/bundle/defaults/configs/markdownlint.json +8 -0
- pyossmtool/bundle/defaults/configs/ruff.toml +8 -0
- pyossmtool/bundle/defaults/configs/ty.toml +5 -0
- pyossmtool/bundle/defaults/configs/yamlfmt.yaml +4 -0
- pyossmtool/bundle/defaults/configs/yamllint.yaml +12 -0
- pyossmtool/bundle/defaults/gates/README.md +150 -0
- pyossmtool/bundle/defaults/gates/acronym-allowlist.sh +15 -0
- pyossmtool/bundle/defaults/gates/folder-breadth.sh +22 -0
- pyossmtool/bundle/defaults/gates/lib.sh +116 -0
- pyossmtool/bundle/defaults/gates/module-private-vars.sh +77 -0
- pyossmtool/bundle/defaults/gates/module-size.sh +60 -0
- pyossmtool/bundle/defaults/semgrep/python-quality.yml +19 -0
- pyossmtool/bundle/suites/demo.yaml +11 -0
- pyossmtool/bundle/suites/extended.yaml +34 -0
- pyossmtool/bundle/suites/format.yaml +10 -0
- pyossmtool/bundle/suites/formatting.yaml +10 -0
- pyossmtool/bundle/suites/policy.yaml +11 -0
- pyossmtool/bundle/suites/python-quality.yaml +9 -0
- pyossmtool/bundle/suites/reslab-parity.yaml +41 -0
- pyossmtool/bundle/suites/standard.yaml +45 -0
- pyossmtool/cli.py +233 -0
- pyossmtool/config_resolver.py +117 -0
- pyossmtool/discovery.py +50 -0
- pyossmtool/gate_config.py +119 -0
- pyossmtool/gates.py +102 -0
- pyossmtool/ignore.py +270 -0
- pyossmtool/ignore_materialize.py +184 -0
- pyossmtool/installer.py +119 -0
- pyossmtool/models.py +279 -0
- pyossmtool/parsers/__init__.py +76 -0
- pyossmtool/parsers/analysis.py +329 -0
- pyossmtool/parsers/base.py +29 -0
- pyossmtool/parsers/common.py +57 -0
- pyossmtool/parsers/format_text.py +67 -0
- pyossmtool/parsers/gates.py +92 -0
- pyossmtool/parsers/patterns.py +78 -0
- pyossmtool/parsers/prose.py +193 -0
- pyossmtool/parsers/ruff.py +103 -0
- pyossmtool/parsers/shell.py +83 -0
- pyossmtool/policy/__init__.py +0 -0
- pyossmtool/policy/acronym_allowlist.py +322 -0
- pyossmtool/policy/folder_breadth.py +306 -0
- pyossmtool/registry.py +72 -0
- pyossmtool/reporter.py +87 -0
- pyossmtool/resolver.py +146 -0
- pyossmtool/runner.py +482 -0
- pyossmtool/runner_script.py +131 -0
- pyossmtool/target_expand.py +112 -0
- pyossmtool-0.0.1.dist-info/METADATA +94 -0
- pyossmtool-0.0.1.dist-info/RECORD +119 -0
- pyossmtool-0.0.1.dist-info/WHEEL +5 -0
- pyossmtool-0.0.1.dist-info/entry_points.txt +2 -0
- pyossmtool-0.0.1.dist-info/licenses/LICENSE +21 -0
- pyossmtool-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Module private-vars gate — no leading-underscore module-level names.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
# shellcheck source=/dev/null
|
|
6
|
+
source "$(pyossmtool gates lib-path)"
|
|
7
|
+
|
|
8
|
+
gate_init "module-private-vars"
|
|
9
|
+
|
|
10
|
+
BASE_BRANCH="${GATE_BASE_BRANCH:-main}"
|
|
11
|
+
ALLOWLIST="${GATE_ALLOWLIST_FILE:-}"
|
|
12
|
+
IFS=' ' read -r -a SCAN_ROOTS <<<"${GATE_SCAN_ROOTS:-src/}"
|
|
13
|
+
|
|
14
|
+
ASSIGN_PATTERN='^_[^_][A-Za-z0-9_]*[[:space:]]*[:=]'
|
|
15
|
+
DEF_PATTERN='^(async[[:space:]]+)?def[[:space:]]+_[^_][A-Za-z0-9_]*[[:space:]]*[\(:]'
|
|
16
|
+
CLASS_PATTERN='^class[[:space:]]+_[^_][A-Za-z0-9_]*[[:space:]]*[\(:]'
|
|
17
|
+
|
|
18
|
+
is_allowlisted() {
|
|
19
|
+
local rel="$1"
|
|
20
|
+
[[ -n ${ALLOWLIST} && -f ${ALLOWLIST} ]] || return 1
|
|
21
|
+
grep -v '^[[:space:]]*#' "${ALLOWLIST}" | grep -v '^[[:space:]]*$' | grep -Fxq "${rel}"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
is_strict_file() {
|
|
25
|
+
local rel="$1"
|
|
26
|
+
local strict
|
|
27
|
+
for strict in "${STRICT_FILES[@]-}"; do
|
|
28
|
+
[[ ${rel} == "${strict}" ]] && return 0
|
|
29
|
+
done
|
|
30
|
+
return 1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
report_matches() {
|
|
34
|
+
local rel="$1"
|
|
35
|
+
local label="$2"
|
|
36
|
+
local matches="$3"
|
|
37
|
+
if [[ -n ${matches} ]]; then
|
|
38
|
+
while IFS= read -r line; do
|
|
39
|
+
[[ -n ${line} ]] || continue
|
|
40
|
+
line_no="${line%%:*}"
|
|
41
|
+
gate_fail "private-${label}" "${rel}:${line}" "${rel}" "${line_no}"
|
|
42
|
+
done <<<"${matches}"
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
STRICT_FILES=()
|
|
47
|
+
if git rev-parse --verify "${BASE_BRANCH}" >/dev/null 2>&1; then
|
|
48
|
+
MERGE_BASE="$(git merge-base HEAD "${BASE_BRANCH}")"
|
|
49
|
+
new_file_globs=()
|
|
50
|
+
for scan_root in "${SCAN_ROOTS[@]}"; do
|
|
51
|
+
new_file_globs+=("${scan_root}" "${scan_root}/**")
|
|
52
|
+
done
|
|
53
|
+
mapfile -t STRICT_FILES < <(git diff --name-only --diff-filter=A "${MERGE_BASE}...HEAD" -- "${new_file_globs[@]}" | grep '\.py$' || true)
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
for scan_dir in "${SCAN_ROOTS[@]}"; do
|
|
57
|
+
[[ -d ${scan_dir} ]] || continue
|
|
58
|
+
while IFS= read -r -d '' file; do
|
|
59
|
+
rel="${file#./}"
|
|
60
|
+
if gate_path_ignored "${rel}"; then
|
|
61
|
+
continue
|
|
62
|
+
fi
|
|
63
|
+
if is_allowlisted "${rel}"; then
|
|
64
|
+
continue
|
|
65
|
+
fi
|
|
66
|
+
assign_matches="$(grep -nE "${ASSIGN_PATTERN}" "${file}" || true)"
|
|
67
|
+
report_matches "${rel}" "assignment" "${assign_matches}"
|
|
68
|
+
if is_strict_file "${rel}"; then
|
|
69
|
+
def_matches="$(grep -nE "${DEF_PATTERN}" "${file}" || true)"
|
|
70
|
+
class_matches="$(grep -nE "${CLASS_PATTERN}" "${file}" || true)"
|
|
71
|
+
report_matches "${rel}" "function" "${def_matches}"
|
|
72
|
+
report_matches "${rel}" "class" "${class_matches}"
|
|
73
|
+
fi
|
|
74
|
+
done < <(find "${scan_dir}" -name '*.py' -print0)
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
gate_finish
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Module size gate — portfolio and new-file line caps.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
# shellcheck source=/dev/null
|
|
6
|
+
source "$(pyossmtool gates lib-path)"
|
|
7
|
+
|
|
8
|
+
gate_init "module-size"
|
|
9
|
+
|
|
10
|
+
PORTFOLIO_MAX="${GATE_PORTFOLIO_MAX_LINES:-1000}"
|
|
11
|
+
NEW_FILE_MAX="${GATE_NEW_FILE_MAX_LINES:-500}"
|
|
12
|
+
BASE_BRANCH="${GATE_BASE_BRANCH:-main}"
|
|
13
|
+
ALLOWLIST="${GATE_ALLOWLIST_FILE:-}"
|
|
14
|
+
IFS=' ' read -r -a SCAN_ROOTS <<<"${GATE_SCAN_ROOTS:-src/}"
|
|
15
|
+
|
|
16
|
+
count_non_blank_lines() {
|
|
17
|
+
local file="$1"
|
|
18
|
+
grep -cve '^[[:space:]]*$' "${file}" || true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
is_allowlisted() {
|
|
22
|
+
local rel="$1"
|
|
23
|
+
[[ -n ${ALLOWLIST} && -f ${ALLOWLIST} ]] || return 1
|
|
24
|
+
grep -v '^[[:space:]]*#' "${ALLOWLIST}" | grep -v '^[[:space:]]*$' | grep -Fxq "${rel}"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for scan_root in "${SCAN_ROOTS[@]}"; do
|
|
28
|
+
[[ -d ${scan_root} ]] || continue
|
|
29
|
+
while IFS= read -r -d '' file; do
|
|
30
|
+
rel="${file#./}"
|
|
31
|
+
if gate_path_ignored "${rel}"; then
|
|
32
|
+
continue
|
|
33
|
+
fi
|
|
34
|
+
loc="$(count_non_blank_lines "${file}")"
|
|
35
|
+
if ((loc > PORTFOLIO_MAX)) && ! is_allowlisted "${rel}"; then
|
|
36
|
+
gate_fail "module-size" "${rel} has ${loc} lines (portfolio cap ${PORTFOLIO_MAX})" "${rel}" "1"
|
|
37
|
+
fi
|
|
38
|
+
done < <(find "${scan_root}" -name '*.py' -print0)
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
if git rev-parse --verify "${BASE_BRANCH}" >/dev/null 2>&1; then
|
|
42
|
+
MERGE_BASE="$(git merge-base HEAD "${BASE_BRANCH}")"
|
|
43
|
+
new_file_globs=()
|
|
44
|
+
for scan_root in "${SCAN_ROOTS[@]}"; do
|
|
45
|
+
new_file_globs+=("${scan_root}" "${scan_root}/**")
|
|
46
|
+
done
|
|
47
|
+
mapfile -t new_files < <(git diff --name-only --diff-filter=A "${MERGE_BASE}...HEAD" -- "${new_file_globs[@]}" | grep '\.py$' || true)
|
|
48
|
+
for rel in "${new_files[@]}"; do
|
|
49
|
+
[[ -f ${rel} ]] || continue
|
|
50
|
+
if gate_path_ignored "${rel}"; then
|
|
51
|
+
continue
|
|
52
|
+
fi
|
|
53
|
+
loc="$(count_non_blank_lines "${rel}")"
|
|
54
|
+
if ((loc > NEW_FILE_MAX)) && ! is_allowlisted "${rel}"; then
|
|
55
|
+
gate_fail "module-size" "${rel} has ${loc} lines (new-file cap ${NEW_FILE_MAX})" "${rel}" "1"
|
|
56
|
+
fi
|
|
57
|
+
done
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
gate_finish
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: no-eval
|
|
3
|
+
patterns:
|
|
4
|
+
- pattern: eval(...)
|
|
5
|
+
message: Avoid eval() — use safer alternatives.
|
|
6
|
+
languages: [python]
|
|
7
|
+
severity: ERROR
|
|
8
|
+
- id: no-exec
|
|
9
|
+
patterns:
|
|
10
|
+
- pattern: exec(...)
|
|
11
|
+
message: Avoid exec() — use safer alternatives.
|
|
12
|
+
languages: [python]
|
|
13
|
+
severity: ERROR
|
|
14
|
+
- id: no-hardcoded-temp-password
|
|
15
|
+
patterns:
|
|
16
|
+
- pattern: password = "..."
|
|
17
|
+
message: Possible hardcoded password.
|
|
18
|
+
languages: [python]
|
|
19
|
+
severity: WARNING
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
id: extended
|
|
2
|
+
name: Extended Quality Suite
|
|
3
|
+
description: Standard suite plus slow or optional checks (mutation testing, sourcery
|
|
4
|
+
review).
|
|
5
|
+
env: auto
|
|
6
|
+
target: .
|
|
7
|
+
checks:
|
|
8
|
+
- id: ruff.format
|
|
9
|
+
- id: ruff.lint
|
|
10
|
+
- id: ruff.unused
|
|
11
|
+
- id: ty.check
|
|
12
|
+
- id: bandit.scan
|
|
13
|
+
- id: gitleaks.secrets
|
|
14
|
+
- id: semgrep.scan
|
|
15
|
+
- id: deadcode.scan
|
|
16
|
+
- id: vulture.scan
|
|
17
|
+
- id: pydeps.cycles
|
|
18
|
+
- id: radon.cc
|
|
19
|
+
- id: radon.mi
|
|
20
|
+
- id: jscpd.duplication
|
|
21
|
+
- id: shfmt.format
|
|
22
|
+
- id: shellcheck
|
|
23
|
+
- id: mdformat.format
|
|
24
|
+
- id: markdownlint.check
|
|
25
|
+
- id: yamlfmt.format
|
|
26
|
+
- id: yamllint.check
|
|
27
|
+
- id: codespell.spelling
|
|
28
|
+
- id: pytest.test
|
|
29
|
+
target: tests/
|
|
30
|
+
- id: pytest.coverage
|
|
31
|
+
target: tests/
|
|
32
|
+
- id: hadolint.dockerfile
|
|
33
|
+
- id: mutmut.run
|
|
34
|
+
- id: sourcery.review
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
id: policy
|
|
2
|
+
name: Policy Gates
|
|
3
|
+
description: Structural policy checks (module size, private vars, folder breadth,
|
|
4
|
+
acronyms).
|
|
5
|
+
env: auto
|
|
6
|
+
target: .
|
|
7
|
+
checks:
|
|
8
|
+
- id: gate.module-size
|
|
9
|
+
- id: gate.module-private-vars
|
|
10
|
+
- id: gate.folder-breadth
|
|
11
|
+
- id: gate.acronym-allowlist
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
id: reslab-parity
|
|
2
|
+
name: ResLab Parity Reference
|
|
3
|
+
description: "Alias for the standard suite \u2014 full reslab-equivalent CLI tool\
|
|
4
|
+
\ coverage."
|
|
5
|
+
env: auto
|
|
6
|
+
target: .
|
|
7
|
+
ignore-profile:
|
|
8
|
+
- .gitignore
|
|
9
|
+
- .cursorignore
|
|
10
|
+
ignore-paths:
|
|
11
|
+
- .pyossmtool/
|
|
12
|
+
- venv/
|
|
13
|
+
- .venv/
|
|
14
|
+
- reports/
|
|
15
|
+
- sample_files/
|
|
16
|
+
checks:
|
|
17
|
+
- id: ruff.format
|
|
18
|
+
- id: ruff.lint
|
|
19
|
+
- id: ruff.unused
|
|
20
|
+
- id: ty.check
|
|
21
|
+
- id: shfmt.format
|
|
22
|
+
- id: shellcheck
|
|
23
|
+
- id: bandit.scan
|
|
24
|
+
- id: gitleaks.secrets
|
|
25
|
+
- id: semgrep.scan
|
|
26
|
+
- id: deadcode.scan
|
|
27
|
+
- id: vulture.scan
|
|
28
|
+
- id: pydeps.cycles
|
|
29
|
+
- id: radon.cc
|
|
30
|
+
- id: radon.mi
|
|
31
|
+
- id: jscpd.duplication
|
|
32
|
+
- id: mdformat.format
|
|
33
|
+
- id: markdownlint.check
|
|
34
|
+
- id: yamlfmt.format
|
|
35
|
+
- id: yamllint.check
|
|
36
|
+
- id: codespell.spelling
|
|
37
|
+
- id: pytest.test
|
|
38
|
+
target: tests/
|
|
39
|
+
- id: pytest.coverage
|
|
40
|
+
target: tests/
|
|
41
|
+
- id: hadolint.dockerfile
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
id: standard
|
|
2
|
+
name: Standard Quality Suite
|
|
3
|
+
description: Full portable quality gate set for new Python projects (matches reslab
|
|
4
|
+
tool coverage).
|
|
5
|
+
env: auto
|
|
6
|
+
target: .
|
|
7
|
+
ignore-profile:
|
|
8
|
+
- .gitignore
|
|
9
|
+
- .cursorignore
|
|
10
|
+
ignore-paths:
|
|
11
|
+
- .pyossmtool/
|
|
12
|
+
- venv/
|
|
13
|
+
- .venv/
|
|
14
|
+
- reports/
|
|
15
|
+
- sample_files/
|
|
16
|
+
checks:
|
|
17
|
+
- id: ruff.format
|
|
18
|
+
- id: ruff.lint
|
|
19
|
+
- id: ruff.unused
|
|
20
|
+
- id: ty.check
|
|
21
|
+
- id: bandit.scan
|
|
22
|
+
- id: gitleaks.secrets
|
|
23
|
+
- id: semgrep.scan
|
|
24
|
+
- id: deadcode.scan
|
|
25
|
+
- id: vulture.scan
|
|
26
|
+
- id: pydeps.cycles
|
|
27
|
+
- id: radon.cc
|
|
28
|
+
- id: radon.mi
|
|
29
|
+
- id: jscpd.duplication
|
|
30
|
+
- id: shfmt.format
|
|
31
|
+
- id: shellcheck
|
|
32
|
+
- id: mdformat.format
|
|
33
|
+
- id: markdownlint.check
|
|
34
|
+
- id: yamlfmt.format
|
|
35
|
+
- id: yamllint.check
|
|
36
|
+
- id: codespell.spelling
|
|
37
|
+
- id: pytest.test
|
|
38
|
+
target: tests/
|
|
39
|
+
- id: pytest.coverage
|
|
40
|
+
target: tests/
|
|
41
|
+
- id: hadolint.dockerfile
|
|
42
|
+
- id: gate.module-size
|
|
43
|
+
- id: gate.module-private-vars
|
|
44
|
+
- id: gate.folder-breadth
|
|
45
|
+
- id: gate.acronym-allowlist
|
pyossmtool/cli.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""pyossmtool command-line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from pyossmtool.gates import lib_path, scaffold_gate
|
|
11
|
+
from pyossmtool.installer import Installer
|
|
12
|
+
from pyossmtool.models import CheckMode, FailureReport
|
|
13
|
+
from pyossmtool.registry import Registry
|
|
14
|
+
from pyossmtool.runner import Runner
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="Quality-gate orchestrator for AI-assisted development")
|
|
17
|
+
list_app = typer.Typer(help="List catalog entries")
|
|
18
|
+
gate_app = typer.Typer(help="Shell script gate framework")
|
|
19
|
+
gates_app = typer.Typer(help="Gate library utilities")
|
|
20
|
+
app.add_typer(list_app, name="list")
|
|
21
|
+
app.add_typer(gate_app, name="gate")
|
|
22
|
+
app.add_typer(gates_app, name="gates")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _project_root() -> Path:
|
|
26
|
+
return Path.cwd()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _registry() -> Registry:
|
|
30
|
+
return Registry(project_root=_project_root())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@list_app.command("tools")
|
|
34
|
+
def list_tools() -> None:
|
|
35
|
+
registry = _registry()
|
|
36
|
+
for tool in registry.tools.values():
|
|
37
|
+
typer.echo(f"{tool.id}\t{tool.name}\t({tool.install.method.value})")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@list_app.command("checks")
|
|
41
|
+
def list_checks() -> None:
|
|
42
|
+
registry = _registry()
|
|
43
|
+
for check in registry.checks.values():
|
|
44
|
+
origin = "project" if check.tool == "script" else "bundled"
|
|
45
|
+
typer.echo(f"{check.id}\t{check.tool}\t{check.name}\t{check.mode.value}\t({origin})")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@list_app.command("suites")
|
|
49
|
+
def list_suites() -> None:
|
|
50
|
+
registry = _registry()
|
|
51
|
+
for suite in registry.suites.values():
|
|
52
|
+
typer.echo(f"{suite.id}\t{suite.name}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@gates_app.command("lib-path")
|
|
56
|
+
def gates_lib_path() -> None:
|
|
57
|
+
"""Print absolute path to defaults/gates/lib.sh for sourcing in scripts."""
|
|
58
|
+
typer.echo(str(lib_path()))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@gate_app.command("init")
|
|
62
|
+
def gate_init(
|
|
63
|
+
name: str = typer.Argument(..., help="Gate name (e.g. module-size)"),
|
|
64
|
+
description: str = typer.Option("Project script gate", "--description", "-d"),
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Scaffold a script gate under .pyossmtool/gates/ and register it in the catalog."""
|
|
67
|
+
project_root = _project_root()
|
|
68
|
+
try:
|
|
69
|
+
script_path, catalog_path = scaffold_gate(project_root, name, description)
|
|
70
|
+
except FileExistsError as exc:
|
|
71
|
+
typer.echo(f"SETUP {exc}", err=True)
|
|
72
|
+
raise typer.Exit(code=2) from exc
|
|
73
|
+
|
|
74
|
+
typer.echo(f"Created gate script: {script_path.relative_to(project_root)}")
|
|
75
|
+
typer.echo(f"Created catalog check: {catalog_path.relative_to(project_root)}")
|
|
76
|
+
typer.echo("")
|
|
77
|
+
typer.echo("Add the check to pyossmtool.yaml:")
|
|
78
|
+
typer.echo(f" - id: {catalog_path.stem}")
|
|
79
|
+
typer.echo("")
|
|
80
|
+
typer.echo("Run it:")
|
|
81
|
+
typer.echo(f" pyossmtool check --check {catalog_path.stem} --target .")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command("schema")
|
|
85
|
+
def export_schema() -> None:
|
|
86
|
+
typer.echo(json.dumps(FailureReport.model_json_schema(), indent=2))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command("install")
|
|
90
|
+
def install(
|
|
91
|
+
suite: str = typer.Option("demo", "--suite", help="Suite whose tool dependencies to install"),
|
|
92
|
+
) -> None:
|
|
93
|
+
registry = _registry()
|
|
94
|
+
installer = Installer(registry, _project_root())
|
|
95
|
+
try:
|
|
96
|
+
installer.install_suite(suite)
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
typer.echo(f"SETUP {exc}", err=True)
|
|
99
|
+
raise typer.Exit(code=2) from exc
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@app.command("check")
|
|
103
|
+
def check_cmd(
|
|
104
|
+
suite: str | None = typer.Option(None, "--suite", help="Suite id to run"),
|
|
105
|
+
check: str | None = typer.Option(None, "--check", help="Single check id to run"),
|
|
106
|
+
target: str | None = typer.Option(None, "--target", help="Target path for single check"),
|
|
107
|
+
fail_fast: bool = typer.Option(False, "--fail-fast", help="Stop at first failure"),
|
|
108
|
+
verbose: bool = typer.Option(False, "--verbose", help="Print commands and keep raw artifacts on success"),
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Report-only quality checks (no file writes from formatters)."""
|
|
111
|
+
raise typer.Exit(code=_execute(CheckMode.CHECK, suite, check, target, fail_fast, verbose))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@app.command("format")
|
|
115
|
+
def format_cmd(
|
|
116
|
+
suite: str | None = typer.Option("format", "--suite", help="Suite id to run"),
|
|
117
|
+
check: str | None = typer.Option(None, "--check", help="Single check id to run"),
|
|
118
|
+
target: str | None = typer.Option(None, "--target", help="Target path for single check"),
|
|
119
|
+
fail_fast: bool = typer.Option(False, "--fail-fast", help="Stop at first failure"),
|
|
120
|
+
verbose: bool = typer.Option(False, "--verbose", help="Print commands and keep raw artifacts on success"),
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Apply formatters / autofix checks that write to the tree."""
|
|
123
|
+
raise typer.Exit(code=_execute(CheckMode.FORMAT, suite, check, target, fail_fast, verbose))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _execute(
|
|
127
|
+
mode: CheckMode,
|
|
128
|
+
suite: str | None,
|
|
129
|
+
check: str | None,
|
|
130
|
+
target: str | None,
|
|
131
|
+
fail_fast: bool,
|
|
132
|
+
verbose: bool,
|
|
133
|
+
) -> int:
|
|
134
|
+
registry = _registry()
|
|
135
|
+
project_root = _project_root()
|
|
136
|
+
project_config = registry.load_project_config(project_root)
|
|
137
|
+
runner = Runner(registry, project_root, verbose=verbose)
|
|
138
|
+
|
|
139
|
+
if check:
|
|
140
|
+
return _run_single_check(runner, check, target, suite, project_config, mode)
|
|
141
|
+
|
|
142
|
+
suite_id = suite or (project_config.suite if project_config else None)
|
|
143
|
+
if not suite_id:
|
|
144
|
+
typer.echo("SETUP provide --suite or create pyossmtool.yaml", err=True)
|
|
145
|
+
return 2
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
suite_result = runner.run_suite(
|
|
149
|
+
suite_id,
|
|
150
|
+
project_config=project_config,
|
|
151
|
+
fail_fast=fail_fast,
|
|
152
|
+
mode=mode,
|
|
153
|
+
)
|
|
154
|
+
except KeyError as exc:
|
|
155
|
+
typer.echo(f"SETUP {exc}", err=True)
|
|
156
|
+
return 2
|
|
157
|
+
return _emit_suite_result(suite_result)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _run_single_check(
|
|
161
|
+
runner: Runner,
|
|
162
|
+
check: str,
|
|
163
|
+
target: str | None,
|
|
164
|
+
suite: str | None,
|
|
165
|
+
project_config,
|
|
166
|
+
mode: CheckMode,
|
|
167
|
+
) -> int:
|
|
168
|
+
if not target:
|
|
169
|
+
typer.echo("SETUP --target is required with --check", err=True)
|
|
170
|
+
return 2
|
|
171
|
+
check_def = runner.registry.get_check(check)
|
|
172
|
+
if check_def.mode != mode:
|
|
173
|
+
typer.echo(
|
|
174
|
+
f"SETUP check '{check}' has mode={check_def.mode.value}; use pyossmtool {check_def.mode.value}",
|
|
175
|
+
err=True,
|
|
176
|
+
)
|
|
177
|
+
return 2
|
|
178
|
+
result = runner.run_check(
|
|
179
|
+
check,
|
|
180
|
+
target=target,
|
|
181
|
+
suite_id=suite or (project_config.suite if project_config else None),
|
|
182
|
+
project_config=project_config,
|
|
183
|
+
)
|
|
184
|
+
return _exit_for_check_result(result)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _exit_for_check_result(result) -> int:
|
|
188
|
+
if result.error:
|
|
189
|
+
typer.echo(f"SETUP {result.error}", err=True)
|
|
190
|
+
return 2
|
|
191
|
+
if result.passed:
|
|
192
|
+
return 0
|
|
193
|
+
typer.echo(f"FAIL {result.check_id} -> {result.report_path}", err=True)
|
|
194
|
+
return 1
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _emit_suite_result(suite_result) -> int:
|
|
198
|
+
if suite_result.passed:
|
|
199
|
+
return 0
|
|
200
|
+
for result in suite_result.results:
|
|
201
|
+
_emit_failed_check(result)
|
|
202
|
+
return 1
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _emit_failed_check(result) -> None:
|
|
206
|
+
if result.passed:
|
|
207
|
+
return
|
|
208
|
+
if result.error:
|
|
209
|
+
typer.echo(f"SETUP {result.check_id}: {result.error}", err=True)
|
|
210
|
+
return
|
|
211
|
+
if result.report_path:
|
|
212
|
+
typer.echo(f"FAIL {result.check_id} -> {result.report_path}", err=True)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def main() -> None:
|
|
216
|
+
app()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
_TYPER_COMMANDS = (
|
|
220
|
+
list_tools,
|
|
221
|
+
list_checks,
|
|
222
|
+
list_suites,
|
|
223
|
+
gates_lib_path,
|
|
224
|
+
gate_init,
|
|
225
|
+
export_schema,
|
|
226
|
+
install,
|
|
227
|
+
check_cmd,
|
|
228
|
+
format_cmd,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == "__main__":
|
|
233
|
+
main()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Resolve tool configuration: repo-native > pyossmtool.yaml paths > bundled defaults."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tomllib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pyossmtool.models import (
|
|
9
|
+
ConfigMode,
|
|
10
|
+
ConfigSpec,
|
|
11
|
+
ProjectConfig,
|
|
12
|
+
ToolConfigSpec,
|
|
13
|
+
ToolDef,
|
|
14
|
+
)
|
|
15
|
+
from pyossmtool.registry import BUNDLE_ROOT
|
|
16
|
+
|
|
17
|
+
DEFAULTS_DIR = BUNDLE_ROOT / "defaults" / "configs"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConfigResolver:
|
|
21
|
+
def __init__(self, project_root: Path) -> None:
|
|
22
|
+
self.project_root = project_root.resolve()
|
|
23
|
+
self.defaults_dir = DEFAULTS_DIR
|
|
24
|
+
|
|
25
|
+
def extra_argv(self, tool: ToolDef, project_config: ProjectConfig | None) -> list[str]:
|
|
26
|
+
config_path = self._selected_config_path(tool, project_config)
|
|
27
|
+
if config_path is None:
|
|
28
|
+
return []
|
|
29
|
+
return self._config_argv(tool, config_path)
|
|
30
|
+
|
|
31
|
+
def resolve_config_path(self, tool: ToolDef, project_config: ProjectConfig | None) -> Path | None:
|
|
32
|
+
path = self._selected_config_path(tool, project_config)
|
|
33
|
+
return path.resolve() if path and path.exists() else None
|
|
34
|
+
|
|
35
|
+
def _selected_config_path(self, tool: ToolDef, project_config: ProjectConfig | None) -> Path | None:
|
|
36
|
+
spec = project_config.configs if project_config else ConfigSpec()
|
|
37
|
+
if spec.mode == ConfigMode.PATHS:
|
|
38
|
+
return self._paths_mode_config(tool.id, spec)
|
|
39
|
+
if tool.config is None:
|
|
40
|
+
return None
|
|
41
|
+
if spec.mode == ConfigMode.BUNDLED:
|
|
42
|
+
return self._bundled_mode_config(tool.config)
|
|
43
|
+
return self._auto_mode_config(tool.config)
|
|
44
|
+
|
|
45
|
+
def _paths_mode_config(self, tool_id: str, spec: ConfigSpec) -> Path | None:
|
|
46
|
+
path = spec.paths.get(tool_id)
|
|
47
|
+
return (self.project_root / path).resolve() if path else None
|
|
48
|
+
|
|
49
|
+
def _bundled_mode_config(self, config: ToolConfigSpec) -> Path | None:
|
|
50
|
+
bundled = self._bundled_path(config)
|
|
51
|
+
return bundled.resolve() if bundled and bundled.exists() else None
|
|
52
|
+
|
|
53
|
+
def _auto_mode_config(self, config: ToolConfigSpec) -> Path | None:
|
|
54
|
+
if config.use_pyproject_as_path and self._pyproject_has_table(*config.pyproject):
|
|
55
|
+
return self.project_root / "pyproject.toml"
|
|
56
|
+
if self._repo_has_config(config):
|
|
57
|
+
return None
|
|
58
|
+
return self._bundled_mode_config(config)
|
|
59
|
+
|
|
60
|
+
def _config_argv(self, tool: ToolDef, config_path: Path) -> list[str]:
|
|
61
|
+
if not config_path.exists() or tool.config is None:
|
|
62
|
+
return []
|
|
63
|
+
if not tool.config.should_pass_flag() or not tool.config.flag:
|
|
64
|
+
return []
|
|
65
|
+
return [tool.config.flag, str(config_path)]
|
|
66
|
+
|
|
67
|
+
def _repo_has_config(self, config: ToolConfigSpec) -> bool:
|
|
68
|
+
if self._repo_has_files(config):
|
|
69
|
+
return True
|
|
70
|
+
if self._repo_has_dirs(config):
|
|
71
|
+
return True
|
|
72
|
+
return bool(config.pyproject) and self._pyproject_has_table(*config.pyproject)
|
|
73
|
+
|
|
74
|
+
def _repo_has_files(self, config: ToolConfigSpec) -> bool:
|
|
75
|
+
return any((self.project_root / name).exists() for name in config.repo_files)
|
|
76
|
+
|
|
77
|
+
def _repo_has_dirs(self, config: ToolConfigSpec) -> bool:
|
|
78
|
+
return any(self._nonempty_dir(name) for name in config.repo_dirs)
|
|
79
|
+
|
|
80
|
+
def _nonempty_dir(self, rel: str) -> bool:
|
|
81
|
+
candidate = self.project_root / rel
|
|
82
|
+
return candidate.is_dir() and any(candidate.iterdir())
|
|
83
|
+
|
|
84
|
+
def _bundled_path(self, config: ToolConfigSpec) -> Path | None:
|
|
85
|
+
if config.bundled_path:
|
|
86
|
+
path = BUNDLE_ROOT / config.bundled_path
|
|
87
|
+
return path if path.exists() else None
|
|
88
|
+
if config.bundled:
|
|
89
|
+
path = self.defaults_dir / config.bundled
|
|
90
|
+
return path if path.exists() else None
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def _pyproject_has_table(self, *keys: str) -> bool:
|
|
94
|
+
if not keys:
|
|
95
|
+
return False
|
|
96
|
+
data = self._load_pyproject()
|
|
97
|
+
if data is None:
|
|
98
|
+
return False
|
|
99
|
+
return _dict_has_path(data, keys)
|
|
100
|
+
|
|
101
|
+
def _load_pyproject(self) -> dict | None:
|
|
102
|
+
pyproject = self.project_root / "pyproject.toml"
|
|
103
|
+
if not pyproject.exists():
|
|
104
|
+
return None
|
|
105
|
+
try:
|
|
106
|
+
return tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
107
|
+
except tomllib.TOMLDecodeError:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _dict_has_path(data: dict, keys: tuple[str, ...]) -> bool:
|
|
112
|
+
node: object = data
|
|
113
|
+
for key in keys:
|
|
114
|
+
if not isinstance(node, dict) or key not in node:
|
|
115
|
+
return False
|
|
116
|
+
node = node[key]
|
|
117
|
+
return True
|