feat(index): generate the skills table from frontmatter
Second half of #29. The index was hand-maintained and had already drifted once: dream and experiment-spec shipped with no row, their rows lost to a squash-merge after a directory reorg. bin/check-index caught that after the fact; generating removes the class. The curated summary and trigger move into each SKILL.md as `summary:` and `use_when:`, carried over verbatim from the existing table rather than derived from `description:` — description is a paragraph written for model selection, and truncating it would have produced a worse table than the one it replaced. Knowledge now lives once, next to the skill it describes, which is the argument that made a hand-maintained index the wrong shape to begin with. The table is written between markers, so the wiring docs, the relationships tree and the trigger table below it are untouched — those are prose, not data. Rows are alphabetical now; the conceptual grouping the old order carried lives in the relationships tree, which is the better place for it. A fourth column shows each skill's surfaces, which is what makes #29's field visible rather than merely enforced. bin/check-index and its test are deleted: a generated table cannot drift in either direction, so `generate-index --check` subsumes both of its assertions. Verified by mutation — deleting the tdd row turns `task check` red with "SKILLS_INDEX.md is out of date". Closes #29 Bump-Type: minor
This commit is contained in:
@@ -1,70 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Tests for bin/check-index.
|
||||
#
|
||||
# The regression these exist for: SKILLS_INDEX.md is maintained by hand, and
|
||||
# two skills (`dream`, `experiment-spec`) shipped without a row — the index
|
||||
# rows lived on feature branches that were squash-merged after a directory
|
||||
# reorg, so the SKILL.md landed and the row did not (#15, #22). Nothing
|
||||
# errored; the index just quietly under-reported the library.
|
||||
|
||||
set -u
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
SUT="$ROOT/bin/check-index"
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
WORK=${TMPDIR:-/tmp}/check_index_test.$$
|
||||
|
||||
if [ ! -f "$SUT" ]; then
|
||||
printf 'FAIL: %s does not exist\n' "$SUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cleanup() { rm -rf "$WORK"; }
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# new_fixture <name> — a checkout with skills/alpha and skills/beta.
|
||||
new_fixture() {
|
||||
_f="$WORK/$1"
|
||||
mkdir -p "$_f/skills/alpha" "$_f/skills/beta"
|
||||
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$_f/skills/alpha/SKILL.md"
|
||||
printf -- '---\nname: beta\ndescription: b\n---\n' > "$_f/skills/beta/SKILL.md"
|
||||
printf '%s' "$_f"
|
||||
}
|
||||
|
||||
index() {
|
||||
_dir=$1; shift
|
||||
printf '# Engineering Skills Index\n\n| Skill | Description | Use when |\n|---|---|---|\n' > "$_dir/SKILLS_INDEX.md"
|
||||
for _n in "$@"; do
|
||||
printf '| `%s` | desc | when |\n' "$_n" >> "$_dir/SKILLS_INDEX.md"
|
||||
done
|
||||
}
|
||||
|
||||
# expect <0|1> <label> <fixture> [grep-for]
|
||||
expect() {
|
||||
_want=$1; _label=$2; _fx=$3; _needle=${4:-}
|
||||
_out=$(sh "$SUT" "$_fx" 2>&1); _rc=$?
|
||||
if [ "$_rc" -ne "$_want" ]; then
|
||||
fail=$((fail + 1)); printf ' FAIL %s (want rc=%s got %s)\n%s\n' "$_label" "$_want" "$_rc" "$_out"; return
|
||||
fi
|
||||
if [ -n "$_needle" ] && ! printf '%s' "$_out" | grep -q "$_needle"; then
|
||||
fail=$((fail + 1)); printf ' FAIL %s (rc ok, but report never names %s)\n%s\n' "$_label" "$_needle" "$_out"; return
|
||||
fi
|
||||
pass=$((pass + 1)); printf ' ok %s\n' "$_label"
|
||||
}
|
||||
|
||||
fx=$(new_fixture complete); index "$fx" alpha beta
|
||||
expect 0 'every skill has a row' "$fx"
|
||||
|
||||
fx=$(new_fixture missing); index "$fx" alpha
|
||||
expect 1 'a skill with no row is reported' "$fx" beta
|
||||
|
||||
fx=$(new_fixture stale); index "$fx" alpha beta gamma
|
||||
expect 1 'a row for a skill that does not exist is reported' "$fx" gamma
|
||||
|
||||
fx=$(new_fixture noindex)
|
||||
expect 1 'a missing index is reported' "$fx" SKILLS_INDEX.md
|
||||
|
||||
printf '\ncheck_index: %d passed, %d failed\n' "$pass" "$fail"
|
||||
[ "$fail" -eq 0 ]
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/bin/sh
|
||||
# Tests for bin/generate-index.
|
||||
#
|
||||
# SKILLS_INDEX.md was maintained by hand and drifted: two skills shipped with
|
||||
# no row at all (#15), because their rows lived on branches squash-merged after
|
||||
# a directory reorg. A gate caught drift after the fact; generating the table
|
||||
# from frontmatter removes the class entirely (#29) — the curated summary and
|
||||
# trigger now live next to the skill they describe, which is the same argument
|
||||
# that made a hand-maintained index the wrong shape.
|
||||
|
||||
set -u
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
SUT="$ROOT/bin/generate-index"
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
WORK=${TMPDIR:-/tmp}/generate_index_test.$$
|
||||
|
||||
if [ ! -f "$SUT" ]; then
|
||||
printf 'FAIL: %s does not exist\n' "$SUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cleanup() { rm -rf "$WORK"; }
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
|
||||
no() { fail=$((fail + 1)); printf ' FAIL %s\n%s\n' "$1" "${2:-}"; }
|
||||
|
||||
# skill <root> <name> <surfaces> [summary] [use_when]
|
||||
skill() {
|
||||
mkdir -p "$1/skills/$2"
|
||||
{
|
||||
printf -- '---\nname: %s\nsurfaces: %s\n' "$2" "$3"
|
||||
[ $# -lt 4 ] || printf 'summary: "%s"\n' "$4"
|
||||
[ $# -lt 5 ] || printf 'use_when: "%s"\n' "$5"
|
||||
printf -- 'description: d\n---\n'
|
||||
} > "$1/skills/$2/SKILL.md"
|
||||
}
|
||||
|
||||
# fixture <name> — a root with an index carrying the generated-table markers.
|
||||
fixture() {
|
||||
_f="$WORK/$1"
|
||||
mkdir -p "$_f"
|
||||
cat > "$_f/SKILLS_INDEX.md" <<'IDX'
|
||||
# Engineering Skills Index
|
||||
|
||||
<!-- BEGIN skills-table -->
|
||||
<!-- END skills-table -->
|
||||
|
||||
## Wiring
|
||||
prose that must survive regeneration
|
||||
IDX
|
||||
printf '%s' "$_f"
|
||||
}
|
||||
|
||||
f=$(fixture basic)
|
||||
skill "$f" beta '[claude-code, library]' 'B summary' 'when B'
|
||||
skill "$f" alpha '[chat]' 'A summary' 'when A'
|
||||
out=$(sh "$SUT" "$f" 2>&1); rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
no 'generates a table' "$out"
|
||||
elif ! grep -q '^| `alpha` | A summary | when A |' "$f/SKILLS_INDEX.md"; then
|
||||
no 'generates a table' "$(cat "$f/SKILLS_INDEX.md")"
|
||||
elif ! grep -q 'prose that must survive regeneration' "$f/SKILLS_INDEX.md"; then
|
||||
no 'content outside the markers survives' "$(cat "$f/SKILLS_INDEX.md")"
|
||||
elif [ "$(grep -n '`alpha`' "$f/SKILLS_INDEX.md" | cut -d: -f1)" -gt "$(grep -n '`beta`' "$f/SKILLS_INDEX.md" | cut -d: -f1)" ]; then
|
||||
no 'rows are sorted' "$(cat "$f/SKILLS_INDEX.md")"
|
||||
else
|
||||
ok 'generates a sorted table and leaves the rest of the file alone'
|
||||
fi
|
||||
|
||||
# The surfaces a skill ships to are the point of #29, so they are visible.
|
||||
grep -q 'chat' "$f/SKILLS_INDEX.md" \
|
||||
&& ok 'the row names the surfaces' \
|
||||
|| no 'the row names the surfaces' "$(cat "$f/SKILLS_INDEX.md")"
|
||||
|
||||
# Idempotent: a second run changes nothing.
|
||||
before=$(cat "$f/SKILLS_INDEX.md")
|
||||
sh "$SUT" "$f" >/dev/null 2>&1
|
||||
[ "$before" = "$(cat "$f/SKILLS_INDEX.md")" ] \
|
||||
&& ok 'regeneration is idempotent' \
|
||||
|| no 'regeneration is idempotent'
|
||||
|
||||
# --check is the CI gate: clean tree passes.
|
||||
sh "$SUT" --check "$f" >/dev/null 2>&1 \
|
||||
&& ok '--check passes on a generated index' \
|
||||
|| no '--check passes on a generated index'
|
||||
|
||||
# ...and fails once a skill appears without regenerating.
|
||||
skill "$f" gamma '[library]' 'G summary' 'when G'
|
||||
if sh "$SUT" --check "$f" >/dev/null 2>&1; then
|
||||
no '--check fails on a stale index'
|
||||
else
|
||||
ok '--check fails on a stale index'
|
||||
fi
|
||||
|
||||
# A skill with no curated summary is an error, not an empty cell: the table
|
||||
# cell is what a reader scans, and a blank one is worse than a missing row.
|
||||
f=$(fixture nosummary)
|
||||
skill "$f" alpha '[library]'
|
||||
out=$(sh "$SUT" "$f" 2>&1); rc=$?
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
no 'a missing summary is an error' "$out"
|
||||
elif ! printf '%s' "$out" | grep -q alpha; then
|
||||
no 'a missing summary is an error' "$out"
|
||||
else
|
||||
ok 'a missing summary is an error naming the skill'
|
||||
fi
|
||||
|
||||
printf '\ngenerate_index: %d passed, %d failed\n' "$pass" "$fail"
|
||||
[ "$fail" -eq 0 ]
|
||||
Reference in New Issue
Block a user