Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,44 @@ package-lock.json text eol=lf -diff linguist-generated=true
**/*.res linguist-detectable=false
*.nix text eol=lf
flake.lock text eol=lf -diff linguist-generated=true

# --- Integration / merge hygiene (fork↔upstream) --------------------------- #
# This fork and upstream share NO git ancestor, so a naive merge conflicts on
# every shared-but-different path. These rules keep the classes of file that
# must NEVER be hand-merged out of the line-conflict set: git will not produce
# conflict markers inside them, so a maintainer resolves them by regenerating
# (`just heal`) or by an explicit `--ours`/`--theirs` pick, never by editing.
#
# `-merge` treats the path as binary for merge purposes (built-in driver; needs
# no per-clone config, so it is safe the moment this file lands). For those who
# want lockfiles to auto-resolve to the target branch and then be regenerated,
# `just merge-drivers` wires the stronger `merge.lockfile` custom driver.

# Generated lockfiles — regenerate, never line-merge.
Manifest.toml text eol=lf -diff -merge linguist-generated=true
renv.lock text eol=lf -diff -merge linguist-generated=true
frontend/bun.lock text eol=lf -diff -merge linguist-generated=true
bun.lockb binary -diff -merge linguist-generated=true
package-lock.json text eol=lf -diff -merge linguist-generated=true
pnpm-lock.yaml text eol=lf -diff -merge linguist-generated=true
# renv scaffold / generated dependency scan (produced by renv, not authored).
renv/activate.R text eol=lf -diff -merge linguist-generated=true
R/_renv_dependencies.R text eol=lf -diff linguist-generated=true
renv/library/** binary -diff linguist-generated=true
# Machine-local tool-path map (written by scripts/gen-tools-yml.sh).
config/tools.yml text eol=lf -diff linguist-generated=true
config/ci/tools.yml text eol=lf -diff linguist-generated=true

# Build output — never source, never hand-merged. (Upstream committed web/dist;
# the fork builds frontend/dist instead. Neither belongs in a diff.)
web/dist/** binary -diff linguist-generated=true
frontend/dist/** binary -diff linguist-generated=true
**/dist/** binary -diff linguist-generated=true

# Test / coverage / benchmark artefacts.
**/coverage/** binary -diff linguist-generated=true
lcov.info text eol=lf -diff linguist-generated=true
*.cov text eol=lf -diff linguist-generated=true
**/test-results/** binary -diff linguist-generated=true
**/*junit.xml text eol=lf -diff linguist-generated=true
**/bench/results/*.json text eol=lf -diff linguist-generated=true
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ docs/*
!docs/types/
!docs/reproducibility.md
!docs/owner-review-2026-09-25.md
!docs/integration/
!docs/integration/**

# Test + benchmark run artifacts (not the committed fixtures/baselines)
frontend/tests/results/
Expand Down
15 changes: 15 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ Pull requests against this fork must use base
`hyperpolymath/MetaManifold-WebUI:main`. GitHub's fork PR page defaults the
base to the upstream parent — change it before clicking *Create*.

## Landing fork work on upstream (low-friction integration)

This fork and upstream (`JoshuaJewell/MetaManifold-WebUI`) share no git
ancestor, so a naive merge conflicts on every shared-but-different file. To
integrate without a wall of conflicts — and to let the maintainer adopt the work
incrementally, from "pure upstream" to "partially transitional" to "everything",
without ever breaking a running system — see:

- **`docs/integration/README.md`** — the guide (profiles, merge hygiene, staging).
- **`docs/integration/conflict-map-2026-09-25.md`** — the measured conflict set.
- **`docs/integration/HANDOFF-granular-reanchor.md`** — the brief to actually land it.
- `just reanchor-plan` / `just reanchor` — replay the fork onto upstream as ~206 granular commits (3 decisions, 0 conflicts).
- `just integrate status | profiles | plan | triage` and `just augment`/`just suspend <component>`.
- `just bootstrap` / `just setup-full` / `just heal` / `just doctor` — the turnkey environment.

## Development setup

```bash
Expand Down
181 changes: 168 additions & 13 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export METAMANIFOLD_REPO_DIR := justfile_directory()

FRONTEND := justfile_directory() / "frontend"

# Integration helper (fork↔upstream profiles, triage, component toggles).
INTEGRATE := justfile_directory() / "scripts/integrate.sh"

# Re-anchor helper (turn the divergent histories into a granular, mergeable one).
REANCHOR := justfile_directory() / "scripts/reanchor.sh"

# Free-RAM floor (KB) for the heavy Julia lanes: cold JIT-compilation of the
# server dependency closure needs several GB; below this the lane fails
# loudly instead of thrashing the box into an OOM kill.
Expand Down Expand Up @@ -77,18 +83,42 @@ info:
doctor:
#!/usr/bin/env bash
rc=0
# Hard requirement: absent => FAIL and non-zero exit.
need() {
if command -v "$1" >/dev/null 2>&1; then printf 'PASS %-10s %s\n' "$1" "$($1 --version 2>&1 | head -1)";
else printf 'FAIL %-10s %s\n' "$1" "$2"; rc=1; fi
if command -v "$1" >/dev/null 2>&1; then printf 'PASS %-12s %s\n' "$1" "$($1 --version 2>&1 | head -1)";
else printf 'FAIL %-12s %s\n' "$1" "$2"; rc=1; fi
}
# Soft requirement: absent => WARN, exit stays 0 (a documented lane is just unavailable).
soft() {
if command -v "$1" >/dev/null 2>&1; then printf 'PASS %-12s %s\n' "$1" "$($1 --version 2>&1 | head -1)";
else printf 'WARN %-12s %s\n' "$1" "$2"; fi
}
need bun "install: curl -fsSL https://bun.sh/install | bash"
need bun "install: curl -fsSL https://bun.sh/install | bash (or: just setup-tools)"
need git "install via package manager"
soft bunx "ships with bun; if absent reinstall bun"
soft node "needed by vite's production build: just setup-tools"
soft mise "toolchain pins (mise.toml): curl https://mise.run | sh (Guix lane is the alternative)"
if $JULIA_CMD --version >/dev/null 2>&1; then
printf 'PASS %-10s %s\n' "julia" "$($JULIA_CMD --version)"
printf 'PASS %-12s %s\n' "julia" "$($JULIA_CMD --version)"
else
printf 'WARN %-12s %s\n' "julia" "Julia lanes unavailable — install via juliaup (install.sh) or just setup-tools"
fi
if command -v Rscript >/dev/null 2>&1; then
printf 'PASS %-12s %s\n' "R" "$(Rscript --version 2>&1 | head -1)"
[ -f renv/activate.R ] && echo "PASS renv renv/activate.R present (restore with: just renv-restore)" \
|| echo "WARN renv renv/activate.R missing — R lane cannot restore"
else
printf 'WARN %-10s %s\n' "julia" "Julia lanes unavailable — install via juliaup (install.sh)"
printf 'WARN %-12s %s\n' "R" "system R >= 4.5 not found (documented exception; not in mise registry)"
fi
[[ -x "{{LAUNCHER}}" ]] && echo "PASS launcher {{LAUNCHER}}" || { echo "WARN launcher not executable: {{LAUNCHER}}"; }
# Merge drivers make lockfiles auto-resolve on the next fork↔upstream merge.
if git config --get merge.lockfile.driver >/dev/null 2>&1; then
echo "PASS merge-drv merge.lockfile wired (just merge-drivers)"
else
echo "WARN merge-drv not wired — run: just merge-drivers"
fi
[[ -x "{{LAUNCHER}}" ]] && echo "PASS launcher {{LAUNCHER}}" || { echo "WARN launcher not executable: {{LAUNCHER}}"; }
echo "-----"
echo "Integration profile: $({{INTEGRATE}} profile 2>/dev/null || echo base)"
exit $rc

# Quick repo statistics.
Expand All @@ -112,8 +142,8 @@ setup: install
# mise.toml (julia 1.12.5, bun 1.3.10, node 20.20.2, just 1.43.1), then
# install frontend dependencies. R is a documented exception: system R +
# renv.lock (R is not in the mise registry — verified 2026-09-18).
bootstrap: setup-tools install codegen-tools hooks
@echo "bootstrap: toolchain + deps + machine tool map ready — next: just ci"
bootstrap: setup-tools install codegen-tools hooks merge-drivers
@echo "bootstrap: toolchain + deps + hooks + merge drivers + machine tool map ready — next: just ci"

# Point git at .githooks so the commit-msg gate actually runs. core.hooksPath is
# per-clone local config -- it cannot be committed -- so documenting it in
Expand All @@ -124,6 +154,26 @@ hooks:
@git config core.hooksPath .githooks
@echo "hooks: core.hooksPath -> .githooks (commit-msg gate live)"

# Wire a git merge driver that keeps lockfiles / generated files out of the
# fork↔upstream conflict set. merge.lockfile auto-resolves such a path to the
# branch being merged INTO (ours) and reminds you to regenerate — never a
# line-merged lockfile. Applied via .git/info/attributes (local, overrides the
# tree, never committed) so it is fully opt-in and cannot break a merge on a
# clone that has not run it. The committed .gitattributes already stops git from
# line-merging these (merge: unset); this just makes the choice automatic.
# Local git config, like core.hooksPath — hence a command, not a committed file.
# Idempotent; safe to re-run.
merge-drivers:
#!/usr/bin/env bash
git config merge.lockfile.name "keep target-branch lockfile, then regenerate (just heal)"
git config merge.lockfile.driver 'echo "merge-drivers: kept target-branch copy of %P — regenerate with: just heal" >&2'
attrs="{{justfile_directory()}}/.git/info/attributes"
mkdir -p "$(dirname "$attrs")"; touch "$attrs"
for p in Manifest.toml renv.lock frontend/bun.lock bun.lockb package-lock.json pnpm-lock.yaml renv/activate.R; do
grep -qxF "$p merge=lockfile" "$attrs" 2>/dev/null || printf '%s merge=lockfile\n' "$p" >> "$attrs"
done
echo "merge-drivers: merge.lockfile wired for lockfiles via .git/info/attributes (opt-in, local)"

# Provision the pinned toolchain via mise (fail-loud with the installer
# one-liner when mise is absent; the Guix lane in guix.scm is the
# alternative, see docs/reproducibility.md).
Expand Down Expand Up @@ -167,11 +217,12 @@ codegen-tools:
./scripts/gen-tools-yml.sh

# Complete first-run on a bare machine, clone-to-launchable in one recipe:
# toolchain + JS deps + machine tool map (bootstrap), Julia package
# instantiate, then install.sh's sha256-pinned external pipeline tools.
# After this: just start. (install-tools downloads several hundred MB by
# design — skip it when you only develop the frontend.)
setup-full: bootstrap julia-instantiate install-tools
# toolchain + JS deps + machine tool map + hooks + merge drivers (bootstrap),
# Julia package instantiate, R package restore (renv), then install.sh's
# sha256-pinned external pipeline tools. After this: just start.
# (install-tools downloads several hundred MB by design — skip it when you only
# develop the frontend.)
setup-full: bootstrap julia-instantiate renv-restore install-tools
@echo "setup-full: complete — launch with: just start"

# Pipeline tools via the byte-exact lane: install.sh fetches the archives
Expand All @@ -191,6 +242,18 @@ outdated:
julia-instantiate:
$JULIA_CMD --project=. -e 'using Pkg; Pkg.instantiate(); println("instantiate OK")'

# Restore the R package set from renv.lock (byte-exact; the R lane). Requires
# system R >= 4.5 (documented exception — R is not in the mise registry). Fails
# loudly if R is absent rather than silently skipping the lane.
renv-restore:
#!/usr/bin/env bash
if ! command -v Rscript >/dev/null 2>&1; then
echo "R LANE UNAVAILABLE: system R (>= 4.5) not found." >&2
echo "Install R for your OS, then re-run: just renv-restore" >&2
exit 1
fi
Rscript --no-init-file -e 'if (!requireNamespace("renv", quietly=TRUE)) { message("installing renv..."); install.packages("renv", repos="https://cloud.r-project.org") }; renv::restore(prompt=FALSE)'

# ----------------------------------------------------------------------- #
# Hygiene gates (scripts/check-*.sh — the canonical bash lanes)
# ----------------------------------------------------------------------- #
Expand Down Expand Up @@ -389,3 +452,95 @@ clean:
# Remove generated outputs AND installed dependencies.
clean-all: clean
rm -rf frontend/node_modules

# ----------------------------------------------------------------------- #
# Integration & environment healing (fork↔upstream)
#
# The fork and upstream share no git ancestor, so a naive merge conflicts on
# every shared path. These recipes expose config/integration.toml as a set of
# trust decisions the maintainer can make incrementally — from "behave exactly
# like upstream" (base) to "everything verified" (full) — without ever
# compromising a running system: the default profile changes no behaviour.
# Engine: scripts/integrate.sh. Guide: docs/integration/README.md.
# ----------------------------------------------------------------------- #

# Repair the local environment to a known-good state: re-sync repo pins, re-wire
# hooks + merge drivers, regenerate the machine tool map, reinstall frontend
# deps, and (where present) re-instantiate Julia and restore the R lockfile.
# Resilient by design — each lane is attempted and a failure is reported, not
# fatal. Idempotent. The "fix my box" one-shot.
heal:
#!/usr/bin/env bash
set -uo pipefail
echo "heal: re-syncing repo pins..."; just sync-pins || echo "heal: sync-pins skipped"
echo "heal: re-wiring hooks + merge drivers..."; just hooks merge-drivers
echo "heal: regenerating machine tool map..."; just codegen-tools || echo "heal: codegen-tools skipped"
echo "heal: reinstalling frontend deps..."; just install || echo "heal: install skipped"
if timeout 15 $JULIA_CMD --version >/dev/null 2>&1; then
echo "heal: re-instantiating Julia..."; just julia-instantiate || echo "heal: julia-instantiate skipped"
else
echo "heal: Julia absent — provision the pinned toolchain with: just setup-tools"
fi
if command -v Rscript >/dev/null 2>&1; then
echo "heal: restoring R lockfile..."; just renv-restore || echo "heal: renv-restore skipped"
else
echo "heal: R absent — R lane left untouched (documented exception)"
fi
echo "heal: done. Verify with: just doctor"

# Integration profiles & component toggles (thin wrappers over scripts/integrate.sh).
integrate: integrate-status

integrate-status:
@{{INTEGRATE}} status

integrate-profiles:
@{{INTEGRATE}} profiles

# Switch the active profile: just integrate-profile <base|transitional|full>.
integrate-profile profile="base":
@{{INTEGRATE}} profile "{{profile}}"

# Recommended staging order (safest → riskiest).
integrate-plan:
@{{INTEGRATE}} plan

# Classify in-progress merge conflicts (auto / component / human).
integrate-triage:
@{{INTEGRATE}} triage

# Gates for the active selection; add strict="--strict" to require the tools be present.
integrate-verify strict="":
@{{INTEGRATE}} verify {{strict}}

# Suspend a component: it stops being active (if runtime-gated, it will refuse).
suspend component:
@{{INTEGRATE}} disable "{{component}}"

# Augment a component: it becomes active for this checkout.
augment component:
@{{INTEGRATE}} enable "{{component}}"

# ----------------------------------------------------------------------- #
# Re-anchoring — collapse the one-shot merge into granular per-commit work
#
# The fork and upstream share the root commit but diverged early and developed
# in parallel, so a single merge shows ~159 conflicts at once. `reanchor` replays
# the fork's commits one-by-one onto upstream (git auto-applies the clean ones),
# turning that wall into a handful of small decisions. Measured: the whole fork
# re-anchors with 3 decisions and 0 residual conflicts, producing ~206 granular
# commits the maintainer can review/merge incrementally. Runs in an isolated
# worktree — it never touches your current branch. Engine: scripts/reanchor.sh.
# ----------------------------------------------------------------------- #

# Read-only plan: classify each fork commit (auto-apply / overlap / delete-risk).
reanchor-plan:
@{{REANCHOR}} plan

# Perform the re-anchor; leave a reviewable branch `reanchor/onto-upstream`.
reanchor:
@{{REANCHOR}} run --branch reanchor/onto-upstream --keep

# Re-anchor but STOP at every conflict for hands-on resolution.
reanchor-manual:
@{{REANCHOR}} run --policy manual --keep
Loading
Loading