# Origin reconciliation — three-way merge algorithm

This document goes deep on what `SKILL.md` summarizes: when `catalyst generate` leaves `.origin` files behind, the AI agent must reconcile three potential authors of the divergent content. Read this before acting on `.origin` files in any non-trivial regen.

---

## Mental model

After a regen with `--force` (or after any regen that ran on a file with a hash mismatch), the filesystem looks like this:

```
<file>                  ← current content on disk. Untouched by this regen.
                          Was authored by some combination of:
                          (a) a previous codegen run
                          (b) a human developer
                          (c) you, the AI, this session

<file>.origin.<ext>     ← what catalyst proposes to write now. Already merged
                          with the user's preservation-region bodies from
                          <file>, but the skeleton is the new template output.
```

Your job is to decide which version wins, possibly per hunk. The right answer depends on which of the three potential authors contributed which hunk.

### Why `.origin` exists at all

The codegen tracks, per file, a `skeleton hash` in the lockfile (`<bc>.<module>.<scope>.lock.json`). The skeleton is the file content with preservation regions stripped out. On every regen:

1. The current skeleton hash on disk is computed.
2. If it matches the stored hash → the file is regenerated transparently (preservation regions preserved; skeleton replaced; new skeleton hash stored). No `.origin`.
3. If it does NOT match → someone edited the file outside the preservation regions. The codegen does **not** touch `<file>`; it writes the proposal to `<file>.origin.<ext>` and lets you reconcile.

So the existence of a `.origin` file is a signal: **the skeleton of this file diverged from what the codegen thinks it produced**. Something happened outside the safe preservation regions.

---

## Who wrote what — the three authors

| Author | Typical signature | How to detect |
|---|---|---|
| **Codegen** (previous run) | Boilerplate, type imports, module scaffolding, signal API shells, exported class declarations, route resolver shape | These are the parts that match the surrounding code style and pattern. They tend to live on the "skeleton" side. |
| **User** (human developer) | Business logic, custom validators, domain-specific labels, integration code, comments that explain *why* not *what*, anything inside `AURORA:*-START/END` markers | `git log -p <file>` shows the developer's commits. `git blame <file>` attributes lines. |
| **AI session** (you, now) | Anything that landed in `<file>` during this conversation, before the regen ran | Check the conversation history. Also: `git diff HEAD <file>` — if the file is modified but not committed and the diff aligns with what you wrote, it is yours. |

The three are not always cleanly separable. A user might have edited code that the AI wrote earlier; the AI might have refactored code originally produced by the codegen. The point of the analysis is not to assign blame — it is to decide which version is the *better starting point* for the file going forward.

---

## The reconciliation algorithm

For each `.origin` file:

### Step 1 — Categorize the divergence

Run `catalyst origin diff <path>` and read the unified patch end-to-end. Mentally tag each hunk:

- **(C) Codegen-style** — looks like template output: imports, type annotations, scaffolding boilerplate, new shape introduced because the template evolved.
- **(U) User-custom** — looks like hand-written domain logic, custom labels, business rule code, comments authored by a human.
- **(A) AI-this-session** — recognise content you wrote during this conversation.
- **(?) Unknown** — cannot tell.

### Step 2 — Apply the decision matrix

| Composition of hunks | Recommended action |
|---|---|
| All (C) | `catalyst origin accept <path>` — pure template improvement, adopt outright |
| All (U) | `catalyst origin reject <path>` — user code that the new template would clobber; keep current |
| All (A) | If the AI hunks belong inside a preservation region that exists in the new proposal → `accept` (the merge already preserved them). If they live OUTSIDE preservation regions and the new template overlaps them → do the three-way merge (Step 3). If the AI work is no longer needed → `reject`. |
| Mix of (C) and (U) | Three-way merge (Step 3). Default to safety: rather lose the template improvement than lose user code. |
| Mix of (C) and (A) | Three-way merge (Step 3). The AI knows what it wrote, so re-applying is feasible. |
| Mix of (U) and (A) | Three-way merge (Step 3). |
| Contains any (?) | **Stop. Ask the user.** Do not guess. |

### Step 3 — The three-way merge

When the divergence mixes authorship, the correct procedure is:

