compass

add find-project-root + orient-contextdocs; route orient-project through find-project-root

commit 2ab2c888ab8e381ea83b93003b8dad08ac6536de · parent fd4110d019 · browse this tree
Ximianth, 2026-06-24 17:04:12 UTC

`find-project-root` is the single source of truth for "what is the current
project".  It's strict (git toplevel or nothing, exit 1 on miss); callers
that want a softer fallback apply it themselves.  Centralizing this means
the inevitable future revisits to "what counts as a project" happen in one
place, not five.

`orient-contextdocs` walks from the project root *down* to a given path,
surfacing conventional orientation docs (README/CONTRIBUTING/HACKME/AGENTS/
CLAUDE/.editorconfig) at each level along the way.  Outermost-first
ordering so the most-local context lands last.  Markdown docs get a TOC
(via `summarize-md-toc`); other docs just get listed by name.  A
`--skip-root` flag lets composers omit the project root when it's already
covered separately (e.g. by `orient-project`).  Silent when there's no
project or no docs found.

A few `# FUTURE:` markers note the planned integration point for path-keyed
notes living in an associated meta repo.

`orient-project` updated to use `find-project-root` rather than inlining
its own resolution logic.  Behaviour is unchanged (it still falls back to
cwd when there's no project, since it should produce *some* orientation
even outside one).
diff --git a/README.md b/README.md
index 3e1b323..3547a7a 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,8 @@ or compose into larger summaries (e.g. an agent's system prompt).
 
 ## Scripts
 
+- `find-project-root`  -- prints the project root path (git toplevel), or exits 1.
+- `orient-contextdocs` -- finds orientation docs from the project root down to a given path.
 - `orient-cwd`         -- describes the current working directory.
 - `orient-entrypoints` -- detects entrypoint systems (Makefile etc.) and lists their targets.
 - `orient-files`       -- classifies files by type (extension, shebang, etc) and counts them.
diff --git a/find-project-root b/find-project-root
new file mode 100755
index 0000000..5f82d6d
--- /dev/null
+++ b/find-project-root
@@ -0,0 +1,36 @@
+#!/bin/bash
+# Single source of truth for "what is the current project root".
+#
+# Usage: find-project-root [PATH]
+#   PATH defaults to cwd.
+#
+# On success: prints the absolute path of the project root and exits 0.
+# On failure: prints nothing and exits 1.
+#
+# Definition of "project root":
+# - The top of the git work tree containing PATH.
+# - If PATH is not inside any git work tree, there is no project root
+#   (and we report that by exiting 1 with no output).
+#
+# This is deliberately strict.  Other compass scripts that want a softer
+# fallback (e.g. "use cwd if there's no project") apply that fallback
+# themselves -- it's a property of those scripts, not of the underlying
+# notion of "project".
+#
+# FUTURE: when we add other project-marker conventions
+# (e.g. a top-level `.project` file, or a configured marker name),
+# this is the one place that resolves them.
+
+set -euo pipefail
+
+start="${1:-$PWD}"
+if [ ! -d "$start" ]; then
+	echo "find-project-root: not a directory: $start" >&2
+	exit 2
+fi
+
+if root=$(cd "$start" && git rev-parse --show-toplevel 2>/dev/null); then
+	echo "$root"
+	exit 0
+fi
+exit 1
diff --git a/orient-contextdocs b/orient-contextdocs
new file mode 100755
index 0000000..aab8a2f
--- /dev/null
+++ b/orient-contextdocs
@@ -0,0 +1,150 @@
+#!/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 `summarize-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/summarize-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
diff --git a/orient-project b/orient-project
index 93d7292..44f7060 100755
--- a/orient-project
+++ b/orient-project
@@ -15,10 +15,15 @@
 set -euo pipefail
 
 # Resolve the project root.
-# Explicit arg wins; otherwise prefer git toplevel; fall back to cwd.
+# Delegated to `find-project-root` so the "what is a project" decision
+# lives in exactly one place (`find-project-root` is strict: git toplevel
+# or nothing).  We add a cwd fallback here because this script wants to
+# produce *some* orientation even outside a project context.
+self_dir=$(cd "$(dirname "$(readlink -f "$0")")" && pwd)
+find_root="$self_dir/find-project-root"
 if [ $# -ge 1 ]; then
 	root=$(cd "$1" && pwd)
-elif root=$(git rev-parse --show-toplevel 2>/dev/null); then
+elif root=$("$find_root" 2>/dev/null); then
 	:
 else
 	root="$PWD"