extract skim-makefile; reshape orient-entrypoints headers
commit 7bbadf52dd92c084b7f17726901f0b8082441405 · parent a4ee40002b
· browse this tree
Ximianth, 2026-06-24 13:34:53 UTC
Per-system parsing moves into its own script (skim-makefile) so it can be used independently, and orient-entrypoints stays focused on detection and delegation. Header shape change: - h4 is now the command an agent would use (e.g. 'make'), not the filename. - A prose line under the header names the specific file (e.g. 'From `GNUmakefile`:'), since the file of note varies for some systems. The skeleton comment block at the end is updated to reflect both conventions (command-named header, parsing in a sibling skim-* script) for future entrypoint systems.
diff --git a/README.md b/README.md index aff2618..a3187ff 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ or compose into larger summaries (e.g. an agent's system prompt). - `orient-git` -- describes the surrounding git repo, if any. - `orient-project` -- inlines root orientation docs and detects a meta repo. - `orient-entrypoints` -- detects entrypoint systems (Makefile etc.) and lists their targets. +- `skim-makefile` -- summarizes a Makefile's targets as a bullet list. - `summarize-md-toc` -- emits a markdown file's heading structure as a bullet list. Scripts share verb-prefix conventions (`orient-*`, `summarize-*`, ...) diff --git a/orient-entrypoints b/orient-entrypoints index 87e555f..dc04775 100755 --- a/orient-entrypoints +++ b/orient-entrypoints @@ -6,15 +6,21 @@ # PATH defaults to the git toplevel of cwd, or cwd itself if not in a repo. # # Silent (no output, exit 0) when no entrypoint system is detected. -# Each detected system emits an h4 heading; intended to sit under an h3. +# Each detected system emits an h4 heading named after the command an agent +# would use to interact further (e.g. `make`); intended to sit under an h3. # # Hard rule: parse only, never invoke. # Repos can be untrusted content (e.g. freshly cloned from elsewhere); # the existence of files in the repo must not translate into code execution. # Any future addition here must follow the same rule. +# +# This script focuses on detection. Per-system parsing lives in sibling +# scripts (e.g. `skim-makefile`) so they can be used independently. set -euo pipefail +HERE=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") + # Resolve the project root (mirrors orient-project). if [ $# -ge 1 ]; then root=$(cd "$1" && pwd) @@ -24,14 +30,10 @@ else root="$PWD" fi -# Cap on how many targets we'll list per system. -# Generous; mirrors orient-project's listing cap. -MAX_TARGETS=50 - # --- Makefile ------------------------------------------------------------- # # Search order matches GNU make's own (GNUmakefile, makefile, Makefile). -# We only consider the first one found, the same as make would. +# Only the first hit matters: that's the one make itself would use. makefile="" for candidate in GNUmakefile makefile Makefile; do if [ -f "$root/$candidate" ]; then @@ -42,78 +44,17 @@ done if [ -n "$makefile" ]; then rel="${makefile#$root/}" - echo "#### \`${rel}\`" + echo "#### \`make\`" echo - - # Two-pass parse, in order of preference: - # - # 1. Annotated targets: `name: [deps] ## description`. - # A widespread cargo-culted convention (often paired with a `help` target - # that greps for the same pattern). When present, treat it as the - # author's curated entrypoint surface. - # - # 2. Bare targets: `name:` or `name: deps`. - # Fallback when no annotations are found. Best-effort: filters out - # pattern rules (`%`), variable assignments (`:=`, `=`, etc.), and - # dotted internal targets (`.PHONY`, `.SUFFIXES`, ...) via the leading - # char class. - # - # Both passes are pure regex over the file; we never expand variables or - # follow includes. That means a Makefile that defines targets via include - # or eval will look empty here -- and that's the correct, safe behavior. - - annotated=$(grep -E '^[a-zA-Z0-9_][a-zA-Z0-9_.-]*[[:space:]]*:[^=]*##' "$makefile" || true) - - if [ -n "$annotated" ]; then - echo "Annotated targets (\`name: ## description\` convention):" - echo - printf '%s\n' "$annotated" \ - | awk -F'##' '{ - split($1, a, ":"); - name = a[1]; - gsub(/^[ \t]+|[ \t]+$/, "", name); - desc = $2; - gsub(/^[ \t]+|[ \t]+$/, "", desc); - printf "- `%s` -- %s\n", name, desc; - }' \ - | head -n "$MAX_TARGETS" - total=$(printf '%s\n' "$annotated" | wc -l) - if [ "$total" -gt "$MAX_TARGETS" ]; then - echo "- ... ($((total - MAX_TARGETS)) more not shown)" - fi - else - # Bare-target parse. - # Regex requires `:` (or `::`) followed by space or end-of-line, which - # naturally excludes `:=` and other assignment forms. - mapfile -t targets < <( - grep -E '^[a-zA-Z0-9_][a-zA-Z0-9_.-]*[[:space:]]*::?([[:space:]]|$)' "$makefile" \ - | grep -v '%' \ - | sed -E 's/^([a-zA-Z0-9_][a-zA-Z0-9_.-]*).*/\1/' \ - | awk '!seen[$0]++' \ - || true - ) - if [ ${#targets[@]} -gt 0 ]; then - echo "Targets (parsed; no \`## description\` annotations found):" - echo - n=0 - for t in "${targets[@]}"; do - if [ "$n" -ge "$MAX_TARGETS" ]; then - echo "- ... ($((${#targets[@]} - MAX_TARGETS)) more not shown)" - break - fi - echo "- \`$t\`" - n=$((n + 1)) - done - else - # Makefile present but yielded no targets we recognize. - # Useful signal in itself: tells the agent "go read it". - echo "(No targets parsed; the Makefile may define targets via include or eval.)" - fi - fi + echo "From \`${rel}\`:" + echo + "$HERE/skim-makefile" "$makefile" fi # --- Future entrypoint systems -------------------------------------------- # # Add detection blocks here for justfile, Taskfile.yml, scripts/, etc. # Same rules apply: parse only, never invoke. Each block stays silent if -# its system isn't present, and emits an h4 heading when it is. +# its system isn't present, and emits an h4 heading named after the command +# an agent would use (`just`, `task`, ...) when it is. Per-system parsing +# should live in its own `skim-*` sibling script. diff --git a/skim-makefile b/skim-makefile new file mode 100755 index 0000000..bf8d602 --- /dev/null +++ b/skim-makefile @@ -0,0 +1,90 @@ +#!/bin/bash +# Summarizes a Makefile's targets as a markdown bullet list. +# +# Usage: skim-makefile PATH +# +# Emits a markdown fragment (a bullet list, possibly preceded by a short +# prose line). No headings: the caller frames the section. +# +# Hard rule: parse only, never invoke. +# Repos can be untrusted content; the existence of a Makefile must not +# translate into code execution. We never run `make`, never expand +# variables, never follow includes. A Makefile that defines targets via +# include or eval will look empty here, and that's the correct safe behavior. +# +# Two-pass parse, in order of preference: +# +# 1. Annotated targets: `name: [deps] ## description`. +# A widely cargo-culted convention (often paired with a `help` target +# that greps for the same pattern). When present, treated as the +# author's curated entrypoint surface -- unannotated targets in such a +# Makefile are typically private helpers and are dropped. +# +# 2. Bare targets: `name:` or `name: deps`. +# Fallback when no annotations are found. Filters pattern rules (`%`), +# variable assignments (regex requires `:` followed by space or EOL, +# which naturally excludes `:=`), and dotted internals (`.PHONY`, ...). + +set -euo pipefail + +if [ $# -lt 1 ]; then + echo "usage: skim-makefile PATH" >&2 + exit 2 +fi +makefile="$1" +if [ ! -f "$makefile" ]; then + echo "skim-makefile: not a file: $makefile" >&2 + exit 2 +fi + +# Cap on listed targets; large recipe lists self-truncate with a counter. +MAX_TARGETS=50 + +annotated=$(grep -E '^[a-zA-Z0-9_][a-zA-Z0-9_.-]*[[:space:]]*:[^=]*##' "$makefile" || true) + +if [ -n "$annotated" ]; then + echo "Annotated targets (\`name: ## description\` convention):" + echo + printf '%s\n' "$annotated" \ + | awk -F'##' '{ + split($1, a, ":"); + name = a[1]; + gsub(/^[ \t]+|[ \t]+$/, "", name); + desc = $2; + gsub(/^[ \t]+|[ \t]+$/, "", desc); + printf "- `%s` -- %s\n", name, desc; + }' \ + | head -n "$MAX_TARGETS" + total=$(printf '%s\n' "$annotated" | wc -l) + if [ "$total" -gt "$MAX_TARGETS" ]; then + echo "- ... ($((total - MAX_TARGETS)) more not shown)" + fi + exit 0 +fi + +# Bare-target parse. +mapfile -t targets < <( + grep -E '^[a-zA-Z0-9_][a-zA-Z0-9_.-]*[[:space:]]*::?([[:space:]]|$)' "$makefile" \ + | grep -v '%' \ + | sed -E 's/^([a-zA-Z0-9_][a-zA-Z0-9_.-]*).*/\1/' \ + | awk '!seen[$0]++' \ + || true +) +if [ ${#targets[@]} -gt 0 ]; then + echo "Targets (parsed; no \`## description\` annotations found):" + echo + n=0 + for t in "${targets[@]}"; do + if [ "$n" -ge "$MAX_TARGETS" ]; then + echo "- ... ($((${#targets[@]} - MAX_TARGETS)) more not shown)" + break + fi + echo "- \`$t\`" + n=$((n + 1)) + done + exit 0 +fi + +# File present but yielded no targets we recognize. +# Useful signal in itself: tells the caller "go read it". +echo "(No targets parsed; the Makefile may define targets via include or eval.)"