Replace log.md + merge=union with one file per entry #1

Closed
opened 2026-09-08 15:46:54 +02:00 by kristofferopsahl · 1 comment

Summary

wiki/log.md is a single append-only file kept mergeable by wiki/log.md merge=union
in .gitattributes. That works locally, but it has two failure modes — one that
removes most of the benefit in a PR-based flow, and one that silently reverts data.

I suggest replacing the single file with one file per log entry, which makes the
merge problem structurally impossible and needs no merge driver at all.

Failure mode 1 — GitHub ignores .gitattributes merge drivers

merge=union applies to local git merge and git rebase. It does not apply to
the mergeability GitHub computes for a pull request. So two branches that each append a
line still show "This branch has conflicts that must be resolved" on the PR page, and
the author has to merge the base branch locally and push to clear it.

The kit's promise — "auto-merges via merge=union" in WIKI.md — is therefore only
true for local operations, which is not where most people meet the conflict.

Minimal repro (no .gitattributes, i.e. what GitHub effectively does):

git init repo && cd repo
mkdir wiki && printf -- '- entry A\n' > wiki/log.md
git add -A && git commit -m base && BASE=$(git rev-parse HEAD)
git checkout -b b1 && printf -- '- from b1\n' >> wiki/log.md && git commit -am b1
git checkout -b b2 $BASE && printf -- '- from b2\n' >> wiki/log.md && git commit -am b2
git merge b1     # CONFLICT in wiki/log.md

With the merge=union attribute present the same merge is clean — which is precisely
the discrepancy: clean locally, conflicted on GitHub.

Failure mode 2 — union silently reverts deletions

Union's rule is "keep both sides", so a deletion on one side always loses to
retention on the other. A line removed on one branch comes back with no conflict and
no warning:

git init repo && cd repo
printf 'log.md merge=union\n' > .gitattributes
printf -- '- keep\n- DELETE-ME\n' > log.md
git add -A && git commit -m base && B=$(git rev-parse HEAD)
git checkout -b del   && sed -i '' '/DELETE-ME/d' log.md && git commit -am remove
git checkout -b other $B && printf -- '- unrelated\n' >> log.md && git commit -am other
git merge del
grep DELETE-ME log.md   # still there

This is not hypothetical. In a downstream repo I renamed a wiki note and repointed its
log line to the new filename. A later rebase resurrected both superseded lines, still
pointing at the pre-rename path. No conflict, no warning, and the lint script cannot
catch it because log lines are plain text rather than markdown links — so a stale path
in the journal is invisible to every check the kit ships.

The counter-argument is fair: WIKI.md calls the log append-only, and editing a line
breaks that convention. But renames happen, the convention is not machine-enforced, and
a mechanism whose failure mode is silent data resurrection is a sharp edge worth
removing rather than documenting.

Proposal — one file per entry

wiki/log/2026-09-08-record-plugin-scope.md
wiki/log/2026-09-08-ingest-oauth-rfc.md

