#!/usr/bin/env bash # CASAN path resolver — single source of truth for harness / state / governance roots. # # Plan-01 restructure: physical file moves (`.specify/` -> `packages/casan-harness/`, # domain -> `apps/okr/domain/`) are absorbed HERE. Callers must reference the # CASAN_*_ROOT variables below instead of hardcoding ".specify/...", so a move only # changes this file, not the 37+ scripts that consume the paths. # # Resolution rules (per root): # - An explicit env override always wins (e.g. CASAN_HARNESS_ROOT=... bash foo.sh). # - CASAN_HARNESS_ROOT derives from THIS file's own location: casan-paths.sh always # lives at /scripts/bash/casan-paths.sh, so `../..` is the harness # root wherever the tree is moved. No edit needed when the package relocates. # - CASAN_APP_ROOT is found by walking UP for the `.specify` state marker. # NEVER use `git rev-parse --show-toplevel`: in this checkout the git root is the # repo PARENT, not the app dir, which would shift every path up one level. # # Idempotent and `set -e` safe: sourcing multiple times is a no-op; nothing here # returns a non-zero status to a caller running under `set -euo pipefail`. if [[ -n "${CASAN_PATHS_SOURCED:-}" ]]; then return 0 2>/dev/null || true fi _casan_paths_self="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Walk up from a starting dir to the app/bundle root. Primary marker is the `.specify` # state dir (present in an adopted repo). Secondary marker is `packages/casan-harness` # (present in a freshly-extracted release bundle that has no `.specify` yet) — so a # just-unpacked core/devkit/demo bundle resolves correctly and creates `.specify` on # first write. In a real repo `.specify` matches first, so behavior is unchanged. _casan_find_app_root() { local d="$1" while [[ -n "$d" && "$d" != "/" ]]; do if [[ -d "$d/.specify" || -d "$d/packages/casan-harness" ]]; then printf '%s\n' "$d" return 0 fi d="$(dirname "$d")" done return 1 } # Harness (CODE) root: the tree that holds scripts/, security/, templates/, tests/, # config/, level5/*.yaml. Derived from this file's location so it follows the move. if [[ -z "${CASAN_HARNESS_ROOT:-}" ]]; then CASAN_HARNESS_ROOT="$(cd "$_casan_paths_self/../.." && pwd)" fi # App root: where runtime state lives (holds the `.specify` marker). Walk up from the # harness root; fall back to the classic `/..`-style layout if not found. if [[ -z "${CASAN_APP_ROOT:-}" ]]; then CASAN_APP_ROOT="$(_casan_find_app_root "$_casan_paths_self" || true)" fi if [[ -z "${CASAN_APP_ROOT:-}" ]]; then CASAN_APP_ROOT="$(cd "$CASAN_HARNESS_ROOT/.." && pwd)" fi # State (RUNTIME) root: logs/, state/, agentops/ — stays with the app, not the package. if [[ -z "${CASAN_STATE_ROOT:-}" ]]; then CASAN_STATE_ROOT="$CASAN_APP_ROOT/.specify" fi # Governance root: central-governance mixes harness pub-keys/registries, runtime # policy-manifest state, and a private key. Rooted under STATE (not HARNESS) so the # runtime-regenerated policy-manifest.{json,sig} and the private key never land inside # the shipped code package; it stays at .specify/level5/central-governance across the # move. Plan-01 can still split individual files later via per-file overrides # (CASAN_AUDIT_PUB, CASAN_AGENT_REGISTRY, CASAN_AGENT_KEYS_DIR, CASAN_BUNDLE_MANIFEST). if [[ -z "${CASAN_GOVERNANCE_ROOT:-}" ]]; then CASAN_GOVERNANCE_ROOT="$CASAN_STATE_ROOT/level5/central-governance" fi # Domain (DATA) root: per-project golden-runs, red-team/benign corpus, requirement # input, traceability map — the reusable harness must NOT bake these in. Lives at # apps/okr/domain (compat symlinks bridge the pre-move .specify paths). A different # app sets CASAN_DOMAIN_ROOT to its own apps//domain (Plan-06 reuse). if [[ -z "${CASAN_DOMAIN_ROOT:-}" ]]; then if [[ -d "$CASAN_APP_ROOT/apps/okr/domain" ]]; then CASAN_DOMAIN_ROOT="$CASAN_APP_ROOT/apps/okr/domain" else # Fallback for the pre-split monolithic layout: domain data co-located in .specify. CASAN_DOMAIN_ROOT="$CASAN_STATE_ROOT" fi fi # NOTE: roots are deliberately NOT exported. Each script/test sources this resolver # and self-derives its roots from its OWN location (BASH_SOURCE), matching the original # per-script `PROJECT_ROOT="$SCRIPT_DIR/../../.."` semantics. Exporting them would leak # the caller's (real-repo) roots into sandbox subprocesses (e.g. `node casan-step.mjs`, # copied telemetry/rollback scripts), breaking hermetic test isolation. The idempotency # flag is likewise NOT exported, so every subprocess re-resolves against its own tree. CASAN_PATHS_SOURCED=1