Files
hyperguild/scripts/migrate-brain-halls.sh
Mathias 75685e7b67
All checks were successful
CI / Lint / Test / Vet (push) Successful in 11s
CI / Mirror to GitHub (push) Successful in 4s
feat(brain): structured wing/hall taxonomy + obsidian-compatible layout
Adds a two-dimensional address (wing, hall) to brain notes. A wing is a
topic domain (e.g. jepa-fx, hyperguild); a hall is one of a closed
vocabulary of memory types (facts, decisions, failures, hypotheses,
sources). Notes route to brain/wiki/<wing>/<hall>/<slug>.md with
wing/hall/created_at YAML frontmatter, making the directory a valid
Obsidian vault.

Changes:
- new package ingestion/internal/brain (NotePath, ValidHalls, Sanitise,
  BuildWingIndex, BuildAllWingIndexes)
- api.WriteNote refactored to WriteNoteOptions; wing+hall routes to
  brain/wiki/, otherwise falls back to brain/knowledge/ (legacy)
- search.Query → QueryOptions with optional Wing/Hall filtering; Result
  carries wing/hall extracted from frontmatter or path segments
- MCP tools brain_write and brain_query gain optional wing/hall params
  (hall enum-validated); new brain_index tool regenerates _index.md MOC
- POST /index REST endpoint mirrors brain_index
- brain_write auto-rebuilds the wing's _index.md after a wing+hall write
- scripts/migrate-brain-halls.sh migrates flat brain/wiki/{concepts,entities}/
  into the new layout (dry-run by default, --commit applies)

All existing tests pass; new tests cover wing/hall write routing, scope
filtering, invalid hall rejection, _index.md generation, and migration
script paths.

Closes hyperguild#1.
2026-05-18 20:47:08 +02:00

136 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# migrate-brain-halls.sh — move flat brain/wiki/{concepts,entities}/ notes
# into the structured brain/wiki/<wing>/<hall>/ layout introduced by
# hyperguild#1.
#
# Reads each note's YAML frontmatter:
# type: maps to hall (decision, hypothesis, failure, source → eponymous;
# concept, entity, anything else → facts)
# domain: maps to wing (sanitised: lowercase, alphanumerics + hyphens);
# empty → "general"
#
# Dry-run by default. Pass --commit to actually move files. Idempotent:
# already-migrated notes (already under a Wing dir) are left alone.
#
# Usage:
# scripts/migrate-brain-halls.sh /path/to/brain # dry-run
# scripts/migrate-brain-halls.sh --commit /path/to/brain # apply
set -euo pipefail
COMMIT=0
BRAIN=""
for arg in "$@"; do
case "$arg" in
--commit) COMMIT=1 ;;
-h|--help)
sed -n '2,18p' "$0"
exit 0
;;
*) BRAIN="$arg" ;;
esac
done
if [[ -z "$BRAIN" ]]; then
echo "error: brain directory required" >&2
echo "usage: $0 [--commit] <brain-dir>" >&2
exit 2
fi
if [[ ! -d "$BRAIN" ]]; then
echo "error: $BRAIN is not a directory" >&2
exit 2
fi
WIKI="$BRAIN/wiki"
if [[ ! -d "$WIKI" ]]; then
echo "no $WIKI/ — nothing to migrate"
exit 0
fi
sanitise() {
# lowercase, replace non-alnum with hyphen, collapse hyphens, trim
local s
s=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g')
printf '%s' "$s"
}
# extract_frontmatter_value <file> <key>
# Echoes the value (trimmed, unquoted) of `key:` from a leading YAML
# frontmatter block. Empty if absent or no frontmatter.
extract_frontmatter_value() {
awk -v key="$2" '
BEGIN { in_fm = 0; first = 1 }
/^---[[:space:]]*$/ {
if (first) { in_fm = 1; first = 0; next }
if (in_fm) { exit }
}
in_fm {
idx = index($0, ":")
if (idx == 0) next
k = substr($0, 1, idx-1)
v = substr($0, idx+1)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", k)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", v)
gsub(/^["'\'']|["'\'']$/, "", v)
if (k == key) { print v; exit }
}
' "$1"
}
hall_for_type() {
case "$1" in
decision|decisions) echo "decisions" ;;
hypothesis|hypotheses) echo "hypotheses" ;;
failure|failures) echo "failures" ;;
source|sources) echo "sources" ;;
*) echo "facts" ;;
esac
}
declare -i moved=0 skipped=0
migrate_source_dir() {
local src="$1"
[[ -d "$src" ]] || return 0
while IFS= read -r -d '' f; do
local typ domain wing hall slug dest
typ=$(extract_frontmatter_value "$f" type)
domain=$(extract_frontmatter_value "$f" domain)
hall=$(hall_for_type "$typ")
wing=$(sanitise "${domain:-general}")
[[ -z "$wing" ]] && wing="general"
slug=$(basename "$f" .md)
dest="$WIKI/$wing/$hall/$slug.md"
if [[ "$f" == "$dest" ]]; then
skipped=$((skipped + 1))
continue
fi
if [[ -e "$dest" ]]; then
echo "skip (target exists): $f$dest"
skipped=$((skipped + 1))
continue
fi
if [[ "$COMMIT" -eq 1 ]]; then
mkdir -p "$(dirname "$dest")"
git -C "$BRAIN" mv "$f" "$dest" 2>/dev/null || mv "$f" "$dest"
fi
echo "move: $f$dest"
moved=$((moved + 1))
done < <(find "$src" -maxdepth 1 -type f -name '*.md' -print0)
}
migrate_source_dir "$WIKI/concepts"
migrate_source_dir "$WIKI/entities"
echo
if [[ "$COMMIT" -eq 1 ]]; then
echo "moved=$moved skipped=$skipped (committed)"
else
echo "moved=$moved skipped=$skipped (dry-run — pass --commit to apply)"
fi