The gesture book is generated from GESTURE tags, so it could not describe a gesture nobody tagged, but nothing made anyone tag one. The arrow keys, Enter, P, X, U, Delete, F1 and F2 all worked in the grid with no line in the help sheet, and a tag could name a key whose handler had gone. Key handlers now compare one canonical string, Keys.chord(event) == "Ctrl+Z", instead of reading event.text and the modifiers themselves. keys.slint folds the key and its modifiers into that spelling, so the literal in the handler is the whole binding and the checker reads exactly what the handler dispatches on. Each handler carries a KEYMAP comment naming the gesture-book section its keys belong to, and a tag's keys field names its keys between backticks. gestures-check now fails when a handler binds a key no tag in that section names, when a tag names a key no handler there binds, when any .slint file other than keys.slint reads event.text, when a compared literal is not canonical, and when keys.slint's named keys drift from the Rust list. Spellings are normalised in one place, chord.rs: Ctrl+z, Control+Z and LeftArrow all mean what the handler's "Ctrl+Z" and "Left" mean. Shift and Alt count only for letters and named keys, because on the French layout every digit needs shift and a 6 has to be a 6 however it was typed. A Rust keymap that both dispatched and was read by the generator was the alternative. It would have moved the handlers' decisions away from the Slint state they depend on, and a window that forgot to install it would have had no working keys at all. The keys that were already bound and undocumented are now tagged.
140 lines
5.6 KiB
YAML
140 lines
5.6 KiB
YAML
name: Traceability
|
|
|
|
# Mirrors JellyTau's traceability gate, including the reason it exists.
|
|
#
|
|
# That gate divided a traced count by frozen literal denominators while the
|
|
# requirements file grew past them, reported 158% coverage, and so could never
|
|
# fail its own threshold. Two rules follow, and the extractor's own tests
|
|
# enforce both:
|
|
#
|
|
# 1. Denominators are parsed from docs/dev/requirements.md at run time.
|
|
# 2. Coverage is |traced ∩ defined| / |defined|, never a raw traced count.
|
|
#
|
|
# This job is static analysis of source comments plus markdown parsing, so it
|
|
# needs no GPU and no Android SDK — only the Rust toolchain.
|
|
|
|
on:
|
|
push:
|
|
branches: [main, master, develop]
|
|
pull_request:
|
|
branches: [main, master, develop]
|
|
|
|
jobs:
|
|
traceability:
|
|
runs-on: linux/amd64
|
|
name: Requirement traces
|
|
# Node for actions/checkout and actions/cache, which the bare runner image
|
|
# cannot execute. Rust is installed below.
|
|
container:
|
|
image: catthehacker/ubuntu:act-latest
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Cache cargo
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.cargo/registry
|
|
~/.cargo/git
|
|
target
|
|
key: traces-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
|
|
|
# Source-comment and markdown parsing only, so the minimal profile is
|
|
# enough — no system libraries and nothing this job itself needs beyond
|
|
# cargo. rust-analyzer is here anyway because rust-toolchain.toml lists
|
|
# it: rustup installs that file's components on the first cargo call in
|
|
# the work tree regardless, and a download named in the install step
|
|
# beats the same download appearing unannounced inside the gate.
|
|
- name: Install Rust 1.92.0
|
|
run: |
|
|
set -e
|
|
curl -fsSL https://sh.rustup.rs | sh -s -- \
|
|
-y --no-modify-path --profile minimal --default-toolchain 1.92.0 \
|
|
--component rust-analyzer
|
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
|
|
# The gate's own arithmetic is the thing being trusted, so its tests run
|
|
# before it does. Untested gate logic is exactly how JellyTau's 158% went
|
|
# unnoticed for months.
|
|
- name: Test the extractor
|
|
run: cargo test -p traceability
|
|
|
|
# Structural failures are unconditional and do not depend on the coverage
|
|
# threshold: zero requirements parsed, zero files scanned, a ratio above
|
|
# 100%, or any orphan tag all fail the build. A misconfigured run must not
|
|
# report a plausible-looking 0%.
|
|
- name: Traceability gate
|
|
run: cargo run -q -p traceability -- check
|
|
|
|
- name: Regenerate matrix and check it is committed
|
|
run: |
|
|
set -e
|
|
cargo run -q -p traceability -- report
|
|
if ! git diff --quiet docs/dev/traceability.md; then
|
|
echo ""
|
|
echo "docs/dev/traceability.md is out of date."
|
|
echo "Run: cargo run -p traceability -- report"
|
|
git diff --stat docs/dev/traceability.md
|
|
exit 1
|
|
fi
|
|
|
|
# The gesture vocabulary, from the same scanner and under the same rule.
|
|
#
|
|
# Blocking, and for a sharper reason than the matrix: these two artefacts
|
|
# are not only read, one of them is *shown to the user*. A stale
|
|
# `gesture_book.rs` is a help sheet in the application telling somebody to
|
|
# perform a gesture that was removed — worse than no help sheet, because
|
|
# they will conclude the application is broken rather than the page.
|
|
#
|
|
# This also fails on a malformed tag, so a typo costs a gesture its
|
|
# desktop half loudly rather than silently — and on a key a Slint
|
|
# handler binds that no tag names, or a key a tag names that no handler
|
|
# binds (tools/traceability/src/keymap.rs).
|
|
- name: Regenerate the gesture vocabulary and check it is committed
|
|
run: cargo run -q -p traceability -- gestures-check
|
|
|
|
# The manual's page, which the packages carry and the help sheet links
|
|
# into. Blocking for the gesture book's reason: it is shown to the user,
|
|
# and a page that disagrees with the README is a manual describing an
|
|
# application that no longer exists.
|
|
- name: Regenerate the manual page and check it is committed
|
|
run: cargo run -q -p traceability -- manual-check
|
|
|
|
# Advisory, not blocking: not every file implements a requirement, and a
|
|
# tag on every function is noise that rots faster than it helps. Tag the
|
|
# unit that decides.
|
|
- name: Check changed files for tags
|
|
if: github.event_name == 'pull_request'
|
|
run: |
|
|
set -e
|
|
CHANGED=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" \
|
|
| grep -E '\.(rs|slint|wgsl)$' || true)
|
|
[ -z "$CHANGED" ] && { echo "No source files changed."; exit 0; }
|
|
|
|
MISSING=0
|
|
for file in $CHANGED; do
|
|
case "$file" in
|
|
*/tests/*|*/test_*|tools/*) continue ;;
|
|
esac
|
|
[ -f "$file" ] || continue
|
|
if ! grep -q 'TRACES:' "$file"; then
|
|
echo " no TRACES tag: $file"
|
|
MISSING=$((MISSING + 1))
|
|
fi
|
|
done
|
|
|
|
if [ "$MISSING" -gt 0 ]; then
|
|
echo ""
|
|
echo "$MISSING changed file(s) carry no requirement tag."
|
|
echo "Format: /// TRACES: FR-CAT-1, FR-CAT-2 | NFR-P1"
|
|
echo " (comma separates IDs, pipe groups types)"
|
|
fi
|
|
|
|
- name: Summary
|
|
if: always()
|
|
run: head -30 docs/dev/traceability.md || true
|