compass

orient-contextdocs

#!/bin/bash
# Finds orientation docs along the path from the project root down to a given
# location, surfacing the context that applies to where you're working.
#
# Usage: orient-contextdocs [--skip-root] [PATH]
#   PATH defaults to cwd.
#
# Walks from the project root (resolved via `find-project-root`) downward
# through each ancestor directory of PATH, all the way to PATH itself.
# At each level, collects conventional orientation docs:
#   README, CONTRIBUTING, HACKME, AGENTS, CLAUDE (any case, common extensions),
#   and `.editorconfig`.
#
# Markdown docs are summarized as a heading-only TOC (via `skim-md-toc`)
# rather than inlined in full: context docs can be large, and the headings are
# usually enough to know whether to open them.
#
# Output ordering: outermost level first, so the most-local context lands
# last in the output -- where the reader's eye comes to rest.
#
# Silent (no output, exit 0) when:
# - PATH is not inside a project (no git work tree), or
# - no orientation docs are found at any level.
#
# Flags:
#   --skip-root   Omit docs at the project root itself.
#                 Useful when a composer is already inlining the root
#                 (e.g. via `orient-project`) and only wants the deeper
#                 ancestor context.
#                 Default (no flag) is to include the root: the script
#                 should make sense when run in isolation.
#
# FUTURE: an associated meta repo may also carry path-specific notes
# (e.g. `<meta>/notes-for-path/<repo-relative-path>/...`) so that private
# orientation can be maintained for specific subpaths of a public repo.
# The exact convention isn't pinned down yet, but the loop below is the
# natural integration point: at each per-directory step, also check the
# meta repo for a corresponding entry.

set -euo pipefail

self_dir=$(cd "$(dirname "$(readlink -f "$0")")" && pwd)
find_root="$self_dir/find-project-root"
summarize="$self_dir/skim-md-toc"

skip_root=0
while [ $# -gt 0 ]; do
	case "$1" in
		--skip-root) skip_root=1; shift ;;
		-h|--help)
			sed -n '2,/^$/p' "$0" | sed 's/^# \?//'
			exit 0 ;;
		--) shift; break ;;
		-*) echo "orient-contextdocs: unknown option: $1" >&2; exit 2 ;;
		*) break ;;
	esac
done

start="${1:-$PWD}"
if [ ! -d "$start" ]; then
	echo "orient-contextdocs: not a directory: $start" >&2
	exit 2
fi
start=$(cd "$start" && pwd)

# Resolve the project root.  Silent exit if there isn't one.
if ! root=$("$find_root" "$start"); then
	exit 0
fi

# Build the list of directories to inspect, from outermost (root) inward.
# We walk up from `start` collecting parents until we hit `root`,
# then reverse the order for outermost-first output.
levels=()
current="$start"
while :; do
	levels=("$current" "${levels[@]}")
	if [ "$current" = "$root" ]; then
		break
	fi
	parent=$(dirname "$current")
	if [ "$parent" = "$current" ]; then
		# Defensive: shouldn't happen if find-project-root succeeded,
		# but means `start` somehow isn't under `root`.
		break
	fi
	current="$parent"
done

if [ "$skip_root" -eq 1 ]; then
	# Drop the first element (the root).
	levels=("${levels[@]:1}")
fi

# Per-level doc finder.
# Case-insensitive name match against the conventional prose files;
# `.editorconfig` matched exactly.
find_docs_in() {
	local dir="$1"
	local base candidate
	shopt -s nullglob nocaseglob
	for base in README CONTRIBUTING HACKME AGENTS CLAUDE; do
		for candidate in "$dir/$base" "$dir/$base".md "$dir/$base".rst "$dir/$base".txt; do
			if [ -f "$candidate" ]; then
				echo "$candidate"
			fi
		done
	done
	shopt -u nocaseglob
	if [ -f "$dir/.editorconfig" ]; then
		echo "$dir/.editorconfig"
	fi
}

# Accumulate output; suppress everything if nothing was found.
buffer=""
for dir in "${levels[@]}"; do
	mapfile -t docs < <(find_docs_in "$dir")
	if [ ${#docs[@]} -eq 0 ]; then
		continue
	fi

	# Show paths relative to the project root for readability.
	if [ "$dir" = "$root" ]; then
		label="./"
	else
		label="${dir#$root/}/"
	fi
	buffer+=$'\n'"#### \`${label}\`"$'\n\n'
	for doc in "${docs[@]}"; do
		rel=$(basename "$doc")
		buffer+="- \`${rel}\`"$'\n'
		# Markdown: append the TOC nested under the bullet.
		case "$doc" in
			*.md|*.MD|*.Md|*.mD)
				while IFS= read -r line; do
					buffer+="  ${line}"$'\n'
				done < <("$summarize" "$doc")
				;;
		esac

		# FUTURE: meta-repo path-keyed notes integration point.
		# Resolve the meta repo (when conventions are pinned down),
		# look for notes keyed by repo-relative path, and inline/summarize.
	done
done

if [ -n "$buffer" ]; then
	printf '%s' "$buffer"
fi

bin/orient-contextdocs · 4.4 KiB · 4072b8e8a0