```
1. Save what you have:
     git stash push <path>         # or commit to a wip branch
     # this saves the current modified state, leaving HEAD content on disk

2. Adopt the codegen proposal:
     catalyst origin accept <path>
     # <file>.origin.<ext> is renamed over <file>; you now have the fresh
     # template output as your starting point

3. Re-apply the saved hunks on top of the new scaffolding:
     git stash pop                  # or git checkout wip-branch -- <path>
     # this attempts the merge; resolve conflicts manually

4. Verify the result:
     - The user/AI hunks are preserved
     - The codegen improvements are present
     - The file compiles (run the typecheck or test for the touched module)
```

If step 3 produces merge conflicts you cannot confidently resolve, stop and ask the user. A bad three-way merge is worse than a missed template improvement — the latter is recoverable on the next regen, the former destroys work.

---

## Session-aware merge — when YOU wrote some of the divergence

This is the case the original CLAUDE.md emphasises and the easiest one to get wrong.

If the AI made changes to a file during the current conversation and then regenerated the module, the regen's `.origin` will contain the new template proposal **without** the AI's session edits. The AI has perfect knowledge of what it wrote this session — it can see the conversation. So:

1. **Inventory**: list the files you (the AI) touched this session. `git status --short` shows everything modified vs HEAD; cross-reference with the messages where you used `Edit`/`Write`.
2. **For each `.origin` pair on a file you touched**:
   - Read the conversation. What was your intent in editing this file? Was it:
     - **A**: a customization that should now go into the YAML schema? → put it in the YAML, accept the `.origin`, regenerate.
     - **B**: a customization that has no YAML home (genuine business logic in a service, etc.)? → three-way merge: stash your edits, accept origin, re-apply.
     - **C**: a fix for a bug in the previous codegen output that the new template also fixes? → just `accept`. Your fix was made redundant by the template improvement.
3. **Document** in the conversation what you concluded for each file, so the user can audit.

The most common mistake: blindly running `catalyst origin accept` on a subtree after a regen, thereby erasing edits the AI itself made minutes earlier. **Never bulk-accept** in a session that included edits to generated files. Run `git status` first; if files you edited appear among the `.origin` companions, treat them individually.

---

## When to `origin ignore` instead

`catalyst origin ignore <path>` prepends an `// ignored file` (or `<!-- -->`, `# ` depending on extension) marker and deletes the `.origin`. The next regen will skip the file entirely. Use it when:

- The file has diverged so far from the codegen that there is no useful overlap left.
- The file is now wholly hand-maintained code that just happens to live where the codegen used to emit something.
- You have decided to fork this file permanently from the template.

**Do not** use `ignore` to escape from a reconciliation you found inconvenient. If the divergence is "the user changed the icon by hand", the right answer is to put the icon in the YAML and regenerate — not to ignore the file forever.

---

## Pitfalls

- **The `.origin` file already contains user preservation regions.** The codegen merges them in *before* writing `.origin`, so the proposal you see is not the raw template output. If a region's body is identical in current and origin, it is because the merge preserved it — that hunk is not a divergence the merge engine cares about.
- **`accept` is atomic on POSIX**, non-atomic on Windows (`fs.renameSync` does not overwrite there; the implementation falls back to `unlink` + `rename`). If you are on Windows and the host crashes mid-`accept`, the current file may be missing for a millisecond. Acceptable; documented.
- **A `noop` from `accept`/`reject`/`ignore` is not an error**, it means there was no `.origin` at the path (idempotent). If you expected one and got `noop`, the file was already reconciled or never had divergence.
- **`origin review` (interactive) and `origin <verb>` call the same domain functions.** A bug fix in one applies to both. They are not parallel implementations.

---

## Worst-case escape hatch

If a reconciliation goes wrong and you are not sure what state the filesystem is in:

```
git status --short                     # see what is modified vs HEAD
git diff <file>                        # see what changed
git checkout <file>                    # discard all changes, return to HEAD
catalyst generate <scope> module --name=<bc>/<module>   # regenerate from scratch
```

`git` is the safety net. The `.origin` files are not tracked (unless you committed them by mistake — clean them out: `find . -name '*.origin.*' -delete` is brute but effective).
