Skip to content
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ Between releases: see `git log` for merged work and [`ROADMAP.md`](ROADMAP.md) f

## [Unreleased]

### Added (adopt batch operations — #120)

Closes [#120](https://github.com/breferrari/shardmind/issues/120). `shardmind adopt` against a divergent vault asked for a decision on *every* differing file (the flagship obsidian-mind run had 35). A top-level mode picker now resolves the whole set at once, with per-file prompting kept as a mode.

- **Mode picker** (`source/components/AdoptModePicker.tsx`) shown once before the per-file loop when files differ and neither `--mode` nor `--yes` is set: **Keep all mine** / **Use all theirs** / **Auto-merge (best-effort)** / **Decide per file**. A `--mode=keep-all-mine|use-all-theirs|auto-merge|decide-per-file` flag drives it non-interactively (overrides `--yes`, which is now shorthand for keep-all-mine).

- **Auto-merge is a best-effort two-way *union* merge** (`source/core/adopt-merge.ts`), not a three-way merge: adopt has no merge base (the vault was cloned without shardmind), so a base-anchored merge is impossible. It keeps common lines, unions each side's unique lines, and sends files where both sides replaced the same span to the per-file prompt. **Its limits are surfaced, not hidden:** it keeps the user's bytes so it **does not apply shard deletions**, and can duplicate non-adjacent edits. Merged files are written as `modified` ownership and flagged **"review recommended"** in the Summary. Non-interactive auto-merge keeps-mine on conflicts.

- **Executor** gains a `{ kind:'merged', content, hash }` resolution (`adopt-executor.ts`): writes the union bytes, records `modified` ownership at the merged hash, snapshots merged paths for rollback, and buckets them in `summary.adoptedMerged`. A subsequent `update` three-way-merges the result against the cached shard template (the proper base). The machine's `diff-review` phase now iterates a `queue` (only the files needing a prompt) with pre-seeded resolutions, so per-file and auto-merge share one loop.

- **Tests**: `adopt-merge` unit matrix + determinism/union properties (fast-check); executor merged-resolution integration test; `AdoptModePicker` component test (modes + firedRef guard); Layer 1 flow scenarios 27-30 (keep-all / use-all / auto-merge mixed conflict / `--mode` non-interactive); Layer 2 PTY scenario picks decide-per-file; `AdoptSummary` merged-bucket test. **Docs**: `docs/ARCHITECTURE.md §10.5` (modes + best-effort note), `docs/AUTHORING.md`, `CLAUDE.md`.

### Changed (hook lifecycle split — #102) — **breaking, shard-author-facing**

Part of [#102](https://github.com/breferrari/shardmind/issues/102) (the engine + fixture + docs; the issue stays open until the release-window items below land). Splits the single `post-install` hook into three named slots with engine-enforced contracts, turning yesterday's comment-checked conventions into machine-checked signals. Ships in the same release as [#121](https://github.com/breferrari/shardmind/issues/121) (engine version-compatibility check), which is the guard that stops a pre-0.2 engine from silently ignoring the new slots; obsidian-mind's hook migration ([`obsidian-mind#75`](https://github.com/breferrari/obsidian-mind/issues/75)) ships in the same window.
Expand Down
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ shardmind/
│ │ ├── ExistingInstallGate.tsx # Install: existing-install disambiguation
│ │ ├── DiffView.tsx # Three-way diff + conflict resolution
│ │ ├── AdoptValuesGate.tsx # Adopt: values confirm-or-override page (#104)
│ │ ├── AdoptModePicker.tsx # Adopt: batch mode picker (keep-all / use-all / auto-merge / per-file) (#120)
│ │ ├── AdoptDiffView.tsx # Adopt: 2-way diff (no merge base) + per-file prompt
│ │ ├── AdoptSummary.tsx # Final adopt report (matched / mine / shard / fresh)
│ │ ├── NewValuesPrompt.tsx # Update: prompt for newly required values
Expand Down Expand Up @@ -180,6 +181,7 @@ shardmind/
│ │ ├── install-executor.ts # Apply install plan with rollback
│ │ ├── adopt-planner.ts # Classify user vault vs shard (matches/differs/shard-only)
│ │ ├── adopt-executor.ts # Apply adopt plan with snapshot-rollback
│ │ ├── adopt-merge.ts # Two-way union merge for adopt --mode=auto-merge (#120)
│ │ ├── values-io.ts # Shared YAML load for shard-values.yaml
│ │ ├── values-defaults.ts # `valuesAreDefaults(values, schema)` — Invariant 2 helper
│ │ ├── update-check.ts # 24h cached latest-version lookup (status + update)
Expand Down Expand Up @@ -338,6 +340,7 @@ Each file in `source/core/` maps 1:1 to a section in `docs/IMPLEMENTATION.md`:
| `update-executor.ts` | §4.12 | Apply update plan with snapshot-based rollback |
| `adopt-planner.ts` | §4.17 | Classify user vault vs shard outputs (matches / differs / shard-only) |
| `adopt-executor.ts` | §4.18 | Apply adopt plan with snapshot-based rollback |
| `adopt-merge.ts` | SHARD-LAYOUT.md §Adopt semantics | Two-way union merge for `adopt --mode=auto-merge` (#120) |
| `values-io.ts` | §4.13 | Shared YAML load for shard-values.yaml (install + update) |
| `values-defaults.ts` | §4.16 (HookContext extensions) | `valuesAreDefaults(values, schema)` — deep-equal pure fn for Invariant 2 |
| `status.ts` | §4.14 | Pure StatusReport builder for the `shardmind` (status) command |
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ UX gaps surfaced during real obsidian-mind v6 install + adopt runs. None block t
- [x] Wizard scroll indicator + boolean prompt consistency (Y/n typed input → selectable Yes/No) ([#100](https://github.com/breferrari/shardmind/issues/100)) — smallest item, recommended starting point.
- [x] `multiselect` value type for module-set questions ([#101](https://github.com/breferrari/shardmind/issues/101)) — pairs with #100 (shortens module list). First-class value type + scrollable widget + per-option `default` + min/max; value→module gating stays #80.
- [x] Adopt: replace step-by-step install wizard with confirm-or-override values flow ([#104](https://github.com/breferrari/shardmind/issues/104))
- [ ] Adopt batch operations (keep all mine / use all theirs / auto-merge non-conflicting) ([#120](https://github.com/breferrari/shardmind/issues/120)) — pairs with #104.
- [x] Adopt batch operations (keep all mine / use all theirs / auto-merge non-conflicting) ([#120](https://github.com/breferrari/shardmind/issues/120)) — pairs with #104.
- [ ] Hook stderr presentation in Summary (truncate, dim, label as non-fatal) ([#105](https://github.com/breferrari/shardmind/issues/105))

### 0.1.x — Done gate
Expand Down
13 changes: 9 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -875,15 +875,20 @@ Phase ordering (logical; UI may interleave loading messages — see IMPLEMENTATI
2. **Values gate** (`source/components/AdoptValuesGate.tsx`, #104) — adopt's user already has a populated vault, so it opens on a single confirm page that surfaces the values that will drive classification (resolved defaults, computed defaults, `--values` prefill, each labelled with its provenance) plus the module default, with **Use these values / Override individually / Cancel**. "Override individually" drops into the same `InstallWizard` install uses (per-value + module editing). Required values with no default and no prefill open straight into the wizard (no confident set to confirm). This step runs **before** classification because `.njk` templates need values to render before their output bytes can be hashed.
3. **Classify** (`source/core/adopt-planner.ts::classifyAdoption`) — for every shard output, render or read, hash, stat the user's vault, and assign one of four buckets:
- **matches** — byte-identical post-render → managed silently.
- **differs** — bytes differ → 2-way diff prompt (`AdoptDiffView`); user picks `keep_mine` (record user's hash, ownership=`modified`) or `use_shard` (overwrite with shard bytes, ownership=`managed`).
- **differs** — bytes differ → resolved per the chosen batch mode (below); `keep_mine` records the user's hash (ownership=`modified`), `use_shard` overwrites with shard bytes (ownership=`managed`), `merged` writes union bytes (ownership=`modified`).
- **shard-only** — user doesn't have the path → install fresh, managed.
- Implicit **user-only** — paths in vault but not in shard → never enumerated, left untouched.
4. **Apply** (`source/core/adopt-executor.ts::runAdopt`) — snapshot any `differs+use_shard` user file before overwriting, write shard-only fresh installs, write engine metadata (`state.json`, cached manifest+schema, templates cache, vault-root `shard-values.yaml`). Snapshot-then-restore rollback on any failure between snapshot and final state-write.
3a. **Mode select** (`source/components/AdoptModePicker.tsx`, #120) — when there is at least one `differs` file and neither `--mode` nor `--yes` is set, adopt prompts once for how to resolve the whole set rather than file-by-file. Four modes:
- **Keep all mine** / **Use all theirs** — resolve every `differs` one way.
- **Auto-merge (best-effort)** — two-way-union each file (`source/core/adopt-merge.ts`): non-conflicting files resolve to their merged bytes; files where both sides replaced the same span fall through to the per-file prompt. **This is a union merge with no merge base** (adopt has no record of the original shard version): it keeps the user's bytes, **does not apply shard deletions**, and can duplicate non-adjacent edits — merged files are flagged "review recommended" in the Summary. Non-interactive auto-merge (`--mode=auto-merge` under `--yes`) keeps-mine on conflicts.
- **Decide per file** — the per-file `AdoptDiffView` prompt (`keep_mine` / `use_shard`).
4. **Apply** (`source/core/adopt-executor.ts::runAdopt`) — snapshot any user file an apply will overwrite (`differs+use_shard` and `differs+merged`), write shard-only fresh installs, write engine metadata (`state.json`, cached manifest+schema, templates cache, vault-root `shard-values.yaml`). Snapshot-then-restore rollback on any failure between snapshot and final state-write.
5. **Hooks** — run the install-side slots via the orchestrator: `bootstrap` (unmanaged setup), then `personalize` (managed edits) unless `valuesAreDefaults` (engine-skipped, Invariant 2). `newFiles=summary.installedFresh`. Non-fatal (Helm semantics, §9.3).
6. **Re-hash** — recompute managed-file hashes per `state.ts::rehashManagedFiles` so any hook edits to managed paths land in the recorded state.

Flags:
- `--yes` — skip the values gate + auto-pick `keep_mine` on every `differs`. Preserves the user's bytes on every divergence; safe default for retroactive adoption.
- `--yes` — skip the values gate + the mode picker; resolve every `differs` as `keep_mine` (shorthand for `--mode=keep-all-mine`). Preserves the user's bytes on every divergence; safe default for retroactive adoption.
- `--mode <keep-all-mine|use-all-theirs|auto-merge|decide-per-file>` (#120) — choose how to resolve the `differs` set, skipping the picker. `keep-all-mine`/`use-all-theirs`/`auto-merge` are non-interactive (`auto-merge` still prompts on conflicts unless `--yes` is also set, which keeps-mine on them); `decide-per-file` is the per-file prompt loop. Overrides `--yes`'s default. The flag governs the diff set, not value collection — combine with `--yes`/`--values` for a fully non-interactive run.
- `--values <file>` — prefill value answers (same shape as `install --values`); shown on the gate's confirm page as `(from --values)`.
Comment on lines 889 to 892
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7f0da50. Same reword applied to the ARCHITECTURE §10.5 flag description so the docs match the implementation (decide-per-file prompts; auto-merge prompts on conflicts unless --yes).

- `--verbose` — show per-file action history during the apply phase.
- `--dry-run` — run the full pipeline (fetch, values gate, classify) without touching the vault. Summary reports what *would* happen.
Expand All @@ -892,7 +897,7 @@ Volatile templates (`{# shardmind: volatile #}`) skip the differs prompt entirel

Excluded modules' files in the user's vault are not classified — adopt mirrors install's "module excluded → file not installed" rule, so user content at those paths stays user-content without any prompt.

Implementation modules: `source/core/adopt-planner.ts` (IMPLEMENTATION §4.17), `source/core/adopt-executor.ts` (§4.18). Orchestration lives in `source/commands/hooks/use-adopt-machine.ts`; UI components are `source/components/AdoptValuesGate.tsx` (values confirm-or-override), `source/components/AdoptDiffView.tsx` + `source/components/AdoptSummary.tsx`.
Implementation modules: `source/core/adopt-planner.ts` (IMPLEMENTATION §4.17), `source/core/adopt-executor.ts` (§4.18), `source/core/adopt-merge.ts` (two-way union merge for auto-merge mode). Orchestration lives in `source/commands/hooks/use-adopt-machine.ts`; UI components are `source/components/AdoptValuesGate.tsx` (values confirm-or-override), `source/components/AdoptModePicker.tsx` (batch mode picker), `source/components/AdoptDiffView.tsx` + `source/components/AdoptSummary.tsx`.

### 10.6 Install Location

Expand Down
2 changes: 1 addition & 1 deletion docs/AUTHORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ A shard ships:

Users run `shardmind install <namespace>/<shard>`. The engine downloads the tarball, walks the shard root applying Tier 1 exclusions + `.shardmindignore` + symlink rejection, prompts for values, lets the user opt out of removable modules, renders + copies + caches, writes `state.json`, and runs your hook. Users never edit your shard directly — they edit their vault, and the next `shardmind update` merges upstream changes into their customizations via three-way merge.

A user who already cloned your repo (typically before shardmind support existed) can run `shardmind adopt <namespace>/<shard>` instead. Adopt walks the existing vault, classifies each shard-output path against what the install would have produced, and asks the user — for any file that differs — whether to keep their version (recorded as `ownership: 'modified'`) or accept the shard's. Files matching exactly are auto-managed; files only the shard ships are installed fresh. After adopt, `shardmind update` works normally — the cache made at adopt time becomes the merge base. See `docs/SHARD-LAYOUT.md §Adopt semantics` for the full contract.
A user who already cloned your repo (typically before shardmind support existed) can run `shardmind adopt <namespace>/<shard>` instead. Adopt walks the existing vault, classifies each shard-output path against what the install would have produced, and — for any file that differs — lets the user resolve the whole set at once (keep all mine / use all theirs / best-effort auto-merge / decide per file, or the `--mode` flag) before recording each file's `ownership` (`modified` for kept/merged, `managed` for use-shard). Files matching exactly are auto-managed; files only the shard ships are installed fresh. After adopt, `shardmind update` works normally — the cache made at adopt time becomes the merge base. See `docs/SHARD-LAYOUT.md §Adopt semantics` for the full contract.

## 2. File layout

Expand Down
16 changes: 11 additions & 5 deletions docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,17 +163,23 @@ graph TD
E["adopt-planner.ts::classifyAdoption<br/>resolveModules walks shard, render or read each output<br/>per-output sha256 vs sha256(user vault path)<br/>→ matches | differs | shard-only buckets"] --> F1

F1{Any<br/>differs?}
F1 -->|Yes, --yes| F2["auto-resolve every differs as keep_mine"]
F1 -->|Yes, interactive| F3["diff-review loop<br/>AdoptDiffView per file → keep_mine / use_shard"]
F1 -->|"Yes, --mode / --yes"| F2["applyMode (non-interactive)<br/>keep-all-mine / use-all-theirs /<br/>auto-merge (conflicts→keep_mine) / —"]
F1 -->|Yes, interactive| FM["mode-select — AdoptModePicker (#120)<br/>keep-all-mine · use-all-theirs ·<br/>auto-merge · decide-per-file"]
F1 -->|No| G

FM -->|keep-all / use-all| G
FM -->|"auto-merge"| FA["adopt-merge.ts::twoWayUnionMerge<br/>non-conflicting → merged bytes<br/>conflicting → queue"]
FM -->|decide-per-file| F3
FA -->|conflicts| F3
FA -->|no conflicts| G
F3["diff-review loop (queue)<br/>AdoptDiffView per file → keep_mine / use_shard"] --> G

F2 --> G
F3 --> G

G["adopt-executor.ts::runAdopt<br/>① Snapshot every differs+use_shard user file<br/>② Apply per classification + resolution<br/>③ Cache templates + manifest + schema<br/>④ Write shard-values.yaml + state.json"] --> H
G["adopt-executor.ts::runAdopt<br/>① Snapshot every differs+use_shard / differs+merged user file<br/>② Apply per classification + resolution (keep_mine / use_shard / merged)<br/>③ Cache templates + manifest + schema<br/>④ Write shard-values.yaml + state.json"] --> H

H["hooks (orchestrator)<br/>bootstrap → personalize (non-fatal)<br/>newFiles = summary.installedFresh"] --> I
I["summary — AdoptSummary<br/>Counts: matched-auto / kept-mine / use-shard / fresh<br/>+ hook output"]
I["summary — AdoptSummary<br/>Counts: matched-auto / kept-mine / use-shard / merged / fresh<br/>+ hook output"]

G -->|Any failure| R["rollbackAdopt<br/>Restore snapshot + erase added paths<br/>+ drop .shardmind/ + shard-values.yaml"]

Expand Down
Loading
Loading