@sebastienrousseau/dotfiles 0.2.500 → 0.2.501
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 +77 -0
- package/README.md +82 -44
- package/docs/.vitepress/reports/localization-readability-audit.md +4 -0
- package/docs/AI.md +8 -2
- package/docs/CNAME +1 -0
- package/docs/COPYRIGHT +1 -1
- package/docs/NAMING_CONVENTIONS.md +7 -0
- package/docs/README.md +4 -0
- package/docs/_config.yml +59 -0
- package/docs/adr/ADR-001-ci-cd-pipeline.md +17 -0
- package/docs/adr/ADR-002-shell-performance.md +9 -0
- package/docs/adr/ADR-003-security-first.md +18 -0
- package/docs/adr/ADR-004-cli-architecture.md +14 -0
- package/docs/adr/ADR-005-chezmoi-choice.md +10 -0
- package/docs/adr/ADR-006-shell-selection.md +9 -0
- package/docs/adr/ADR-007-multi-shell-parity.md +10 -1
- package/docs/adr/ADR-008-alias-system-architecture.md +10 -0
- package/docs/adr/ADR-009-wallpaper-driven-theming.md +131 -0
- package/docs/adr/ADR-010-starship-transient-prompt.md +144 -0
- package/docs/adr/ADR-011-nushell-tier3-keep.md +144 -0
- package/docs/adr/README.md +7 -0
- package/docs/architecture/ARCHITECTURE.md +4 -0
- package/docs/architecture/INTEROP.md +8 -0
- package/docs/architecture/REPO_LAYOUT.md +5 -1
- package/docs/architecture/WALKTHROUGH.md +4 -0
- package/docs/architecture/fleet-deployment.md +4 -0
- package/docs/archive/EUXIS_2026_REVIEW.md +11 -3
- package/docs/archive/LEGACY_ROADMAP.md +45 -27
- package/docs/archive/MILESTONE_v0.2.493.md +4 -0
- package/docs/archive/PLAN.md +46 -8
- package/docs/archive/REPO_AUDIT.md +8 -0
- package/docs/guides/INSTALL.md +4 -0
- package/docs/guides/NEOVIM_IDE_GUIDE.md +9 -0
- package/docs/guides/THEMING.md +8 -0
- package/docs/guides/TROUBLESHOOTING.md +28 -0
- package/docs/guides/WSL2_NIX_TROUBLESHOOTING.md +70 -1
- package/docs/index.md +6 -2
- package/docs/interop/A2A.md +7 -0
- package/docs/interop/POWERSHELL.md +102 -0
- package/docs/manual/00-introduction.md +5 -1
- package/docs/manual/01-concepts/01-architecture.md +4 -0
- package/docs/manual/01-concepts/02-trust-model.md +6 -2
- package/docs/manual/01-concepts/03-theme-engine.md +4 -0
- package/docs/manual/01-concepts/04-fleet.md +7 -1
- package/docs/manual/01-concepts/05-self-healing.md +8 -1
- package/docs/manual/02-tutorials/01-first-install.md +5 -0
- package/docs/manual/02-tutorials/02-add-wallpaper.md +5 -0
- package/docs/manual/02-tutorials/03-create-profile.md +6 -0
- package/docs/manual/02-tutorials/04-encrypt-secret.md +6 -0
- package/docs/manual/02-tutorials/05-deploy-fleet.md +5 -1
- package/docs/manual/03-reference/01-dot-cli.md +4 -0
- package/docs/manual/03-reference/02-config-files.md +8 -2
- package/docs/manual/03-reference/03-environment.md +4 -0
- package/docs/manual/03-reference/04-templates.md +7 -1
- package/docs/manual/03-reference/05-feature-flags.md +10 -0
- package/docs/manual/04-cookbook/01-recipes.md +4 -0
- package/docs/manual/04-cookbook/02-troubleshooting.md +32 -0
- package/docs/manual/04-cookbook/03-faq.md +8 -0
- package/docs/manual/05-appendices/A-platform-matrix.md +4 -0
- package/docs/manual/05-appendices/B-security-checklist.md +6 -0
- package/docs/manual/05-appendices/C-glossary.md +4 -0
- package/docs/manual/05-appendices/D-bibliography.md +5 -1
- package/docs/manual/05-appendices/E-license.md +4 -0
- package/docs/manual/_toc.yml +1 -1
- package/docs/manual/command-index.md +4 -0
- package/docs/manual/concept-index.md +4 -0
- package/docs/operations/ATTESTATION.md +5 -0
- package/docs/operations/CI_CADENCE.md +107 -0
- package/docs/operations/CI_COMPOSITES.md +156 -0
- package/docs/operations/COMPLETIONS.md +123 -0
- package/docs/operations/COVERAGE.md +148 -0
- package/docs/operations/DRIFT.md +107 -0
- package/docs/operations/MAINTENANCE.md +5 -1
- package/docs/operations/MIGRATION.md +14 -6
- package/docs/operations/OPERATIONS.md +24 -0
- package/docs/operations/PERFORMANCE.md +133 -0
- package/docs/operations/RELIABILITY.md +6 -0
- package/docs/operations/ROADMAP.md +36 -18
- package/docs/operations/TESTING.md +4 -0
- package/docs/operations/TRACEABILITY.md +4 -0
- package/docs/operations/TRUSTED_AGENT_WORKSTATION.md +4 -0
- package/docs/operations/VERSION_SYNC.md +54 -5
- package/docs/reference/ALIASES.md +7 -0
- package/docs/reference/ALIASES_CHEATSHEET.md +5 -1
- package/docs/reference/ALIASES_DEPRECATIONS.md +5 -1
- package/docs/reference/FEATURES.md +4 -0
- package/docs/reference/FONTS.md +4 -0
- package/docs/reference/PROFILES.md +4 -0
- package/docs/reference/SCREENSHOTS.md +4 -0
- package/docs/reference/SCRIPTS.md +4 -0
- package/docs/reference/SUPPORT_MATRIX.md +10 -4
- package/docs/reference/THEMES.md +4 -0
- package/docs/reference/TOOLS.md +4 -0
- package/docs/reference/UTILS.md +16 -12
- package/docs/security/AI_ACT_COMPLIANCE.md +4 -0
- package/docs/security/AUDIT_BYPASS.md +103 -0
- package/docs/security/AUTOMATION_SECRETS.md +4 -0
- package/docs/security/CI_EGRESS_ALLOWLIST.md +127 -0
- package/docs/security/COMPLIANCE.md +5 -0
- package/docs/security/DEPS_DEV_EXCEPTIONS.md +86 -0
- package/docs/security/ENCRYPTION.md +5 -0
- package/docs/security/FMEA.md +4 -0
- package/docs/security/HISTORY_FILTERING.md +132 -0
- package/docs/security/INCIDENT_RESPONSE.md +4 -0
- package/docs/security/INSTALL_VERIFICATION.md +122 -0
- package/docs/security/KEYS.md +4 -0
- package/docs/security/KEY_ROTATION.md +4 -0
- package/docs/security/MCP_POLICY.md +9 -0
- package/docs/security/POLICY_RELEASES.md +4 -0
- package/docs/security/README.md +4 -0
- package/docs/security/SCORECARD.md +80 -0
- package/docs/security/SECRETS.md +12 -0
- package/docs/security/SECURITY.md +4 -0
- package/docs/security/SECURITY_CHECKLIST.md +11 -0
- package/docs/security/SHELL_EXEMPTIONS.md +145 -0
- package/docs/security/SOUP_REGISTER.md +4 -0
- package/docs/security/THREAT_MODEL.md +10 -0
- package/docs/security/VERIFICATION_VALIDATION.md +5 -1
- package/docs/themes/README.md +4 -0
- package/docs/themes/VISUAL_INTEGRITY_REPORT.md +4 -0
- package/dot_config/ai/identity.md +3 -0
- package/dot_config/ai/patterns/architect.md +2 -0
- package/dot_config/ai/patterns/hardener.md +2 -0
- package/dot_config/ai/patterns/refactor.md +2 -0
- package/dot_config/alacritty/alacritty.toml.tmpl +3 -3
- package/dot_config/atuin/config.toml.tmpl +47 -0
- package/dot_config/dotfiles/agent-card.json +1 -1
- package/dot_config/dotfiles/boot/README.md +2 -0
- package/dot_config/dotfiles/grub/README.md +2 -0
- package/dot_config/dotfiles/lock/README.md +2 -0
- package/dot_config/fish/conf.d/init.fish.tmpl +21 -0
- package/dot_config/fish/functions/_cached_eval.fish +84 -11
- package/dot_config/fish/functions/_cached_eval_clear.fish +17 -0
- package/dot_config/foot/foot.ini.tmpl +3 -3
- package/dot_config/fuzzel/fuzzel.ini.tmpl +2 -2
- package/dot_config/ghostty/config.tmpl +3 -3
- package/dot_config/git/hooks/executable_commit-msg +146 -0
- package/dot_config/goose/config.yaml +2 -2
- package/dot_config/gtk-3.0/gtk.css.tmpl +2 -2
- package/dot_config/gtk-3.0/settings.ini.tmpl +2 -2
- package/dot_config/gtk-4.0/gtk.css.tmpl +2 -2
- package/dot_config/gtk-4.0/settings.ini.tmpl +2 -2
- package/dot_config/kitty/kitty.conf.tmpl +3 -3
- package/dot_config/mise/config.toml +1 -1
- package/dot_config/niri/config.kdl.tmpl +2 -2
- package/dot_config/nushell/cached_eval.nu +80 -0
- package/dot_config/nushell/env.nu.tmpl +21 -13
- package/dot_config/shell/00-core-paths.sh.tmpl +1 -0
- package/dot_config/shell/05-core-safety.sh +1 -0
- package/dot_config/shell/10-secrets.sh +1 -0
- package/dot_config/shell/40-fzf-defaults.sh.tmpl +1 -0
- package/dot_config/shell/40-ls-colors.sh +1 -0
- package/dot_config/shell/50-logic-functions-core.sh.tmpl +1 -0
- package/dot_config/shell/50-logic-functions.sh.tmpl +1 -0
- package/dot_config/shell/51-logic-functions-extra.sh.tmpl +1 -0
- package/dot_config/shell/90-ux-aliases.sh.tmpl +1 -0
- package/dot_config/shell/91-ux-aliases-lazy.sh.tmpl +1 -0
- package/dot_config/shell/README.md +25 -8
- package/dot_config/starship.toml.tmpl +2 -2
- package/dot_config/tmux/tmux.conf.tmpl +3 -3
- package/dot_config/user-dirs.dirs +1 -0
- package/dot_config/vscode/settings.json.tmpl +2 -2
- package/dot_config/waybar/config.jsonc.tmpl +2 -2
- package/dot_config/waybar/style.css.tmpl +2 -2
- package/dot_config/wezterm/wezterm.lua.tmpl +3 -3
- package/dot_config/zsh/dot_zshrc.tmpl +154 -17
- package/dot_config/zsh/rc.d/00-alias-shims.zsh +28 -6
- package/dot_config/zsh/rc.d/30-options.zsh.tmpl +26 -6
- package/dot_local/bin/executable_bm +2 -0
- package/dot_local/bin/executable_dot +9 -2
- package/dot_local/bin/executable_dot-load-benchmark +1 -1
- package/dot_local/bin/executable_notify +2 -0
- package/dot_local/bin/executable_open +2 -0
- package/dot_local/bin/executable_tour +4 -2
- package/dot_local/bin/executable_up +3 -1
- package/install.sh +12 -4
- package/package.json +1 -1
- package/scripts/ci/check-dangerous-chmod.sh +19 -0
- package/scripts/ci/check-deps-dev.sh +236 -0
- package/scripts/ci/check-insecure-tls.sh +61 -0
- package/scripts/ci/check-regression-traceability.sh +67 -0
- package/scripts/ci/check-shell-preamble.sh +106 -0
- package/scripts/ci/run-coverage.sh +362 -0
- package/scripts/ci/validate-chezmoidata.sh +25 -0
- package/scripts/diagnostics/doctor.sh +173 -5
- package/scripts/diagnostics/drift-dashboard.sh +177 -13
- package/scripts/diagnostics/health.sh +21 -4
- package/scripts/diagnostics/perf.sh +304 -77
- package/scripts/diagnostics/workstation-attestation.sh +6 -1
- package/scripts/dot/commands/agent.sh +14 -5
- package/scripts/dot/commands/ai.sh +75 -10
- package/scripts/dot/lib/bento.sh +2 -1
- package/scripts/dot/lib/log.sh +6 -0
- package/scripts/dot/lib/platform.sh +1 -0
- package/scripts/dot/lib/ui.sh +11 -0
- package/scripts/dot/lib/utils.sh +5 -0
- package/scripts/git-hooks/pre-commit-audit.sh +1 -1
- package/scripts/git-hooks/pre-push +66 -3
- package/scripts/ops/heal-chezmoi.sh +41 -6
- package/scripts/qa/powershell-contract.ps1 +95 -0
- package/scripts/theme/merge-wallpaper.sh +4 -0
- package/scripts/theme/switch.sh +20 -10
- package/dot_config/atuin/config.toml +0 -40
- package/dot_local/bin/__pycache__/executable_dot-load-benchmark-ptycpython-312.pyc +0 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Copyright (c) 2015-2026 Dotfiles. All rights reserved.
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# run-coverage.sh — Bash code-coverage runner using pure xtrace
|
|
5
|
+
# instrumentation. Emits lcov.info at $COVERAGE_OUT.
|
|
6
|
+
#
|
|
7
|
+
# Why pure xtrace (not kcov):
|
|
8
|
+
# kcov v43 on Ubuntu 24.04 + bash 5.2 won't emit bash-script
|
|
9
|
+
# coverage in either mode: without bash-dbgsym it captures nothing,
|
|
10
|
+
# and with bash-dbgsym it captures bash's *C-binary* internals
|
|
11
|
+
# (ctype.h, stdio.h) instead of the .sh file lines we care about.
|
|
12
|
+
# We bypass kcov entirely by using bash's own xtrace mechanism:
|
|
13
|
+
#
|
|
14
|
+
# PS4='+:${LINENO}:${BASH_SOURCE}:' # encode line + file in trace
|
|
15
|
+
# BASH_ENV=/setup-that-runs-set-x # turn on xtrace in every
|
|
16
|
+
# # non-interactive bash, so
|
|
17
|
+
# # children inherit tracing
|
|
18
|
+
# bash test.sh 2>traces/test.trace # capture lines per test
|
|
19
|
+
#
|
|
20
|
+
# Then we parse all traces with regex and write lcov.info. This is
|
|
21
|
+
# the same mechanism bashcov uses, minus the Ruby runtime.
|
|
22
|
+
#
|
|
23
|
+
# Linux + macOS both work (macOS bash 3.2 supports BASH_XTRACEFD as
|
|
24
|
+
# well — verified locally on bash 5.3 via Homebrew). The pre-commit
|
|
25
|
+
# hook in this repo can invoke this on any platform.
|
|
26
|
+
#
|
|
27
|
+
# Closes the runner half of #856 / Slice 1 of #883.
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
set -uo pipefail
|
|
31
|
+
|
|
32
|
+
REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
33
|
+
TESTS_DIR="${TESTS_DIR:-$REPO_ROOT/tests}"
|
|
34
|
+
COVERAGE_DIR="${COVERAGE_DIR:-$REPO_ROOT/coverage}"
|
|
35
|
+
COVERAGE_OUT="${COVERAGE_OUT:-$COVERAGE_DIR/lcov.info}"
|
|
36
|
+
MIN_COVERAGE_PCT="${MIN_COVERAGE_PCT:-0}" # initial floor; tighten per slice
|
|
37
|
+
COV_INCLUDE_DIRS="${COV_INCLUDE_DIRS:-$REPO_ROOT/scripts:$REPO_ROOT/dot_local/bin:$REPO_ROOT/.chezmoitemplates/functions}"
|
|
38
|
+
JOBS="${JOBS:-$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)}"
|
|
39
|
+
COV_TEST_TIMEOUT="${COV_TEST_TIMEOUT:-60}"
|
|
40
|
+
|
|
41
|
+
mkdir -p "$COVERAGE_DIR"
|
|
42
|
+
trace_dir="$COVERAGE_DIR/traces"
|
|
43
|
+
rm -rf "$trace_dir"
|
|
44
|
+
mkdir -p "$trace_dir"
|
|
45
|
+
|
|
46
|
+
# -----------------------------------------------------------------------------
|
|
47
|
+
# BASH_ENV setup file: enables xtrace in every non-interactive bash that
|
|
48
|
+
# inherits it. Child processes spawned via `bash $SCRIPT` get their own
|
|
49
|
+
# tracing turned on automatically.
|
|
50
|
+
# -----------------------------------------------------------------------------
|
|
51
|
+
bash_env="$COVERAGE_DIR/_cov_bashenv.sh"
|
|
52
|
+
cat >"$bash_env" <<'SETUP'
|
|
53
|
+
# coverage runtime — enable xtrace, define PS4 with file+line markers.
|
|
54
|
+
# `${BASH_SOURCE:-}` (not `${BASH_SOURCE}`) prevents PS4 evaluation
|
|
55
|
+
# from failing under `set -u`: at the top level of a `bash -c`
|
|
56
|
+
# script, BASH_SOURCE[0] is unbound; an unguarded expansion under
|
|
57
|
+
# `set -u` aborts the shell, taking out any test that sources a
|
|
58
|
+
# `set -euo pipefail` library file.
|
|
59
|
+
set -x
|
|
60
|
+
PS4='+@COV@:${LINENO}:${BASH_SOURCE:-}:@ '
|
|
61
|
+
SETUP
|
|
62
|
+
|
|
63
|
+
# Sanity-probe: run one trivial script through the pipeline so a
|
|
64
|
+
# subsequent failure of the real sweep can be diagnosed quickly.
|
|
65
|
+
probe_target="$COVERAGE_DIR/_probe_target.sh"
|
|
66
|
+
cat >"$probe_target" <<'PROBE'
|
|
67
|
+
#!/usr/bin/env bash
|
|
68
|
+
set -uo pipefail
|
|
69
|
+
echo "probe-line-a"
|
|
70
|
+
x=1; y=2
|
|
71
|
+
echo "probe-sum=$((x + y))"
|
|
72
|
+
PROBE
|
|
73
|
+
chmod +x "$probe_target"
|
|
74
|
+
|
|
75
|
+
PS4='+@COV@:${LINENO}:${BASH_SOURCE}:@ ' \
|
|
76
|
+
BASH_ENV="$bash_env" \
|
|
77
|
+
bash "$probe_target" 2>"$trace_dir/_probe.trace" >/dev/null || true
|
|
78
|
+
probe_lines=$(grep -cE "^\+@COV@:[0-9]+:.*${probe_target}:@" "$trace_dir/_probe.trace" 2>/dev/null || echo 0)
|
|
79
|
+
echo "probe: ${probe_lines} traced lines from ${probe_target}" >&2
|
|
80
|
+
if [[ "$probe_lines" -eq 0 ]]; then
|
|
81
|
+
echo "::error::xtrace probe captured no lines — coverage mechanism broken" >&2
|
|
82
|
+
exit 2
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# -----------------------------------------------------------------------------
|
|
86
|
+
# Collect test files (mirror tests/framework/test_runner.sh discovery).
|
|
87
|
+
# -----------------------------------------------------------------------------
|
|
88
|
+
mapfile -t test_files < <(find "$TESTS_DIR/unit" -name 'test_*.sh' -type f | sort)
|
|
89
|
+
|
|
90
|
+
if [[ "${#test_files[@]}" -eq 0 ]]; then
|
|
91
|
+
echo "::error::no unit test files discovered under $TESTS_DIR/unit" >&2
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
echo "Tracing ${#test_files[@]} test files (parallel × $JOBS, timeout ${COV_TEST_TIMEOUT}s/test)..." >&2
|
|
96
|
+
|
|
97
|
+
# -----------------------------------------------------------------------------
|
|
98
|
+
# Worker function — runs one test under xtrace, captures stderr.
|
|
99
|
+
# -----------------------------------------------------------------------------
|
|
100
|
+
# shellcheck disable=SC2329 # called indirectly via xargs subshell
|
|
101
|
+
run_one() {
|
|
102
|
+
local f="$1"
|
|
103
|
+
local trace
|
|
104
|
+
trace="$COV_TRACE_DIR/$(basename "$f" .sh).trace"
|
|
105
|
+
PS4='+@COV@:${LINENO}:${BASH_SOURCE}:@ ' \
|
|
106
|
+
BASH_ENV="$COV_BASH_ENV" \
|
|
107
|
+
timeout --kill-after=5 "$COV_TEST_TIMEOUT" \
|
|
108
|
+
bash "$f" 2>"$trace" >/dev/null || true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export COV_TRACE_DIR="$trace_dir"
|
|
112
|
+
export COV_BASH_ENV="$bash_env"
|
|
113
|
+
export COV_TEST_TIMEOUT
|
|
114
|
+
export -f run_one
|
|
115
|
+
|
|
116
|
+
start_ts=$(date +%s)
|
|
117
|
+
printf '%s\n' "${test_files[@]}" |
|
|
118
|
+
xargs -I{} -n1 -P"$JOBS" bash -c 'run_one "$@"' _ {} ||
|
|
119
|
+
true
|
|
120
|
+
elapsed=$(($(date +%s) - start_ts))
|
|
121
|
+
echo "trace phase done in ${elapsed}s" >&2
|
|
122
|
+
|
|
123
|
+
# -----------------------------------------------------------------------------
|
|
124
|
+
# Aggregate trace files → lcov.info.
|
|
125
|
+
# -----------------------------------------------------------------------------
|
|
126
|
+
python3 - "$COVERAGE_OUT" "$trace_dir" "$REPO_ROOT" "$COV_INCLUDE_DIRS" <<'PY'
|
|
127
|
+
"""Aggregate bash xtrace output into lcov.info."""
|
|
128
|
+
import os
|
|
129
|
+
import re
|
|
130
|
+
import sys
|
|
131
|
+
from collections import defaultdict
|
|
132
|
+
from pathlib import Path
|
|
133
|
+
|
|
134
|
+
out_path, trace_dir, repo_root, include_dirs_spec = sys.argv[1:5]
|
|
135
|
+
include_dirs = [Path(p).resolve() for p in include_dirs_spec.split(":") if p]
|
|
136
|
+
trace_dir = Path(trace_dir)
|
|
137
|
+
repo_root = Path(repo_root).resolve()
|
|
138
|
+
|
|
139
|
+
# Pattern: +@COV@:<lineno>:<source>:@
|
|
140
|
+
hit_re = re.compile(r"^\+@COV@:(\d+):([^:]+):@")
|
|
141
|
+
|
|
142
|
+
# files[abs_path][line] = total hits
|
|
143
|
+
files = defaultdict(lambda: defaultdict(int))
|
|
144
|
+
|
|
145
|
+
def in_includes(path: Path) -> bool:
|
|
146
|
+
try:
|
|
147
|
+
rp = path.resolve()
|
|
148
|
+
except (OSError, RuntimeError):
|
|
149
|
+
return False
|
|
150
|
+
for inc in include_dirs:
|
|
151
|
+
try:
|
|
152
|
+
rp.relative_to(inc)
|
|
153
|
+
return True
|
|
154
|
+
except ValueError:
|
|
155
|
+
continue
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
# Parse every trace file. Each trace can be MBs; iterate line by line.
|
|
159
|
+
for trace_path in sorted(trace_dir.glob("*.trace")):
|
|
160
|
+
try:
|
|
161
|
+
with open(trace_path, "r", errors="replace") as f:
|
|
162
|
+
for line in f:
|
|
163
|
+
m = hit_re.match(line)
|
|
164
|
+
if not m:
|
|
165
|
+
continue
|
|
166
|
+
lineno = int(m.group(1))
|
|
167
|
+
src = m.group(2).strip()
|
|
168
|
+
if not src or src == "main":
|
|
169
|
+
continue
|
|
170
|
+
src_path = Path(src)
|
|
171
|
+
if not src_path.is_absolute():
|
|
172
|
+
src_path = (repo_root / src_path)
|
|
173
|
+
# Always resolve so `..`-style paths from inside
|
|
174
|
+
# `commands/foo.sh` sourcing `../lib/log.sh` collapse
|
|
175
|
+
# to the same canonical SF: key as a direct hit on
|
|
176
|
+
# `lib/log.sh`. Without this we get three SF: blocks
|
|
177
|
+
# for one file and the denominator inflates.
|
|
178
|
+
try:
|
|
179
|
+
src_path = src_path.resolve()
|
|
180
|
+
except (OSError, RuntimeError):
|
|
181
|
+
pass
|
|
182
|
+
if not in_includes(src_path):
|
|
183
|
+
continue
|
|
184
|
+
files[str(src_path)][lineno] += 1
|
|
185
|
+
except OSError as e:
|
|
186
|
+
print(f"warn: read error {trace_path}: {e}", file=sys.stderr)
|
|
187
|
+
|
|
188
|
+
# Sweep through include-dirs and add executable lines for files we never
|
|
189
|
+
# touched, so lcov coverage % reflects total source surface, not just
|
|
190
|
+
# touched files.
|
|
191
|
+
ws_re = re.compile(r"^\s*$")
|
|
192
|
+
comment_re = re.compile(r"^\s*#")
|
|
193
|
+
shebang_re = re.compile(r"^\s*#!")
|
|
194
|
+
|
|
195
|
+
def executable_lines(path: Path):
|
|
196
|
+
"""Lines that bash xtrace can plausibly emit.
|
|
197
|
+
|
|
198
|
+
Excludes:
|
|
199
|
+
- blanks / shebangs / comments (already)
|
|
200
|
+
- structural-only keywords (`fi`, `done`, `else`, `esac`, `then`,
|
|
201
|
+
`do`, `in`, bare `{`/`}` braces) — these are syntax, not
|
|
202
|
+
commands; xtrace never traces them
|
|
203
|
+
- heredoc body lines (between `<<EOF` and `EOF`) — content, not
|
|
204
|
+
executed
|
|
205
|
+
- `case` pattern labels (`foo)`) — the matched body executes,
|
|
206
|
+
not the pattern itself
|
|
207
|
+
|
|
208
|
+
This matches what `bash -x` actually produces a `+`-prefixed
|
|
209
|
+
line for, so the denominator reflects measurable lines.
|
|
210
|
+
|
|
211
|
+
Honor LCOV_EXCL_LINE / LCOV_EXCL_START / LCOV_EXCL_STOP markers
|
|
212
|
+
so authors can exempt genuinely unreachable lines (platform-
|
|
213
|
+
gated branches, root-only paths, dead-code stubs).
|
|
214
|
+
"""
|
|
215
|
+
import re as _re
|
|
216
|
+
structural = _re.compile(
|
|
217
|
+
r"^\s*("
|
|
218
|
+
r"fi|done|else|elif|esac|then|do|in|"
|
|
219
|
+
r"\}|\{|"
|
|
220
|
+
r"\)\s*;?;?\s*$|" # bare `)` (case pattern close)
|
|
221
|
+
r";;\s*$" # `;;` case-clause terminator
|
|
222
|
+
r")\s*(#.*)?$"
|
|
223
|
+
)
|
|
224
|
+
case_pattern = _re.compile(r"^\s*[a-zA-Z0-9_\*\?\[\|/\-\.+]+\)\s*(#.*)?$")
|
|
225
|
+
heredoc_open = _re.compile(r"<<-?\s*[\"\']?([A-Za-z_][A-Za-z0-9_]*)[\"\']?")
|
|
226
|
+
excl_line = _re.compile(r"#\s*LCOV_EXCL_LINE")
|
|
227
|
+
excl_start = _re.compile(r"#\s*LCOV_EXCL_START")
|
|
228
|
+
excl_stop = _re.compile(r"#\s*LCOV_EXCL_STOP")
|
|
229
|
+
# Scripts that explicitly turn off xtrace can't be measured by
|
|
230
|
+
# this mechanism — the bash runtime simply stops emitting trace
|
|
231
|
+
# records. Treat everything after `set +x` / `set +o xtrace`
|
|
232
|
+
# as excluded so it doesn't sink the denominator.
|
|
233
|
+
xtrace_off = _re.compile(r"^\s*set\s+(\+x|\+o\s+xtrace)\b")
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
with open(path, "r", errors="replace") as f:
|
|
237
|
+
text = f.read().splitlines()
|
|
238
|
+
except OSError:
|
|
239
|
+
return []
|
|
240
|
+
|
|
241
|
+
out = []
|
|
242
|
+
in_heredoc = None # the terminator we're waiting for
|
|
243
|
+
excluding = False
|
|
244
|
+
xtrace_disabled = False
|
|
245
|
+
for i, line in enumerate(text, 1):
|
|
246
|
+
# LCOV_EXCL handling
|
|
247
|
+
if excl_start.search(line):
|
|
248
|
+
excluding = True
|
|
249
|
+
continue
|
|
250
|
+
if excl_stop.search(line):
|
|
251
|
+
excluding = False
|
|
252
|
+
continue
|
|
253
|
+
if excluding:
|
|
254
|
+
continue
|
|
255
|
+
if excl_line.search(line):
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
# Once a script disables xtrace, no further lines can be
|
|
259
|
+
# measured by this mechanism — drop them from the denominator.
|
|
260
|
+
if xtrace_disabled:
|
|
261
|
+
continue
|
|
262
|
+
if xtrace_off.match(line):
|
|
263
|
+
xtrace_disabled = True
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
# Heredoc body — track and skip
|
|
267
|
+
if in_heredoc is not None:
|
|
268
|
+
if line.strip() == in_heredoc:
|
|
269
|
+
in_heredoc = None
|
|
270
|
+
continue
|
|
271
|
+
hd = heredoc_open.search(line)
|
|
272
|
+
if hd and not comment_re.match(line):
|
|
273
|
+
in_heredoc = hd.group(1)
|
|
274
|
+
|
|
275
|
+
if not line or ws_re.match(line):
|
|
276
|
+
continue
|
|
277
|
+
if shebang_re.match(line):
|
|
278
|
+
continue
|
|
279
|
+
if comment_re.match(line):
|
|
280
|
+
continue
|
|
281
|
+
if structural.match(line):
|
|
282
|
+
continue
|
|
283
|
+
if case_pattern.match(line):
|
|
284
|
+
continue
|
|
285
|
+
out.append(i)
|
|
286
|
+
return out
|
|
287
|
+
|
|
288
|
+
for inc in include_dirs:
|
|
289
|
+
if not inc.exists():
|
|
290
|
+
continue
|
|
291
|
+
for path in inc.rglob("*.sh"):
|
|
292
|
+
ap = str(path.resolve())
|
|
293
|
+
existing = files[ap] # creates the entry on touch
|
|
294
|
+
for ln in executable_lines(path):
|
|
295
|
+
if ln not in existing:
|
|
296
|
+
existing[ln] = 0
|
|
297
|
+
for path in inc.rglob("*"):
|
|
298
|
+
# also include shebanged shell scripts without .sh
|
|
299
|
+
if path.is_file() and not path.suffix and path.stat().st_size > 0:
|
|
300
|
+
try:
|
|
301
|
+
with open(path, "r", errors="replace") as f:
|
|
302
|
+
first = f.readline()
|
|
303
|
+
except OSError:
|
|
304
|
+
continue
|
|
305
|
+
if first.startswith("#!") and ("bash" in first or "sh" in first):
|
|
306
|
+
ap = str(path.resolve())
|
|
307
|
+
existing = files[ap]
|
|
308
|
+
for ln in executable_lines(path):
|
|
309
|
+
if ln not in existing:
|
|
310
|
+
existing[ln] = 0
|
|
311
|
+
|
|
312
|
+
# Emit lcov.info
|
|
313
|
+
with open(out_path, "w") as out:
|
|
314
|
+
for filename in sorted(files):
|
|
315
|
+
out.write(f"SF:{filename}\n")
|
|
316
|
+
for ln in sorted(files[filename]):
|
|
317
|
+
out.write(f"DA:{ln},{files[filename][ln]}\n")
|
|
318
|
+
out.write("end_of_record\n")
|
|
319
|
+
|
|
320
|
+
# Summary
|
|
321
|
+
total_files = len(files)
|
|
322
|
+
total_lines = sum(len(lines) for lines in files.values())
|
|
323
|
+
covered = sum(1 for lines in files.values() for h in lines.values() if h > 0)
|
|
324
|
+
pct = (covered * 100.0 / total_lines) if total_lines else 0.0
|
|
325
|
+
print(f"Aggregated: {total_files} files, {covered}/{total_lines} lines = {pct:.2f}%",
|
|
326
|
+
file=sys.stderr)
|
|
327
|
+
PY
|
|
328
|
+
|
|
329
|
+
if [[ ! -s "$COVERAGE_OUT" ]]; then
|
|
330
|
+
echo "::error::failed to produce lcov.info — aggregator wrote empty file." >&2
|
|
331
|
+
exit 1
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
# -----------------------------------------------------------------------------
|
|
335
|
+
# Threshold check.
|
|
336
|
+
# -----------------------------------------------------------------------------
|
|
337
|
+
summary=$(
|
|
338
|
+
python3 - "$COVERAGE_OUT" <<'PY'
|
|
339
|
+
import sys
|
|
340
|
+
total = covered = 0
|
|
341
|
+
with open(sys.argv[1]) as f:
|
|
342
|
+
for line in f:
|
|
343
|
+
if line.startswith("DA:"):
|
|
344
|
+
_, rest = line.split(":", 1)
|
|
345
|
+
_, hits = rest.strip().split(",")
|
|
346
|
+
total += 1
|
|
347
|
+
if int(hits) > 0:
|
|
348
|
+
covered += 1
|
|
349
|
+
pct = (covered * 100.0 / total) if total else 0.0
|
|
350
|
+
print(f"{covered} {total} {pct:.2f}")
|
|
351
|
+
PY
|
|
352
|
+
)
|
|
353
|
+
read -r covered total pct <<<"$summary"
|
|
354
|
+
echo "Coverage: ${covered}/${total} lines = ${pct}% (output: $COVERAGE_OUT)"
|
|
355
|
+
|
|
356
|
+
below=$(awk -v p="$pct" -v t="$MIN_COVERAGE_PCT" 'BEGIN{print (p+0 < t+0) ? "1" : "0"}')
|
|
357
|
+
if [[ "$below" == "1" ]]; then
|
|
358
|
+
echo "::error::coverage ${pct}% is below the floor ${MIN_COVERAGE_PCT}%" >&2
|
|
359
|
+
exit 1
|
|
360
|
+
fi
|
|
361
|
+
|
|
362
|
+
exit 0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Copyright (c) 2015-2026 Dotfiles. All rights reserved.
|
|
3
|
+
# Validate .chezmoidata.toml against config/chezmoidata.schema.json via taplo.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# bash scripts/ci/validate-chezmoidata.sh
|
|
7
|
+
#
|
|
8
|
+
# In CI the workflow installs taplo before invoking this script. Locally,
|
|
9
|
+
# install via: cargo install --locked taplo-cli (or `brew install taplo`).
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
14
|
+
cd "$REPO_ROOT"
|
|
15
|
+
|
|
16
|
+
if ! command -v taplo >/dev/null 2>&1; then
|
|
17
|
+
echo "error: taplo not found in PATH" >&2
|
|
18
|
+
echo " install: cargo install --locked taplo-cli" >&2
|
|
19
|
+
echo " or: brew install taplo" >&2
|
|
20
|
+
exit 127
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# `taplo check` reads .taplo.toml at the repo root, applies the schema
|
|
24
|
+
# rule, and reports schema violations + TOML syntax errors.
|
|
25
|
+
taplo check
|
|
@@ -387,6 +387,49 @@ else
|
|
|
387
387
|
_fail "dot" "not found in PATH"
|
|
388
388
|
fi
|
|
389
389
|
|
|
390
|
+
# --- Pre-push audit bypass log (closes #871) ---
|
|
391
|
+
_section "Pre-Push Audit Bypass"
|
|
392
|
+
|
|
393
|
+
bypass_log="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/audit-bypass.log"
|
|
394
|
+
if [[ -s "$bypass_log" ]]; then
|
|
395
|
+
# Count entries in the last 7 days (timestamps are ISO-8601 UTC).
|
|
396
|
+
seven_days_ago=$(date -u -v-7d +%Y-%m-%dT00:00:00Z 2>/dev/null ||
|
|
397
|
+
date -u -d '7 days ago' +%Y-%m-%dT00:00:00Z 2>/dev/null ||
|
|
398
|
+
echo "1970-01-01T00:00:00Z")
|
|
399
|
+
recent=$(awk -v cutoff="$seven_days_ago" '$1 >= cutoff' "$bypass_log" | wc -l | tr -d ' ')
|
|
400
|
+
if ((recent > 0)); then
|
|
401
|
+
_warn "audit bypass" "$recent push(es) bypassed in last 7 days — see $(pretty_path "$bypass_log")"
|
|
402
|
+
else
|
|
403
|
+
_ok "audit bypass" "log exists; no recent entries (last 7d)"
|
|
404
|
+
fi
|
|
405
|
+
else
|
|
406
|
+
_ok "audit bypass" "clean — no pre-push audits bypassed"
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
# --- Atuin history-filter (closes #872) ---
|
|
410
|
+
_section "Atuin History Filter"
|
|
411
|
+
|
|
412
|
+
atuin_cfg="$HOME/.config/atuin/config.toml"
|
|
413
|
+
if [[ ! -f "$atuin_cfg" ]]; then
|
|
414
|
+
_warn "atuin config" "not found at $(pretty_path "$atuin_cfg")"
|
|
415
|
+
else
|
|
416
|
+
if grep -Eq "^history_filter[[:space:]]*=" "$atuin_cfg"; then
|
|
417
|
+
pattern_count=$(awk '
|
|
418
|
+
/^history_filter[[:space:]]*=[[:space:]]*\[/ { inside = 1; next }
|
|
419
|
+
inside && /^\]/ { inside = 0 }
|
|
420
|
+
inside && /^[[:space:]]*"/ { count++ }
|
|
421
|
+
END { print count + 0 }
|
|
422
|
+
' "$atuin_cfg")
|
|
423
|
+
if ((pattern_count >= 10)); then
|
|
424
|
+
_ok "history_filter" "$pattern_count patterns (chezmoi-managed)"
|
|
425
|
+
else
|
|
426
|
+
_warn "history_filter" "$pattern_count patterns (expected ≥10 from secrets-patterns.toml)"
|
|
427
|
+
fi
|
|
428
|
+
else
|
|
429
|
+
_fail "history_filter" "no history_filter block in $(pretty_path "$atuin_cfg") — secrets may leak into shell history"
|
|
430
|
+
fi
|
|
431
|
+
fi
|
|
432
|
+
|
|
390
433
|
# --- Topgrade Integration ---
|
|
391
434
|
_section "Topgrade Integration"
|
|
392
435
|
|
|
@@ -472,10 +515,12 @@ fi
|
|
|
472
515
|
# --- Performance ---
|
|
473
516
|
_section "Performance"
|
|
474
517
|
|
|
475
|
-
|
|
518
|
+
cache_base="${XDG_CACHE_HOME:-$HOME/.cache}"
|
|
519
|
+
|
|
520
|
+
# 1. Shell cache freshness for tools the project already wraps in _cached_eval.
|
|
521
|
+
# Stale caches force runtime regeneration on next shell start.
|
|
476
522
|
stale_caches=0
|
|
477
523
|
stale_tools=""
|
|
478
|
-
cache_base="${XDG_CACHE_HOME:-$HOME/.cache}"
|
|
479
524
|
for tool in mise starship zoxide atuin fzf direnv; do
|
|
480
525
|
tool_bin="$(command -v "$tool" 2>/dev/null || true)"
|
|
481
526
|
[[ -n "$tool_bin" ]] || continue
|
|
@@ -486,22 +531,98 @@ for tool in mise starship zoxide atuin fzf direnv; do
|
|
|
486
531
|
esac
|
|
487
532
|
if [[ ! -f "$cache_file" ]] || [[ "$tool_bin" -nt "$cache_file" ]]; then
|
|
488
533
|
stale_caches=$((stale_caches + 1))
|
|
489
|
-
# Track unique tool names for the summary
|
|
490
534
|
case "$stale_tools" in
|
|
491
535
|
*"$tool"*) ;;
|
|
492
536
|
*) stale_tools="${stale_tools:+$stale_tools, }$tool" ;;
|
|
493
537
|
esac
|
|
494
|
-
break
|
|
538
|
+
break
|
|
495
539
|
fi
|
|
496
540
|
done
|
|
497
541
|
done
|
|
498
|
-
|
|
499
542
|
if [[ $stale_caches -eq 0 ]]; then
|
|
500
543
|
_ok "shell caches" "fresh"
|
|
501
544
|
else
|
|
502
545
|
_warn "shell caches" "stale ($stale_tools) — run dot prewarm"
|
|
503
546
|
fi
|
|
504
547
|
|
|
548
|
+
# 2. Slow-init tools that are present but NOT wrapped in _cached_eval.
|
|
549
|
+
# Each of these runs uncached on every shell start; common offenders eat
|
|
550
|
+
# 100-500ms apiece on a populated dev machine.
|
|
551
|
+
unwrapped=""
|
|
552
|
+
for tool in nvm fnm pyenv rbenv jenv asdf sdkman conda kubectl helm gh cargo pnpm yarn thefuck broot mcfly; do
|
|
553
|
+
command -v "$tool" >/dev/null 2>&1 || continue
|
|
554
|
+
found=0
|
|
555
|
+
for shell_dir in zsh bash fish; do
|
|
556
|
+
case "$shell_dir" in
|
|
557
|
+
fish) [[ -f "$cache_base/$shell_dir/${tool}-init.fish" ]] && found=1 ;;
|
|
558
|
+
*) [[ -f "$cache_base/$shell_dir/${tool}-init.$shell_dir" ]] && found=1 ;;
|
|
559
|
+
esac
|
|
560
|
+
((found == 1)) && break
|
|
561
|
+
done
|
|
562
|
+
((found == 0)) && unwrapped="${unwrapped:+$unwrapped, }$tool"
|
|
563
|
+
done
|
|
564
|
+
if [[ -z "$unwrapped" ]]; then
|
|
565
|
+
_ok "uncached slow-init tools" "none detected"
|
|
566
|
+
else
|
|
567
|
+
_warn "uncached slow-init tools" "$unwrapped — consider wrapping in _cached_eval"
|
|
568
|
+
fi
|
|
569
|
+
|
|
570
|
+
# 3. Zsh completion dump health. compinit is usually the single biggest
|
|
571
|
+
# cost on a zsh startup; a stale or uncompiled .zcompdump compounds it.
|
|
572
|
+
if command -v zsh >/dev/null 2>&1; then
|
|
573
|
+
zcompdump="${HOME}/.zcompdump"
|
|
574
|
+
if [[ -f "$zcompdump" ]]; then
|
|
575
|
+
dump_mtime=$(stat -c %Y "$zcompdump" 2>/dev/null || stat -f %m "$zcompdump" 2>/dev/null || echo 0)
|
|
576
|
+
age_days=$((($(date +%s) - dump_mtime) / 86400))
|
|
577
|
+
if ((age_days > 7)); then
|
|
578
|
+
_warn ".zcompdump" "${age_days}d old — refresh: rm ~/.zcompdump* && zsh -ic exit"
|
|
579
|
+
else
|
|
580
|
+
_ok ".zcompdump" "fresh (${age_days}d)"
|
|
581
|
+
fi
|
|
582
|
+
if [[ ! -f "${zcompdump}.zwc" ]]; then
|
|
583
|
+
_warn ".zcompdump.zwc" "missing — completion init slower than necessary"
|
|
584
|
+
fi
|
|
585
|
+
fi
|
|
586
|
+
fi
|
|
587
|
+
|
|
588
|
+
# 4. PATH length. Each entry is searched on every command resolution;
|
|
589
|
+
# >40 entries is noticeably slow on cold-cache filesystems.
|
|
590
|
+
path_count=$(printf '%s' "${PATH:-}" | tr ':' '\n' | grep -c . || true)
|
|
591
|
+
if [[ "$path_count" -le 40 ]]; then
|
|
592
|
+
_ok "PATH length" "$path_count entries"
|
|
593
|
+
elif [[ "$path_count" -le 80 ]]; then
|
|
594
|
+
_warn "PATH length" "$path_count entries — consider pruning"
|
|
595
|
+
else
|
|
596
|
+
_fail "PATH length" "$path_count entries — likely slowing every command"
|
|
597
|
+
fi
|
|
598
|
+
|
|
599
|
+
# 5. Shell coverage. Surface installed shells that the project's caching
|
|
600
|
+
# infrastructure (zsh/bash/fish) doesn't currently maintain caches for.
|
|
601
|
+
shells_unmanaged=""
|
|
602
|
+
for sh in nu pwsh; do
|
|
603
|
+
command -v "$sh" >/dev/null 2>&1 && shells_unmanaged="${shells_unmanaged:+$shells_unmanaged, }$sh"
|
|
604
|
+
done
|
|
605
|
+
if [[ -z "$shells_unmanaged" ]]; then
|
|
606
|
+
_ok "shell coverage" "all installed shells (zsh/bash/fish) are managed"
|
|
607
|
+
else
|
|
608
|
+
_warn "shell coverage" "$shells_unmanaged installed — startup not measured by dot perf"
|
|
609
|
+
fi
|
|
610
|
+
|
|
611
|
+
# 6. Zsh hook count. Heavy precmd/preexec functions compound per-prompt.
|
|
612
|
+
# Probe an interactive zsh with a hard timeout so a broken zshrc doesn't
|
|
613
|
+
# stall doctor; skip cleanly if the probe fails.
|
|
614
|
+
if command -v zsh >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1; then
|
|
615
|
+
hook_counts=$(timeout 5 zsh -i -c 'echo "$#precmd_functions $#preexec_functions"' 2>/dev/null || echo "")
|
|
616
|
+
if [[ -n "$hook_counts" ]]; then
|
|
617
|
+
read -r precmd_n preexec_n <<<"$hook_counts"
|
|
618
|
+
if [[ "${precmd_n:-0}" -le 5 && "${preexec_n:-0}" -le 5 ]]; then
|
|
619
|
+
_ok "zsh hooks" "precmd=$precmd_n preexec=$preexec_n"
|
|
620
|
+
else
|
|
621
|
+
_warn "zsh hooks" "precmd=$precmd_n preexec=$preexec_n — heavy per-prompt work"
|
|
622
|
+
fi
|
|
623
|
+
fi
|
|
624
|
+
fi
|
|
625
|
+
|
|
505
626
|
if command -v hyperfine >/dev/null 2>&1; then
|
|
506
627
|
if bash "$SCRIPT_DIR/../../tests/performance/bench.sh" 2>/dev/null; then
|
|
507
628
|
_ok "startup latency" "within target thresholds"
|
|
@@ -512,6 +633,53 @@ else
|
|
|
512
633
|
_warn "hyperfine" "missing (benchmark skipped)"
|
|
513
634
|
fi
|
|
514
635
|
|
|
636
|
+
# 7. Baseline check + top-3 slowest tools from EVALCACHE_TIMING.
|
|
637
|
+
# Closes part of #863. Reads the same baseline file `dot perf` writes,
|
|
638
|
+
# and the same eval-timings.jsonl _cached_eval populates. Skipped
|
|
639
|
+
# silently when either file is absent (first-run state).
|
|
640
|
+
baseline_file="$cache_base/dotfiles/perf-baseline.json"
|
|
641
|
+
if [[ -s "$baseline_file" ]] && command -v python3 >/dev/null 2>&1; then
|
|
642
|
+
baseline_age_days=$(python3 -c '
|
|
643
|
+
import json, sys, datetime
|
|
644
|
+
try:
|
|
645
|
+
d = json.load(open(sys.argv[1]))
|
|
646
|
+
rec = d.get("recorded_at", "")
|
|
647
|
+
if not rec: print(-1); sys.exit(0)
|
|
648
|
+
rec = rec.replace("Z", "+00:00")
|
|
649
|
+
age = (datetime.datetime.now(datetime.timezone.utc) - datetime.datetime.fromisoformat(rec)).days
|
|
650
|
+
print(age)
|
|
651
|
+
except Exception:
|
|
652
|
+
print(-1)
|
|
653
|
+
' "$baseline_file" 2>/dev/null)
|
|
654
|
+
if [[ "$baseline_age_days" -ge 0 ]]; then
|
|
655
|
+
_ok "perf baseline" "recorded ${baseline_age_days}d ago — run \`dot perf\` to compare"
|
|
656
|
+
fi
|
|
657
|
+
fi
|
|
658
|
+
|
|
659
|
+
timings_file="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/eval-timings.jsonl"
|
|
660
|
+
if [[ -s "$timings_file" ]] && command -v python3 >/dev/null 2>&1; then
|
|
661
|
+
top_tools=$(python3 -c '
|
|
662
|
+
import json, sys
|
|
663
|
+
from collections import defaultdict
|
|
664
|
+
samples = defaultdict(list)
|
|
665
|
+
try:
|
|
666
|
+
for line in open(sys.argv[1]):
|
|
667
|
+
try:
|
|
668
|
+
ev = json.loads(line)
|
|
669
|
+
ms = int(ev.get("ms", 0) or 0)
|
|
670
|
+
samples[ev.get("label", "?")].append(ms)
|
|
671
|
+
except Exception:
|
|
672
|
+
pass
|
|
673
|
+
rows = sorted(samples.items(), key=lambda kv: sum(kv[1]) // max(len(kv[1]), 1), reverse=True)[:3]
|
|
674
|
+
print(", ".join(f"{lbl}({sum(v)//max(len(v),1)}ms)" for lbl, v in rows))
|
|
675
|
+
except Exception:
|
|
676
|
+
pass
|
|
677
|
+
' "$timings_file" 2>/dev/null)
|
|
678
|
+
if [[ -n "$top_tools" ]]; then
|
|
679
|
+
_ok "perf top-tools" "$top_tools"
|
|
680
|
+
fi
|
|
681
|
+
fi
|
|
682
|
+
|
|
515
683
|
# --- Summary ---
|
|
516
684
|
dot_log info "doctor_complete" "errors=$Errors" "warnings=$Warnings"
|
|
517
685
|
dot_metric "doctor_errors" "$Errors" "count"
|