diff --git a/packages/mosaic/framework/tools/_scripts/mosaic-doctor b/packages/mosaic/framework/tools/_scripts/mosaic-doctor index 6fbe8367..03ab14a3 100755 --- a/packages/mosaic/framework/tools/_scripts/mosaic-doctor +++ b/packages/mosaic/framework/tools/_scripts/mosaic-doctor @@ -379,8 +379,54 @@ check_fleet_transport() { fi } +check_structure_anchor_provisioning() { + # T51 WP0b (spec §1.2a + PHASE2-MAP F7): audit the two declaration anchors. + # Doctor runs from operator shells and CI where the launcher exports do not + # exist, so this is an AUDIT ONLY — it never exports, writes, or fabricates + # values for consumption. Four states (charter): + # both present+nonempty PASS (values reported as paths only) + # one missing/empty WARN naming the var + the launcher as authority + # neither present INFORMATIONAL launcher-equivalent derivation, + # explicitly non-authoritative, + launcher warning; + # never an error by design (F7(b)) + # Severity follows the doctor's existing conventions: pass/note are quiet + # (note unless --verbose), warn counts toward --fail-on-warn. + local host_root="${MOSAIC_HOST_ROOT:-}" brain_home="${MOSAIC_BRAIN_HOME:-}" + # T51P2WP0BRW B1: presence is tracked SEPARATELY from value — `${VAR:-}` + # collapses exported-empty into genuinely-unset, which mis-filed both-empty + # and the mixed empty/unset states as informational. Only BOTH-genuinely- + # absent may be informational (charter state 3); any present-but-empty or + # single-present state warns. + local host_set=0 brain_set=0 + [[ -v MOSAIC_HOST_ROOT ]] && host_set=1 + [[ -v MOSAIC_BRAIN_HOME ]] && brain_set=1 + if [[ "$host_set" -eq 1 && "$brain_set" -eq 1 && -n "$host_root" && -n "$brain_home" ]]; then + pass "Structure anchors provisioned: MOSAIC_HOST_ROOT=$host_root MOSAIC_BRAIN_HOME=$brain_home (paths reported only; not expanded, not consumed)" + return + fi + if [[ "$host_set" -eq 0 && "$brain_set" -eq 0 ]]; then + note "Structure anchors not provisioned in this environment. Launcher-equivalent derivation (INFORMATIONAL, NON-AUTHORITATIVE — seats receive the authoritative values from the launchers): MOSAIC_HOST_ROOT would default to the operator home; MOSAIC_BRAIN_HOME would default to the brain tree resolved at launch. Doctor does not guess values for consumption; it audits provisioning." + note "Provision both anchors via the seat launchers (launch-seat.sh / launch-seat-claude.sh export them; see T51 spec §1.2a)." + return + fi + # At least one variable is present (possibly empty), or exactly one exists: + # every missing/empty anchor gets its own loud WARN naming the launchers. + if [[ "$host_set" -eq 0 ]]; then + warn "MOSAIC_HOST_ROOT is not set in this environment while MOSAIC_BRAIN_HOME is — declaration consumers fail closed without it (spec §1.2a). The seat launchers are the authoritative source." + elif [[ -z "$host_root" ]]; then + warn "MOSAIC_HOST_ROOT is present but EMPTY in this environment — declaration consumers fail closed without a usable value (spec §1.2a). The seat launchers are the authoritative source." + fi + if [[ "$brain_set" -eq 0 ]]; then + warn "MOSAIC_BRAIN_HOME is not set in this environment while MOSAIC_HOST_ROOT is — the projects/ mirror and brain declaration resolve from it (spec §1.2a). The seat launchers are the authoritative source." + elif [[ -z "$brain_home" ]]; then + warn "MOSAIC_BRAIN_HOME is present but EMPTY in this environment — the projects/ mirror and brain declaration resolve from it (spec §1.2a). The seat launchers are the authoritative source." + fi +} + check_fleet_transport +check_structure_anchor_provisioning + check_brain_home # Legacy migration surfaces should no longer contain symlink trees. diff --git a/packages/mosaic/framework/tools/_scripts/test-structure-anchor-check.sh b/packages/mosaic/framework/tools/_scripts/test-structure-anchor-check.sh new file mode 100755 index 00000000..11bafb03 --- /dev/null +++ b/packages/mosaic/framework/tools/_scripts/test-structure-anchor-check.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# Covers the structure-anchor provisioning check in `mosaic-doctor` (T51 WP0b). +# +# Same discipline as test-brain-home-check.sh: functions are extracted from the +# shipped script (exact header + closing brace), never copied — a test carrying +# its own copy of the logic keeps passing after the shipped copy changes. +# +# Four contract states (charter T51P2WP0B-20260824): +# 1. both present+nonempty -> pass ([OK]), no warns, no notes +# 2a. host missing, brain set -> warn naming MOSAIC_HOST_ROOT + launchers +# 2b. brain missing, host set -> warn naming MOSAIC_BRAIN_HOME + launchers +# 2c. present-but-EMPTY counts as missing (warns; NEVER informational) +# 3. neither present -> informational notes, NON-AUTHORITATIVE, never warn +# Arms include genuinely-UNSET (env -u) forms, not only empty strings. +# Red control: empty-vs-unset distinction removed in a mutated copy -> suite red. +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname "$0")" && pwd) +DOCTOR="$SCRIPT_DIR/mosaic-doctor" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +[ -f "$DOCTOR" ] || fail "missing mosaic-doctor at $DOCTOR" + +extract_function() { + local name="$1" + local extracted + extracted=$(sed -n "/^${name}() {/,/^}/p" "$DOCTOR") + [ -n "$extracted" ] || fail "could not extract ${name}() from mosaic-doctor — script reshaped?" + printf '%s\n' "$extracted" +} + +for fn in check_structure_anchor_provisioning; do + extract_function "$fn" >/dev/null +done + +# run_case LABEL EXPECT(ok|warn|note) [env assignments as args; -u VAR tokens for unset] +run_case() { + local label="$1" expect="$2" + shift 2 + local envs=() unsets=() + local a + for a in "$@"; do + case "$a" in + -u:*) unsets+=("${a#-u:}") ;; + *) envs+=("$a") ;; + esac + done + local out warns notes oks + # build the env command with proper -u flags (array expansion must not + # glue '-u VAR' into one word) + local cmd=(env) + local e u + # env(1) parses options only before the first assignment — -u flags FIRST + for u in "${unsets[@]:-}"; do [ -n "$u" ] && cmd+=(-u "$u"); done + for e in "${envs[@]:-}"; do [ -n "$e" ] && cmd+=("$e"); done + cmd+=(bash -c "warn() { echo \"[WARN] \$*\"; }; note() { echo \"[NOTE] \$*\"; return 0; }; pass() { echo \"[OK] \$*\"; return 0; }; $(extract_function check_structure_anchor_provisioning); check_structure_anchor_provisioning") + out=$("${cmd[@]}" 2>&1) + warns=$(printf '%s\n' "$out" | grep -c '^\[WARN\]' || true) + notes=$(printf '%s\n' "$out" | grep -c '^\[NOTE\]' || true) + oks=$(printf '%s\n' "$out" | grep -c '^\[OK\]' || true) + if [[ "$expect" == ok && "$oks" -gt 0 && "$warns" -eq 0 && "$notes" -eq 0 ]]; then + echo "ok - $label" + elif [[ "$expect" == warn && "$warns" -ge 1 && "$notes" -eq 0 ]]; then + echo "ok - $label (warned x$warns)" + elif [[ "$expect" == note && "$notes" -gt 0 && "$warns" -eq 0 ]]; then + echo "ok - $label (noted)" + else + echo "output: $out" >&2 + fail "$label: expected $expect (oks=$oks warns=$warns notes=$notes)" + fi +} + +ROOT=$(mktemp -d) +trap 'rm -rf "$ROOT"' EXIT +HOST="$ROOT/host" +BRAIN="$ROOT/brain" + +# ── state 1: both present + nonempty → pass ──────────────────────────────── +run_case "both anchors present passes" ok \ + MOSAIC_HOST_ROOT="$HOST" MOSAIC_BRAIN_HOME="$BRAIN" + +# ── state 2a: host missing (unset), brain set → exactly one warn ─────────── +run_case "unset host root warns" warn \ + -u:MOSAIC_HOST_ROOT MOSAIC_BRAIN_HOME="$BRAIN" + +# ── state 2b: brain missing (unset), host set → exactly one warn ─────────── +run_case "unset brain home warns" warn \ + MOSAIC_HOST_ROOT="$HOST" -u:MOSAIC_BRAIN_HOME + +# ── state 2c-empty: present-but-empty counts as missing ──────────────────── +run_case "empty-string host root warns (empty != set)" warn \ + MOSAIC_HOST_ROOT= MOSAIC_BRAIN_HOME="$BRAIN" +run_case "empty-string brain home warns (empty != set)" warn \ + MOSAIC_HOST_ROOT="$HOST" MOSAIC_BRAIN_HOME= + +# ── state 3: neither present (genuinely unset) → notes, never warn ───────── +run_case "both unset yields non-authoritative notes" note \ + -u:MOSAIC_HOST_ROOT -u:MOSAIC_BRAIN_HOME +run_case "both empty-string warns (empty is present, not absent)" warn \ + MOSAIC_HOST_ROOT= MOSAIC_BRAIN_HOME= +run_case "host empty + brain unset warns" warn \ + MOSAIC_HOST_ROOT= -u:MOSAIC_BRAIN_HOME +run_case "host unset + brain empty warns" warn \ + -u:MOSAIC_HOST_ROOT MOSAIC_BRAIN_HOME= + +# ── red control (mutation): presence tracking removed → red ──────────────── +# Mutant regresses to the reviewed defect shape: presence derived from +# NONEMPTINESS (the `${VAR:-}` collapse) instead of true -v tracking. Both-empty +# then looks genuinely-absent and is mis-filed as informational; the both-empty +# warn arm above finds no WARN and the suite reds. +MUT="$ROOT/mosaic-doctor.mutant" +sed 's/\[\[ -v MOSAIC_HOST_ROOT \]\] \&\& host_set=1/[[ -n "${MOSAIC_HOST_ROOT:-}" ]] \&\& host_set=1/; s/\[\[ -v MOSAIC_BRAIN_HOME \]\] \&\& brain_set=1/[[ -n "${MOSAIC_BRAIN_HOME:-}" ]] \&\& brain_set=1/' \ + "$DOCTOR" > "$MUT" +if cmp -s "$DOCTOR" "$MUT"; then + echo "SKIP red control (mutation anchor not found — sed pattern drifted)" >&2 +else + mut_fn=$(sed -n "/^check_structure_anchor_provisioning() {/,/^}/p" "$MUT") + outm=$(env MOSAIC_HOST_ROOT= MOSAIC_BRAIN_HOME= bash -c \ + "warn() { echo \"[WARN] \$*\"; }; note() { echo \"[NOTE] \$*\"; return 0; }; pass() { echo \"[OK] \$*\"; return 0; }; $mut_fn; check_structure_anchor_provisioning" 2>&1) + if printf '%s\n' "$outm" | grep -q '^\[NOTE\]'; then + echo "ok - red control bites (mutant collapses empty into informational; shipped does not)" + else + fail "red control did not reproduce the regression shape (mutant output unexpected)" + fi +fi + +echo "structure anchor doctor check: all arms passed" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index cc2a48a6..be07e02a 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*",