Each holds what one line holds today: date, verb, page, summary. The journal is the
directory; reading it is cat wiki/log/*.md or ls wiki/log/, both naturally ordered
by the date prefix.

What this buys:

  • Two branches never touch the same file, so there is nothing to merge — locally or
    on GitHub. The PR page stops disagreeing with git.
  • merge=union can be dropped entirely, and with it the deletion hazard.
  • Deleting or correcting an entry becomes a normal, visible file change.
  • The lint script can check entries the way it already checks pages — frontmatter,
    referenced page exists — which today it cannot do at all.

Costs, stated honestly:

  • The journal is no longer one scrollable file. cat wiki/log/*.md restores that, and a
    lint or skill step could regenerate a concatenated view if the single-file reading
    experience matters.
  • Migration: keep the existing log.md in place as the historical record and start the
    directory alongside it, rather than splitting old entries. That keeps the diff small
    and loses nothing.
  • More files in the tree. For a journal that gains a few entries per session this is the
    same trade raw/ and notes/ already make.

Alternatives considered

  • Keep union, enforce append-only strictly (never edit a line; on a rename append a
    new entry rather than repointing). Zero machinery, but leaves failure mode 1 entirely
    and relies on a convention no check enforces.
  • Drop the merge driver, accept normal conflicts. Honest and visible, but with
    several agent sessions running in parallel it means a conflict on nearly every branch.
  • Drop log.md altogether and rely on git log -- wiki/. Loses the curated
    one-line summaries, which read better than commit subjects.

Happy to send a PR for the directory layout plus the WIKI.md and lint changes if the
direction looks right.

## Summary `wiki/log.md` is a single append-only file kept mergeable by `wiki/log.md merge=union` in `.gitattributes`. That works locally, but it has two failure modes — one that removes most of the benefit in a PR-based flow, and one that silently reverts data. I suggest replacing the single file with **one file per log entry**, which makes the merge problem structurally impossible and needs no merge driver at all. ## Failure mode 1 — GitHub ignores `.gitattributes` merge drivers `merge=union` applies to local `git merge` and `git rebase`. It does **not** apply to the mergeability GitHub computes for a pull request. So two branches that each append a line still show *"This branch has conflicts that must be resolved"* on the PR page, and the author has to merge the base branch locally and push to clear it. The kit's promise — "auto-merges via `merge=union`" in `WIKI.md` — is therefore only true for local operations, which is not where most people meet the conflict. Minimal repro (no `.gitattributes`, i.e. what GitHub effectively does): ```bash git init repo && cd repo mkdir wiki && printf -- '- entry A\n' > wiki/log.md git add -A && git commit -m base && BASE=$(git rev-parse HEAD) git checkout -b b1 && printf -- '- from b1\n' >> wiki/log.md && git commit -am b1 git checkout -b b2 $BASE && printf -- '- from b2\n' >> wiki/log.md && git commit -am b2 git merge b1 # CONFLICT in wiki/log.md ``` With the `merge=union` attribute present the same merge is clean — which is precisely the discrepancy: clean locally, conflicted on GitHub. ## Failure mode 2 — union silently reverts deletions Union's rule is "keep both sides", so a **deletion** on one side always loses to **retention** on the other. A line removed on one branch comes back with no conflict and no warning: ```bash git init repo && cd repo printf 'log.md merge=union\n' > .gitattributes printf -- '- keep\n- DELETE-ME\n' > log.md git add -A && git commit -m base && B=$(git rev-parse HEAD) git checkout -b del && sed -i '' '/DELETE-ME/d' log.md && git commit -am remove git checkout -b other $B && printf -- '- unrelated\n' >> log.md && git commit -am other git merge del grep DELETE-ME log.md # still there ``` This is not hypothetical. In a downstream repo I renamed a wiki note and repointed its log line to the new filename. A later rebase resurrected both superseded lines, still pointing at the pre-rename path. No conflict, no warning, and the lint script cannot catch it because log lines are plain text rather than markdown links — so a stale path in the journal is invisible to every check the kit ships. The counter-argument is fair: `WIKI.md` calls the log append-only, and editing a line breaks that convention. But renames happen, the convention is not machine-enforced, and a mechanism whose failure mode is silent data resurrection is a sharp edge worth removing rather than documenting. ## Proposal — one file per entry ``` wiki/log/2026-09-08-record-plugin-scope.md wiki/log/2026-09-08-ingest-oauth-rfc.md ``` Each holds what one line holds today: date, verb, page, summary. The journal is the directory; reading it is `cat wiki/log/*.md` or `ls wiki/log/`, both naturally ordered by the date prefix. What this buys: - **Two branches never touch the same file**, so there is nothing to merge — locally or on GitHub. The PR page stops disagreeing with git. - **`merge=union` can be dropped entirely**, and with it the deletion hazard. - Deleting or correcting an entry becomes a normal, visible file change. - The lint script can check entries the way it already checks pages — frontmatter, referenced page exists — which today it cannot do at all. Costs, stated honestly: - The journal is no longer one scrollable file. `cat wiki/log/*.md` restores that, and a lint or skill step could regenerate a concatenated view if the single-file reading experience matters. - Migration: keep the existing `log.md` in place as the historical record and start the directory alongside it, rather than splitting old entries. That keeps the diff small and loses nothing. - More files in the tree. For a journal that gains a few entries per session this is the same trade `raw/` and `notes/` already make. ## Alternatives considered - **Keep union, enforce append-only strictly** (never edit a line; on a rename append a new entry rather than repointing). Zero machinery, but leaves failure mode 1 entirely and relies on a convention no check enforces. - **Drop the merge driver, accept normal conflicts.** Honest and visible, but with several agent sessions running in parallel it means a conflict on nearly every branch. - **Drop `log.md` altogether** and rely on `git log -- wiki/`. Loses the curated one-line summaries, which read better than commit subjects. Happy to send a PR for the directory layout plus the `WIKI.md` and lint changes if the direction looks right.
Author
Owner

Direction confirmed — implemented in PR #2 (closes this issue on merge). Three review deltas from the proposal as filed: lint treats log/ entries as history (filename-format check only, no frontmatter/target checks — deleted pages would otherwise make old entries perpetually noisy), entries stay frontmatter-free one-liners so cat wiki/log/*.md reads like the old journal, and the installer seeds a dated init entry so fresh installs have a journal and the session-start hook a real last-update date. Also noted in the PR: the merge=union PR-conflict claim is verified for GitHub-hosted repos; Forgejo merges via real git and may honor the attribute — failure mode 2 (silent resurrection, which you hit) justifies the change regardless.

Direction confirmed — implemented in PR #2 (closes this issue on merge). Three review deltas from the proposal as filed: lint treats log/ entries as history (filename-format check only, no frontmatter/target checks — deleted pages would otherwise make old entries perpetually noisy), entries stay frontmatter-free one-liners so cat wiki/log/*.md reads like the old journal, and the installer seeds a dated init entry so fresh installs have a journal and the session-start hook a real last-update date. Also noted in the PR: the merge=union PR-conflict claim is verified for GitHub-hosted repos; Forgejo merges via real git and may honor the attribute — failure mode 2 (silent resurrection, which you hit) justifies the change regardless.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kristofferopsahl/auto-llm-wiki#1
No description provided.