From 02fdbccb39a99fcd20068cce98c5c9dd118715d0 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 21 Jun 2026 18:08:03 -0500 Subject: [PATCH] =?UTF-8?q?docs(framework):=20P4.1=20=E2=80=94=20fix=20sta?= =?UTF-8?q?le=20install.sh=20comments=20+=20cmp-equal=20early-exit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Non-blocking fast-follow from #590 dual-engine review: - install.sh: PRESERVE_PATHS "(never overwritten)" and the seed-block "never be overwritten once customized" comments contradicted P4's framework-owned overwrite — clarified both (PRESERVE protects from rsync --delete; reconcile re-applies framework-owned with backup-once). - reconcile (install.sh + file-adapter.ts): skip the copy when content already matches (cmp-equal early-exit) to avoid mtime churn. Parity preserved. install.sh fixtures 14/14 green; gate green; prettier clean. Refs #542, closes #592 Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/mosaic/framework/install.sh | 22 +++++++++++++--------- packages/mosaic/src/config/file-adapter.ts | 4 +++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/mosaic/framework/install.sh b/packages/mosaic/framework/install.sh index 5083a41..da5d596 100755 --- a/packages/mosaic/framework/install.sh +++ b/packages/mosaic/framework/install.sh @@ -19,7 +19,9 @@ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}" INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}" -# Files/dirs preserved across upgrades (never overwritten). +# Files/dirs protected from rsync --delete during sync. NOTE: framework-owned +# entries (CONSTITUTION/AGENTS/STANDARDS) ARE re-applied afterward by +# reconcile_framework_files (overwrite + backup-once); the rest stay user-owned. # User-created content in these paths survives rsync --delete. PRESERVE_PATHS=("CONSTITUTION.md" "AGENTS.md" "SOUL.md" "USER.md" "TOOLS.md" "STANDARDS.md" "memory" "sources" "credentials") @@ -70,11 +72,13 @@ reconcile_framework_files() { [[ -d "$defaults" ]] || return 0 for f in "${FRAMEWORK_OWNED[@]}"; do [[ -f "$defaults/$f" ]] || continue - if [[ -f "$TARGET_DIR/$f" ]] && ! cmp -s "$TARGET_DIR/$f" "$defaults/$f"; then - if [[ ! -f "$TARGET_DIR/${f}.pre-constitution.bak" ]]; then - cp "$TARGET_DIR/$f" "$TARGET_DIR/${f}.pre-constitution.bak" - warn "$f is now framework-owned and was updated; your previous copy is saved as ${f}.pre-constitution.bak — re-apply intended changes as a .local overlay or policy/ file (see CONSTITUTION.md / constitution/LAYER-MODEL.md)." - fi + # Already current — skip to avoid mtime churn. + if [[ -f "$TARGET_DIR/$f" ]] && cmp -s "$TARGET_DIR/$f" "$defaults/$f"; then + continue + fi + if [[ -f "$TARGET_DIR/$f" && ! -f "$TARGET_DIR/${f}.pre-constitution.bak" ]]; then + cp "$TARGET_DIR/$f" "$TARGET_DIR/${f}.pre-constitution.bak" + warn "$f is now framework-owned and was updated; your previous copy is saved as ${f}.pre-constitution.bak — re-apply intended changes as a .local overlay or policy/ file (see CONSTITUTION.md / constitution/LAYER-MODEL.md)." fi cp "$defaults/$f" "$TARGET_DIR/$f" done @@ -281,9 +285,9 @@ sync_framework mkdir -p "$TARGET_DIR/memory" mkdir -p "$TARGET_DIR/credentials" -# Seed defaults — copy framework contract files from defaults/ to framework -# root if not already present. These ship with sensible defaults but must -# never be overwritten once the user has customized them. +# Reconcile contract files from defaults/ into the framework root: framework-owned +# files (CONSTITUTION/AGENTS/STANDARDS) are overwritten every upgrade (a divergent +# copy is backed up once); user-seeded files (TOOLS) are written on first install only. # # This list must match the framework-contract whitelist in # packages/mosaic/src/config/file-adapter.ts (FileConfigAdapter.syncFramework). diff --git a/packages/mosaic/src/config/file-adapter.ts b/packages/mosaic/src/config/file-adapter.ts index ea92f6d..8e45617 100644 --- a/packages/mosaic/src/config/file-adapter.ts +++ b/packages/mosaic/src/config/file-adapter.ts @@ -198,8 +198,10 @@ export class FileConfigAdapter implements ConfigService { const src = join(defaultsDir, entry); const dest = join(this.mosaicHome, entry); if (!existsSync(src) || !statSync(src).isFile()) continue; + // Already current — skip to avoid mtime churn. + if (existsSync(dest) && readFileSync(src).equals(readFileSync(dest))) continue; const bak = `${dest}.pre-constitution.bak`; - if (existsSync(dest) && !readFileSync(src).equals(readFileSync(dest)) && !existsSync(bak)) { + if (existsSync(dest) && !existsSync(bak)) { copyFileSync(dest, bak); } copyFileSync(src, dest);