compass

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.)"