hstack 0.16.0 → 0.17.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 +37 -0
- package/README.md +73 -163
- package/VERSION +1 -1
- package/dist/commands/doctor.js +18 -8
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +7 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.js +9 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/descriptions.js +10 -17
- package/dist/lib/descriptions.js.map +1 -1
- package/dist/lib/wire.js +166 -138
- package/dist/lib/wire.js.map +1 -1
- package/dist/manifest.js +41 -29
- package/dist/manifest.js.map +1 -1
- package/package.json +3 -4
- package/template/.claude/agents/adversarial-reviewer.md +42 -50
- package/template/.claude/agents/app-architect.md +44 -96
- package/template/.claude/agents/data-architect.md +46 -89
- package/template/.claude/agents/test-strategist.md +39 -68
- package/template/.claude/skills/hstack-adr-new/SKILL.md +23 -73
- package/template/.claude/skills/hstack-adversarial-review/SKILL.md +46 -152
- package/template/.claude/skills/hstack-adversarial-review/references/finding-categories.md +80 -103
- package/template/.claude/skills/hstack-app-architecture/SKILL.md +43 -68
- package/template/.claude/skills/hstack-commit/SKILL.md +27 -68
- package/template/.claude/skills/hstack-data-architecture/SKILL.md +34 -56
- package/template/.claude/skills/hstack-promote/SKILL.md +55 -0
- package/template/.claude/skills/hstack-story/SKILL.md +58 -0
- package/template/.claude/skills/hstack-test-audit/SKILL.md +64 -0
- package/template/.claude/skills/hstack-wrap/SKILL.md +48 -0
- package/template/.claude/skills/hstack-wrap/references/pr-description.md +45 -0
- package/template/KERNEL.md +75 -318
- package/template/templates/adr.md +2 -18
- package/template/templates/app-architecture.md +38 -50
- package/template/templates/data-architecture.md +38 -89
- package/template/templates/infrastructure.md +54 -148
- package/template/templates/roadmap.md +6 -12
- package/template/templates/story.md +7 -13
- package/template/templates/tech-debt.md +2 -37
- package/template/templates/tech-stack.md +2 -12
- package/template/.claude/agents/data-specialist.md +0 -81
- package/template/.claude/agents/implementer.md +0 -97
- package/template/.claude/agents/kernel-fit-analyst.md +0 -129
- package/template/.claude/agents/planner.md +0 -75
- package/template/.claude/agents/product-discovery.md +0 -129
- package/template/.claude/agents/product-manager.md +0 -76
- package/template/.claude/agents/researcher.md +0 -87
- package/template/.claude/agents/security-reviewer.md +0 -88
- package/template/.claude/agents/spec-author.md +0 -89
- package/template/.claude/agents/stack-architect.md +0 -108
- package/template/.claude/agents/ui-ux-briefer.md +0 -75
- package/template/.claude/agents/verifier.md +0 -81
- package/template/.claude/skills/hstack-branch/SKILL.md +0 -76
- package/template/.claude/skills/hstack-brownfield-init/SKILL.md +0 -131
- package/template/.claude/skills/hstack-change-new/SKILL.md +0 -84
- package/template/.claude/skills/hstack-change-plan/SKILL.md +0 -108
- package/template/.claude/skills/hstack-configure/SKILL.md +0 -99
- package/template/.claude/skills/hstack-coord/SKILL.md +0 -95
- package/template/.claude/skills/hstack-data-review/SKILL.md +0 -93
- package/template/.claude/skills/hstack-finalize/SKILL.md +0 -140
- package/template/.claude/skills/hstack-flag/SKILL.md +0 -99
- package/template/.claude/skills/hstack-greenfield-init/SKILL.md +0 -148
- package/template/.claude/skills/hstack-help/SKILL.md +0 -153
- package/template/.claude/skills/hstack-implement/SKILL.md +0 -155
- package/template/.claude/skills/hstack-kernel-fit-promote/SKILL.md +0 -125
- package/template/.claude/skills/hstack-kernel-fit-scan/SKILL.md +0 -142
- package/template/.claude/skills/hstack-kernel-fit-scan/references/slack-setup.md +0 -42
- package/template/.claude/skills/hstack-kernel-fit-triage/SKILL.md +0 -121
- package/template/.claude/skills/hstack-module-spec/SKILL.md +0 -78
- package/template/.claude/skills/hstack-product-discovery/SKILL.md +0 -75
- package/template/.claude/skills/hstack-research/SKILL.md +0 -115
- package/template/.claude/skills/hstack-scaffold/SKILL.md +0 -113
- package/template/.claude/skills/hstack-security-review/SKILL.md +0 -95
- package/template/.claude/skills/hstack-ship/SKILL.md +0 -119
- package/template/.claude/skills/hstack-stack-decide/SKILL.md +0 -81
- package/template/.claude/skills/hstack-story-draft/SKILL.md +0 -92
- package/template/.claude/skills/hstack-tech-debt-new/SKILL.md +0 -95
- package/template/.claude/skills/hstack-tech-debt-resolve/SKILL.md +0 -123
- package/template/.claude/skills/hstack-tech-debt-stale/SKILL.md +0 -86
- package/template/.claude/skills/hstack-tech-debt-wontfix/SKILL.md +0 -77
- package/template/.claude/skills/hstack-telemetry/SKILL.md +0 -73
- package/template/.claude/skills/hstack-test-plan/SKILL.md +0 -159
- package/template/.claude/skills/hstack-ui-brief/SKILL.md +0 -81
- package/template/.claude/skills/hstack-verify/SKILL.md +0 -128
- package/template/scripts/compute-merge-readiness.mjs +0 -780
- package/template/scripts/coord/coord_scan.py +0 -681
- package/template/scripts/run-gates.sh +0 -388
- package/template/scripts/telemetry/__init__.py +0 -6
- package/template/scripts/telemetry/insights/__init__.py +0 -0
- package/template/scripts/telemetry/insights/contract_drift.py +0 -137
- package/template/scripts/telemetry/insights/kernel_fit.py +0 -438
- package/template/scripts/telemetry/insights/overengineering.py +0 -115
- package/template/scripts/telemetry/insights/quality_outcomes.py +0 -131
- package/template/scripts/telemetry/insights/token_economics.py +0 -302
- package/template/scripts/telemetry/insights/workflow_shape.py +0 -198
- package/template/scripts/telemetry/parsers/__init__.py +0 -0
- package/template/scripts/telemetry/parsers/bodies.py +0 -87
- package/template/scripts/telemetry/parsers/commits.py +0 -219
- package/template/scripts/telemetry/parsers/frontmatter.py +0 -322
- package/template/scripts/telemetry/parsers/sidecars.py +0 -61
- package/template/scripts/telemetry/parsers/transcripts.py +0 -294
- package/template/scripts/telemetry/render.py +0 -460
- package/template/scripts/telemetry/report.py +0 -150
- package/template/scripts/telemetry/run_kernel_fit.py +0 -95
- package/template/scripts/telemetry/session_id.py +0 -139
- package/template/scripts/validate-spec.mjs +0 -3303
- package/template/templates/adversarial-review.md +0 -54
- package/template/templates/bootstrap.md +0 -105
- package/template/templates/change-spec.md +0 -82
- package/template/templates/ci-cd.md +0 -41
- package/template/templates/coord-message.md +0 -56
- package/template/templates/data-review.md +0 -54
- package/template/templates/discovery/brainstorm.md +0 -69
- package/template/templates/discovery/forcing-questions.md +0 -76
- package/template/templates/discovery/project-brief.md +0 -53
- package/template/templates/figma-handoff.md +0 -38
- package/template/templates/glossary.md +0 -20
- package/template/templates/hardening-checklist.md +0 -73
- package/template/templates/incident-runbook.md +0 -57
- package/template/templates/kernel-fit-finding.md +0 -63
- package/template/templates/kernel-fit-flag.md +0 -52
- package/template/templates/module-spec.md +0 -49
- package/template/templates/persona.md +0 -38
- package/template/templates/plan.md +0 -53
- package/template/templates/product-brief.md +0 -56
- package/template/templates/security-review.md +0 -63
- package/template/templates/telemetry-sidecar.md +0 -227
- package/template/templates/test-plan.md +0 -119
- package/template/templates/threat-model.md +0 -54
- package/template/templates/ui-brief.md +0 -49
- package/template/templates/verification.md +0 -63
- package/template/templates/vision.md +0 -34
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# hstack gate runner — the verifier's machine hands, and the thing
|
|
4
|
-
# {{TODO-SCRIPT: hstack/scripts/run-gates.sh}} stood in for.
|
|
5
|
-
#
|
|
6
|
-
# hstack/scripts/run-gates.sh --change <change-id>
|
|
7
|
-
# hstack/scripts/run-gates.sh --change <id> --suite unit --suite lint
|
|
8
|
-
# hstack/scripts/run-gates.sh --list
|
|
9
|
-
# hstack/scripts/run-gates.sh --change <id> --json
|
|
10
|
-
#
|
|
11
|
-
# It reads the canonical commands declared in hstack/context/ci-cd.md, runs
|
|
12
|
-
# every one of them, captures combined stdout/stderr to the pointer file the
|
|
13
|
-
# verification artifact references, and emits an observed-test-count PER SUITE
|
|
14
|
-
# so V-05 ("a suite that executed zero tests cannot be recorded as pass") is a
|
|
15
|
-
# measurement rather than a paragraph of parsing instructions in a prompt.
|
|
16
|
-
#
|
|
17
|
-
# Exit codes:
|
|
18
|
-
# 0 every suite ran, exited 0, and every test suite observed > 0 tests
|
|
19
|
-
# 1 a suite failed, or a test suite observed zero tests (V-05)
|
|
20
|
-
# 2 usage / environment error (no ci-cd.md, no canonical-commands block)
|
|
21
|
-
#
|
|
22
|
-
# Dependency-free by construction: POSIX tools only, no jq, no node. The
|
|
23
|
-
# consuming repo has no node_modules for hstack — same constraint that made
|
|
24
|
-
# validate-spec.mjs plain ESM (ADR-0001).
|
|
25
|
-
|
|
26
|
-
set -uo pipefail
|
|
27
|
-
|
|
28
|
-
# ---------------------------------------------------------------------------
|
|
29
|
-
# 1. Argument parsing
|
|
30
|
-
# ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
CHANGE_ID=""
|
|
33
|
-
OUT=""
|
|
34
|
-
ROOT=""
|
|
35
|
-
JSON=0
|
|
36
|
-
LIST=0
|
|
37
|
-
SUITES_REQUESTED=""
|
|
38
|
-
|
|
39
|
-
usage() {
|
|
40
|
-
cat <<'EOF'
|
|
41
|
-
hstack run-gates — run the canonical test / lint / typecheck suites
|
|
42
|
-
|
|
43
|
-
hstack/scripts/run-gates.sh [options]
|
|
44
|
-
|
|
45
|
-
Options
|
|
46
|
-
--change ID change-spec id; the pointer file defaults to
|
|
47
|
-
hstack/specs/changes/<ID>/test-output.txt
|
|
48
|
-
--out PATH pointer file path (overrides --change)
|
|
49
|
-
--suite NAME run only this suite; repeatable
|
|
50
|
-
--list print the parsed canonical commands and exit
|
|
51
|
-
--json emit the per-suite summary as JSON on stdout
|
|
52
|
-
--root DIR repo root to resolve hstack/ from (default: search upward)
|
|
53
|
-
-h, --help this text
|
|
54
|
-
|
|
55
|
-
Exit codes: 0 all green with a non-zero test count per test suite,
|
|
56
|
-
1 a suite failed or observed zero tests (V-05), 2 usage / environment error.
|
|
57
|
-
EOF
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
while [ $# -gt 0 ]; do
|
|
61
|
-
case "$1" in
|
|
62
|
-
--change) CHANGE_ID="${2:-}"; shift 2 ;;
|
|
63
|
-
--out) OUT="${2:-}"; shift 2 ;;
|
|
64
|
-
--suite) SUITES_REQUESTED="$SUITES_REQUESTED ${2:-}"; shift 2 ;;
|
|
65
|
-
--root) ROOT="${2:-}"; shift 2 ;;
|
|
66
|
-
--json) JSON=1; shift ;;
|
|
67
|
-
--list) LIST=1; shift ;;
|
|
68
|
-
-h|--help) usage; exit 0 ;;
|
|
69
|
-
*) echo "run-gates: unknown option $1" >&2; usage >&2; exit 2 ;;
|
|
70
|
-
esac
|
|
71
|
-
done
|
|
72
|
-
|
|
73
|
-
# ---------------------------------------------------------------------------
|
|
74
|
-
# 2. Locate the hstack tree
|
|
75
|
-
# ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
find_hstack_root() {
|
|
78
|
-
dir="${1:-$PWD}"
|
|
79
|
-
dir=$(cd "$dir" 2>/dev/null && pwd) || return 1
|
|
80
|
-
while :; do
|
|
81
|
-
if [ -f "$dir/hstack/KERNEL.md" ] || [ -f "$dir/hstack/CLAUDE.md" ] || [ -f "$dir/hstack/config.yaml" ]; then
|
|
82
|
-
printf '%s\n' "$dir"
|
|
83
|
-
return 0
|
|
84
|
-
fi
|
|
85
|
-
parent=$(dirname "$dir")
|
|
86
|
-
[ "$parent" = "$dir" ] && return 1
|
|
87
|
-
dir="$parent"
|
|
88
|
-
done
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
REPO_ROOT=$(find_hstack_root "${ROOT:-$PWD}") || {
|
|
92
|
-
echo "run-gates: no hstack/ tree found (looked for hstack/KERNEL.md upward from ${ROOT:-$PWD}). Pass --root <repo>." >&2
|
|
93
|
-
exit 2
|
|
94
|
-
}
|
|
95
|
-
HSTACK="$REPO_ROOT/hstack"
|
|
96
|
-
CI_CD="$HSTACK/context/ci-cd.md"
|
|
97
|
-
|
|
98
|
-
[ -f "$CI_CD" ] || {
|
|
99
|
-
echo "run-gates: $CI_CD not found. The canonical commands live there; run \`/hstack:configure --interview ci-cd\` first." >&2
|
|
100
|
-
exit 2
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
# ---------------------------------------------------------------------------
|
|
104
|
-
# 3. Parse the canonical commands
|
|
105
|
-
# ---------------------------------------------------------------------------
|
|
106
|
-
#
|
|
107
|
-
# ci-cd.md declares them in a fenced block with the info string `hstack-gates`,
|
|
108
|
-
# one `suite: command` pair per line. The fence is the contract: everything
|
|
109
|
-
# else in ci-cd.md is prose written for humans, and a runner that guessed at
|
|
110
|
-
# prose would produce a confident wrong answer about what the repo's tests are.
|
|
111
|
-
|
|
112
|
-
CANONICAL=$(awk '
|
|
113
|
-
/^```[[:space:]]*hstack-gates[[:space:]]*$/ { inblock=1; next }
|
|
114
|
-
inblock && /^```/ { inblock=0; next }
|
|
115
|
-
inblock {
|
|
116
|
-
line=$0
|
|
117
|
-
sub(/#.*$/, "", line) # trailing comment
|
|
118
|
-
if (line ~ /^[[:space:]]*$/) next
|
|
119
|
-
idx = index(line, ":")
|
|
120
|
-
if (idx == 0) next
|
|
121
|
-
key = substr(line, 1, idx-1)
|
|
122
|
-
val = substr(line, idx+1)
|
|
123
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
|
|
124
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", val)
|
|
125
|
-
if (val == "" || val == "none" || val == "null") next # declared absent
|
|
126
|
-
printf "%s\t%s\n", key, val
|
|
127
|
-
}
|
|
128
|
-
' "$CI_CD")
|
|
129
|
-
|
|
130
|
-
if [ -z "$CANONICAL" ]; then
|
|
131
|
-
echo "run-gates: no \`hstack-gates\` fenced block in hstack/context/ci-cd.md." >&2
|
|
132
|
-
echo " Declare the canonical commands there — see hstack/templates/ci-cd.md § Canonical Commands." >&2
|
|
133
|
-
exit 2
|
|
134
|
-
fi
|
|
135
|
-
|
|
136
|
-
# Suites that are evidence of behaviour, and therefore subject to V-05. Lint
|
|
137
|
-
# and typecheck are exempt: both produce a diagnostic count whose floor is
|
|
138
|
-
# naturally zero on a clean repo, so zero is not a signal of a skipped run.
|
|
139
|
-
is_test_suite() {
|
|
140
|
-
case "$1" in
|
|
141
|
-
unit|integration|e2e) return 0 ;;
|
|
142
|
-
*) return 1 ;;
|
|
143
|
-
esac
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
wanted() {
|
|
147
|
-
[ -z "$SUITES_REQUESTED" ] && return 0
|
|
148
|
-
for s in $SUITES_REQUESTED; do [ "$s" = "$1" ] && return 0; done
|
|
149
|
-
return 1
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if [ "$LIST" -eq 1 ]; then
|
|
153
|
-
echo "hstack run-gates — canonical commands from hstack/context/ci-cd.md"
|
|
154
|
-
echo ""
|
|
155
|
-
printf '%s\n' "$CANONICAL" | while IFS="$(printf '\t')" read -r suite cmd; do
|
|
156
|
-
printf ' %-12s %s\n' "$suite" "$cmd"
|
|
157
|
-
done
|
|
158
|
-
exit 0
|
|
159
|
-
fi
|
|
160
|
-
|
|
161
|
-
# ---------------------------------------------------------------------------
|
|
162
|
-
# 4. Pointer file
|
|
163
|
-
# ---------------------------------------------------------------------------
|
|
164
|
-
|
|
165
|
-
if [ -z "$OUT" ]; then
|
|
166
|
-
if [ -n "$CHANGE_ID" ]; then
|
|
167
|
-
OUT="$HSTACK/specs/changes/$CHANGE_ID/test-output.txt"
|
|
168
|
-
[ -d "$HSTACK/specs/changes/$CHANGE_ID" ] || {
|
|
169
|
-
echo "run-gates: no change folder at hstack/specs/changes/$CHANGE_ID/" >&2
|
|
170
|
-
exit 2
|
|
171
|
-
}
|
|
172
|
-
else
|
|
173
|
-
OUT="$REPO_ROOT/hstack-gates-output.txt"
|
|
174
|
-
fi
|
|
175
|
-
fi
|
|
176
|
-
mkdir -p "$(dirname "$OUT")" || exit 2
|
|
177
|
-
: > "$OUT" || { echo "run-gates: cannot write $OUT" >&2; exit 2; }
|
|
178
|
-
|
|
179
|
-
{
|
|
180
|
-
echo "hstack run-gates"
|
|
181
|
-
echo "repo: $REPO_ROOT"
|
|
182
|
-
echo "source: hstack/context/ci-cd.md"
|
|
183
|
-
echo "====================================================================="
|
|
184
|
-
} >> "$OUT"
|
|
185
|
-
|
|
186
|
-
# ---------------------------------------------------------------------------
|
|
187
|
-
# 5. Observed-test-count extraction
|
|
188
|
-
# ---------------------------------------------------------------------------
|
|
189
|
-
#
|
|
190
|
-
# One awk pass per suite over that suite's captured output. The runners hstack
|
|
191
|
-
# meets in practice all print a summary line; the point is not to understand
|
|
192
|
-
# every runner, it is to answer one question honestly: did this suite execute
|
|
193
|
-
# anything? When no known pattern matches, the answer is "unknown" — and
|
|
194
|
-
# unknown is treated as zero, because a count nobody could read is not evidence.
|
|
195
|
-
#
|
|
196
|
-
# "Executed" is passed + failed, NOT total. A run that collected fifteen tests
|
|
197
|
-
# and skipped all fifteen executed nothing, and V-05 exists precisely for that
|
|
198
|
-
# case: `Tests: 15 skipped, 15 total` is a non-zero total with zero assertions.
|
|
199
|
-
#
|
|
200
|
-
# Jest / Vitest Tests: 12 passed, 3 skipped, 15 total | No tests found
|
|
201
|
-
# Mocha 12 passing / 3 pending / 1 failing
|
|
202
|
-
# Playwright 12 passed (4.2s) / 1 failed / 3 skipped
|
|
203
|
-
# pytest collected 15 items | 12 passed, 3 skipped | no tests ran
|
|
204
|
-
# go test ok pkg 0.4s | testing: warning: no tests to run
|
|
205
|
-
|
|
206
|
-
count_tests() {
|
|
207
|
-
# $1 = file holding this suite's output
|
|
208
|
-
awk '
|
|
209
|
-
function num(s) { return s + 0 }
|
|
210
|
-
# --- explicit zero-collection statements, strongest signal ----------------
|
|
211
|
-
/[Nn]o tests found/ { zero=1 }
|
|
212
|
-
/collected 0 items/ { zero=1; seen=1 }
|
|
213
|
-
/no tests ran/ { zero=1; seen=1 }
|
|
214
|
-
/no tests to run/ { zero=1 }
|
|
215
|
-
/^[[:space:]]*Test Files[[:space:]]+no tests/ { zero=1 }
|
|
216
|
-
|
|
217
|
-
# --- Jest / Vitest summary ----------------------------------------------
|
|
218
|
-
/^[[:space:]]*Tests:?[[:space:]]/ {
|
|
219
|
-
seen=1
|
|
220
|
-
line=$0
|
|
221
|
-
if (match(line, /[0-9]+ passed/)) { s=substr(line, RSTART, RLENGTH); passed=num(s) }
|
|
222
|
-
if (match(line, /[0-9]+ failed/)) { s=substr(line, RSTART, RLENGTH); failed=num(s) }
|
|
223
|
-
if (match(line, /[0-9]+ skipped/)) { s=substr(line, RSTART, RLENGTH); skipped=num(s) }
|
|
224
|
-
if (match(line, /[0-9]+ todo/)) { s=substr(line, RSTART, RLENGTH); skipped+=num(s) }
|
|
225
|
-
if (match(line, /[0-9]+ total/)) { s=substr(line, RSTART, RLENGTH); total=num(s) }
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
# --- pytest short summary ------------------------------------------------
|
|
229
|
-
/=+ .*(passed|failed|error|skipped).* =+/ {
|
|
230
|
-
seen=1
|
|
231
|
-
line=$0
|
|
232
|
-
if (match(line, /[0-9]+ passed/)) { s=substr(line, RSTART, RLENGTH); passed=num(s) }
|
|
233
|
-
if (match(line, /[0-9]+ failed/)) { s=substr(line, RSTART, RLENGTH); failed=num(s) }
|
|
234
|
-
if (match(line, /[0-9]+ error/)) { s=substr(line, RSTART, RLENGTH); failed+=num(s) }
|
|
235
|
-
if (match(line, /[0-9]+ skipped/)) { s=substr(line, RSTART, RLENGTH); skipped=num(s) }
|
|
236
|
-
}
|
|
237
|
-
/collected [0-9]+ item/ {
|
|
238
|
-
seen=1
|
|
239
|
-
if (match($0, /collected [0-9]+/)) { s=substr($0, RSTART+10, RLENGTH-10); collected=num(s) }
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
# --- Mocha ---------------------------------------------------------------
|
|
243
|
-
/^[[:space:]]*[0-9]+ passing/ { seen=1; if (match($0, /[0-9]+/)) { passed=num(substr($0, RSTART, RLENGTH)) } }
|
|
244
|
-
/^[[:space:]]*[0-9]+ pending/ { seen=1; if (match($0, /[0-9]+/)) { skipped=num(substr($0, RSTART, RLENGTH)) } }
|
|
245
|
-
/^[[:space:]]*[0-9]+ failing/ { seen=1; if (match($0, /[0-9]+/)) { failed=num(substr($0, RSTART, RLENGTH)) } }
|
|
246
|
-
|
|
247
|
-
# --- Playwright ("12 passed (4.2s)") and bare runner tallies -------------
|
|
248
|
-
# No \b here: POSIX awk reads it as a backspace, not a word boundary.
|
|
249
|
-
/^[[:space:]]*[0-9]+ (passed|failed|skipped|flaky)([^a-z]|$)/ {
|
|
250
|
-
seen=1
|
|
251
|
-
line=$0
|
|
252
|
-
if (match(line, /[0-9]+ passed/)) { s=substr(line, RSTART, RLENGTH); passed=num(s) }
|
|
253
|
-
if (match(line, /[0-9]+ failed/)) { s=substr(line, RSTART, RLENGTH); failed=num(s) }
|
|
254
|
-
if (match(line, /[0-9]+ skipped/)) { s=substr(line, RSTART, RLENGTH); skipped=num(s) }
|
|
255
|
-
if (match(line, /[0-9]+ flaky/)) { s=substr(line, RSTART, RLENGTH); passed+=num(s) }
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
END {
|
|
259
|
-
if (total == 0) total = passed + failed + skipped
|
|
260
|
-
if (total == 0 && collected > 0) { total = collected }
|
|
261
|
-
if (zero) { total = 0; passed = 0; failed = 0 }
|
|
262
|
-
executed = passed + failed
|
|
263
|
-
# `known` says whether any pattern matched at all. An unreadable summary
|
|
264
|
-
# is reported as unknown and treated as zero downstream — a count nobody
|
|
265
|
-
# could read is not evidence that tests ran.
|
|
266
|
-
known = (seen || zero) ? 1 : 0
|
|
267
|
-
printf "%d %d %d %d %d %d\n", passed+0, failed+0, skipped+0, total+0, executed+0, known
|
|
268
|
-
}
|
|
269
|
-
' "$1"
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
# ---------------------------------------------------------------------------
|
|
273
|
-
# 6. Run
|
|
274
|
-
# ---------------------------------------------------------------------------
|
|
275
|
-
|
|
276
|
-
TMPDIR_RUN=$(mktemp -d "${TMPDIR:-/tmp}/hstack-run-gates.XXXXXX") || exit 2
|
|
277
|
-
trap 'rm -rf "$TMPDIR_RUN"' EXIT
|
|
278
|
-
|
|
279
|
-
SUMMARY="$TMPDIR_RUN/summary"
|
|
280
|
-
: > "$SUMMARY"
|
|
281
|
-
OVERALL=0
|
|
282
|
-
RAN_ANY=0
|
|
283
|
-
|
|
284
|
-
while IFS="$(printf '\t')" read -r suite cmd; do
|
|
285
|
-
[ -n "$suite" ] || continue
|
|
286
|
-
wanted "$suite" || continue
|
|
287
|
-
RAN_ANY=1
|
|
288
|
-
|
|
289
|
-
suite_out="$TMPDIR_RUN/$suite.out"
|
|
290
|
-
{
|
|
291
|
-
echo ""
|
|
292
|
-
echo "--- suite: $suite ------------------------------------------------"
|
|
293
|
-
echo "\$ $cmd"
|
|
294
|
-
} >> "$OUT"
|
|
295
|
-
|
|
296
|
-
# stdin from /dev/null, not inherited: the loop below is fed by a heredoc of
|
|
297
|
-
# the canonical commands, and a suite that reads stdin (an interactive watch
|
|
298
|
-
# mode, a prompt) would otherwise eat the remaining suites.
|
|
299
|
-
( cd "$REPO_ROOT" && eval "$cmd" ) > "$suite_out" 2>&1 < /dev/null
|
|
300
|
-
code=$?
|
|
301
|
-
cat "$suite_out" >> "$OUT"
|
|
302
|
-
|
|
303
|
-
if is_test_suite "$suite"; then
|
|
304
|
-
read -r passed failed skipped total executed known <<EOF
|
|
305
|
-
$(count_tests "$suite_out")
|
|
306
|
-
EOF
|
|
307
|
-
else
|
|
308
|
-
passed=0; failed=0; skipped=0; total=0; executed=0; known=1
|
|
309
|
-
fi
|
|
310
|
-
|
|
311
|
-
# V-05: zero executed tests is `not-run`, never `pass`. "Zero failures" is
|
|
312
|
-
# not evidence of correctness when there were zero assertions to fail.
|
|
313
|
-
if [ "$code" -ne 0 ]; then
|
|
314
|
-
verdict="fail"
|
|
315
|
-
reason="command exited $code"
|
|
316
|
-
OVERALL=1
|
|
317
|
-
elif is_test_suite "$suite" && [ "$executed" -eq 0 ]; then
|
|
318
|
-
verdict="not-run"
|
|
319
|
-
if [ "$known" -eq 0 ]; then
|
|
320
|
-
reason="no test count could be read from the runner's output (unrecognised summary format)"
|
|
321
|
-
elif [ "$skipped" -gt 0 ]; then
|
|
322
|
-
reason="the runner reported zero executed tests — $skipped skipped of $total collected (all-skipped, or a filter that collapsed the set)"
|
|
323
|
-
else
|
|
324
|
-
reason="the runner reported zero executed tests (env-gated, empty-collection, or filter-collapse)"
|
|
325
|
-
fi
|
|
326
|
-
OVERALL=1
|
|
327
|
-
else
|
|
328
|
-
verdict="pass"
|
|
329
|
-
reason=""
|
|
330
|
-
fi
|
|
331
|
-
|
|
332
|
-
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
333
|
-
"$suite" "$cmd" "$code" "$verdict" "$passed" "$failed" "$skipped" "$total" "$executed" "$reason" >> "$SUMMARY"
|
|
334
|
-
|
|
335
|
-
{
|
|
336
|
-
echo "--- suite: $suite → $verdict (exit $code, $executed of $total test(s) executed)"
|
|
337
|
-
} >> "$OUT"
|
|
338
|
-
done <<EOF
|
|
339
|
-
$CANONICAL
|
|
340
|
-
EOF
|
|
341
|
-
|
|
342
|
-
if [ "$RAN_ANY" -eq 0 ]; then
|
|
343
|
-
echo "run-gates: no suite matched --suite${SUITES_REQUESTED}" >&2
|
|
344
|
-
exit 2
|
|
345
|
-
fi
|
|
346
|
-
|
|
347
|
-
# ---------------------------------------------------------------------------
|
|
348
|
-
# 7. Report
|
|
349
|
-
# ---------------------------------------------------------------------------
|
|
350
|
-
|
|
351
|
-
REL_OUT="${OUT#"$REPO_ROOT"/}"
|
|
352
|
-
|
|
353
|
-
if [ "$JSON" -eq 1 ]; then
|
|
354
|
-
printf '{\n'
|
|
355
|
-
printf ' "ok": %s,\n' "$([ "$OVERALL" -eq 0 ] && echo true || echo false)"
|
|
356
|
-
printf ' "test-output": "%s",\n' "$REL_OUT"
|
|
357
|
-
printf ' "suites": [\n'
|
|
358
|
-
first=1
|
|
359
|
-
while IFS="$(printf '\t')" read -r suite cmd code verdict passed failed skipped total executed reason; do
|
|
360
|
-
[ "$first" -eq 1 ] || printf ',\n'
|
|
361
|
-
first=0
|
|
362
|
-
esc_cmd=$(printf '%s' "$cmd" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
363
|
-
esc_reason=$(printf '%s' "$reason" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
364
|
-
printf ' {"suite": "%s", "command": "%s", "exit": %s, "verdict": "%s", "observed": {"passed": %s, "failed": %s, "skipped": %s, "total": %s, "executed": %s}, "reason": "%s"}' \
|
|
365
|
-
"$suite" "$esc_cmd" "$code" "$verdict" "$passed" "$failed" "$skipped" "$total" "$executed" "$esc_reason"
|
|
366
|
-
done < "$SUMMARY"
|
|
367
|
-
printf '\n ]\n}\n'
|
|
368
|
-
else
|
|
369
|
-
echo ""
|
|
370
|
-
echo "hstack run-gates — $REPO_ROOT"
|
|
371
|
-
echo ""
|
|
372
|
-
printf ' %-12s %-9s %-6s %s\n' "suite" "verdict" "exit" "observed (passed/failed/skipped/total)"
|
|
373
|
-
while IFS="$(printf '\t')" read -r suite cmd code verdict passed failed skipped total executed reason; do
|
|
374
|
-
printf ' %-12s %-9s %-6s %s/%s/%s/%s\n' "$suite" "$verdict" "$code" "$passed" "$failed" "$skipped" "$total"
|
|
375
|
-
[ -n "$reason" ] && printf ' %s\n' "$reason"
|
|
376
|
-
done < "$SUMMARY"
|
|
377
|
-
echo ""
|
|
378
|
-
echo " captured output: $REL_OUT"
|
|
379
|
-
echo " → verification.artifacts.test-output: $REL_OUT"
|
|
380
|
-
echo ""
|
|
381
|
-
if [ "$OVERALL" -eq 0 ]; then
|
|
382
|
-
echo "run-gates: all suites green with a non-zero executed-test count."
|
|
383
|
-
else
|
|
384
|
-
echo "run-gates: at least one suite failed or executed zero tests (V-05). status: passed is blocked."
|
|
385
|
-
fi
|
|
386
|
-
fi
|
|
387
|
-
|
|
388
|
-
exit "$OVERALL"
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
"""hstack-telemetry — retrospective observability for the hstack workflow.
|
|
2
|
-
|
|
3
|
-
Reads on-disk artifacts (frontmatter + bodies), git history, and Claude Code
|
|
4
|
-
transcripts to compute a small set of metrics that drive the hstack
|
|
5
|
-
learning/improvement loop. Pure derivation; never the authoritative source.
|
|
6
|
-
"""
|
|
File without changes
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
"""Contract-drift insights: module-spec staleness, ADR supersession lag,
|
|
2
|
-
tech-debt half-life by exit path."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
from collections import defaultdict
|
|
7
|
-
from datetime import date, datetime
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def compute(commits: list[dict], changes: dict, tech_debt: list[dict],
|
|
11
|
-
adrs: list[dict], module_specs: list[dict]) -> dict:
|
|
12
|
-
return {
|
|
13
|
-
"module_spec_staleness": _module_staleness(module_specs, commits),
|
|
14
|
-
"adr_supersession_lag": _adr_lag(adrs),
|
|
15
|
-
"tech_debt_half_life": _td_half_life(tech_debt),
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _parse_date(value) -> date | None:
|
|
20
|
-
if value is None:
|
|
21
|
-
return None
|
|
22
|
-
if isinstance(value, date):
|
|
23
|
-
return value
|
|
24
|
-
if isinstance(value, datetime):
|
|
25
|
-
return value.date()
|
|
26
|
-
if isinstance(value, str):
|
|
27
|
-
try:
|
|
28
|
-
return datetime.fromisoformat(value).date()
|
|
29
|
-
except ValueError:
|
|
30
|
-
return None
|
|
31
|
-
return None
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _module_staleness(module_specs: list[dict], commits: list[dict]) -> dict:
|
|
35
|
-
"""Per module: spec status + count of recent commits touching that module's
|
|
36
|
-
files. A `needs-refresh` module with high recent activity is the drift
|
|
37
|
-
pathology."""
|
|
38
|
-
# Count commits touching each module dir.
|
|
39
|
-
module_activity: dict[str, int] = defaultdict(int)
|
|
40
|
-
for c in commits:
|
|
41
|
-
for f in c.get("files", []):
|
|
42
|
-
for ms in module_specs:
|
|
43
|
-
module = ms["module"]
|
|
44
|
-
# Use a heuristic — count when a commit touches files outside
|
|
45
|
-
# hstack/ in directories named like the module. Imperfect, but
|
|
46
|
-
# avoids requiring a module→file-path map.
|
|
47
|
-
if module in f and not f.startswith("hstack/"):
|
|
48
|
-
module_activity[module] += 1
|
|
49
|
-
break
|
|
50
|
-
rows = []
|
|
51
|
-
for ms in module_specs:
|
|
52
|
-
status = ms["fm"].get("status", "-")
|
|
53
|
-
updated = _parse_date(ms["fm"].get("updated"))
|
|
54
|
-
rows.append({
|
|
55
|
-
"module": ms["module"],
|
|
56
|
-
"status": status,
|
|
57
|
-
"updated": str(updated) if updated else "-",
|
|
58
|
-
"recent_commits_touching_module": module_activity.get(ms["module"], 0),
|
|
59
|
-
"drift_flag": status == "needs-refresh" and module_activity.get(ms["module"], 0) > 0,
|
|
60
|
-
})
|
|
61
|
-
rows.sort(key=lambda r: -r["recent_commits_touching_module"])
|
|
62
|
-
return {"rows": rows}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _adr_lag(adrs: list[dict]) -> dict:
|
|
66
|
-
"""Among ADRs at `superseded` status, how long they lived before the
|
|
67
|
-
superseding ADR landed."""
|
|
68
|
-
by_id = {a["fm"].get("id") or a["path"].stem: a for a in adrs}
|
|
69
|
-
lags = []
|
|
70
|
-
for a in adrs:
|
|
71
|
-
status = a["fm"].get("status")
|
|
72
|
-
if status != "superseded":
|
|
73
|
-
continue
|
|
74
|
-
sup = a["fm"].get("superseded-by")
|
|
75
|
-
a_created = _parse_date(a["fm"].get("created"))
|
|
76
|
-
if not sup or not a_created:
|
|
77
|
-
continue
|
|
78
|
-
sup_adr = by_id.get(sup)
|
|
79
|
-
if not sup_adr:
|
|
80
|
-
continue
|
|
81
|
-
sup_created = _parse_date(sup_adr["fm"].get("created"))
|
|
82
|
-
if not sup_created:
|
|
83
|
-
continue
|
|
84
|
-
days = (sup_created - a_created).days
|
|
85
|
-
lags.append({
|
|
86
|
-
"adr": a["fm"].get("id"),
|
|
87
|
-
"superseded_by": sup,
|
|
88
|
-
"lag_days": days,
|
|
89
|
-
})
|
|
90
|
-
return {"rows": lags}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _td_half_life(tech_debt: list[dict]) -> dict:
|
|
94
|
-
"""For each TD, compute days from created → exit (resolved | wontfix |
|
|
95
|
-
stale-no-longer-reproducible). Surface by exit-path histogram + per-TD
|
|
96
|
-
rows."""
|
|
97
|
-
today = date.today()
|
|
98
|
-
by_exit = defaultdict(list)
|
|
99
|
-
rows = []
|
|
100
|
-
for td in tech_debt:
|
|
101
|
-
fm = td["fm"]
|
|
102
|
-
status = fm.get("status")
|
|
103
|
-
created = _parse_date(fm.get("created"))
|
|
104
|
-
if not created:
|
|
105
|
-
continue
|
|
106
|
-
exit_date = None
|
|
107
|
-
if status == "resolved":
|
|
108
|
-
exit_date = _parse_date(fm.get("updated"))
|
|
109
|
-
elif status == "wontfix":
|
|
110
|
-
exit_date = _parse_date(fm.get("updated"))
|
|
111
|
-
elif status == "stale-no-longer-reproducible":
|
|
112
|
-
exit_date = _parse_date(fm.get("stale-verified-at") or fm.get("updated"))
|
|
113
|
-
if exit_date:
|
|
114
|
-
days = (exit_date - created).days
|
|
115
|
-
by_exit[status].append(days)
|
|
116
|
-
elif status == "open" or status == "in-progress":
|
|
117
|
-
days = (today - created).days
|
|
118
|
-
by_exit[f"{status} (still open)"].append(days)
|
|
119
|
-
exit_date = None
|
|
120
|
-
rows.append({
|
|
121
|
-
"id": fm.get("id"),
|
|
122
|
-
"status": status,
|
|
123
|
-
"created": str(created),
|
|
124
|
-
"days_to_exit": (exit_date - created).days if exit_date else (today - created).days,
|
|
125
|
-
"exit_path": status if exit_date else None,
|
|
126
|
-
})
|
|
127
|
-
summary = {}
|
|
128
|
-
for exit_path, days_list in by_exit.items():
|
|
129
|
-
if not days_list:
|
|
130
|
-
continue
|
|
131
|
-
days_list.sort()
|
|
132
|
-
summary[exit_path] = {
|
|
133
|
-
"count": len(days_list),
|
|
134
|
-
"median_days": days_list[len(days_list) // 2],
|
|
135
|
-
"max_days": max(days_list),
|
|
136
|
-
}
|
|
137
|
-
return {"summary": summary, "rows": rows}
|