tools, instructions: Query-Log - reine Lesevorgänge nicht mehr in kb/log.md, Reconcile erzeugt den Katalog bei reinem Log-Zusatz nicht neu (#223); 10.1.1-beta.1
CI / verify (push) Successful in 8m13s
CI / pwsh (push) Successful in 2m46s
Release / converters (push) Successful in 27s
Release / release (push) Successful in 37s
Release / wiki-mirror (push) Skipped

Files changed:
- CHANGES.md
- VERSION
- docs/explanation/architecture.md
- docs/reference/commands/catalog-and-log.md
- docs/reference/commands/git.md
- instructions/publish-cycle.md
- instructions/session-setup.md
- instructions/wiki-query/SKILL.md
- tools/CONTRACT.md
- tools/chemenu/commands/git_publish.py
- tools/chemenu/commands/log_append.py
- tools/chemenu/dist_templates/log.md
- tools/chemenu/tests/test_git_publish.py

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
torbenandClaude Sonnet 5.5 committed 2026-10-16 19:49:58 +02:00
1 parent c1ddefdc7f
commit 05c6697d3d
13 files changed
+140 -39

No files matched your search

+3 -2
View File
@@ -1035,7 +1035,7 @@ Append a formatted entry to `kb/log.md`.
**SYNOPSIS**
- `wikitool log append --op ingest|query|lint|create|update|delete|rename|move --title "..." [--body "..."|--body-file path]`
- `wikitool log append --op ingest|lint|create|update|delete|rename|move|query --title "..." [--body "..."|--body-file path]`
**PROPERTIES**
@@ -1069,6 +1069,7 @@ Append a formatted entry to `kb/log.md`.
- Appends one entry to `kb/log.md`: a `## [YYYY-MM-DD] <op> | <title>` heading, the body if one is given, and a `---` separator.
- Not idempotent: every successful run appends a new entry, including a repeated one.
- `query` is a legacy value: older versions wrote one entry per read-only query. It is still accepted so existing logs and an instance's own instructions keep working, but the stack no longer writes it - a read is not an operation on the wiki.
**SEE ALSO**
@@ -2142,7 +2143,7 @@ Fetch `<remote>/<branch>` and bring the local branch up to date with it.
**NOTES**
- Fetches `<remote>/<branch>`, then: fast-forwards when only the remote moved; rebases the local commits on top when both sides moved but touched disjoint files; exits 42 (Rebase-Review Gate) when both sides touched the same file.
- Files `wikitool` generates (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `kb/**/INDEX.md`) never count as an overlap and never reach the gate. Where they are what stops git - uncommitted ones in the way of a fast-forward or rebase, or a rebase conflict in them - they are merged mechanically: `kb/log.md` becomes the incoming log plus the entries only this side appended, and the catalog and `kb/provenance.md` are regenerated from the merged pages and left as an uncommitted change. A reconcile git can do on its own is unchanged and regenerates nothing.
- Files `wikitool` generates (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `kb/**/INDEX.md`) never count as an overlap and never reach the gate. Where they are what stops git - uncommitted ones in the way of a fast-forward or rebase, or a rebase conflict in them - they are merged mechanically: `kb/log.md` becomes the incoming log plus the entries only this side appended, and the catalog and `kb/provenance.md` are regenerated from the merged pages and left as an uncommitted change. When only `kb/log.md` stood in the way - an open log entry, nothing else generated - the catalog is not rebuilt: the incoming one stays, and just the local entries are appended again. A reconcile git can do on its own is unchanged and regenerates nothing.
- Uncommitted changes that no incoming commit touches do not stop a rebase: they are carried through it (`git rebase --autostash`) and come back unchanged.
- Before anything is set aside, the working tree is stored as a commit under `refs/wikitool/reconcile-backup`. A successful reconcile drops it; a failed one restores the working tree and names the ref.
- A refused call performs no rebase attempt and leaves the branch where it was.
+35 -13
View File
@@ -859,7 +859,9 @@ def rebase_review_token(remote: str, branch: str, local_before: str, remote_tip:
#
# The path below takes the generated files out of git's way and puts the result back
# together mechanically: the log is the incoming version plus what only this side appended,
# and the catalog and kb/provenance.md are regenerated from the merged pages. It runs only
# and the catalog and kb/provenance.md are regenerated from the merged pages - unless the log
# was the only generated file in the way: then the incoming catalog is already right for the
# incoming pages, and only the log entries are appended again. It runs only
# where git would otherwise have failed, so a reconcile that succeeds today is unchanged -
# in particular it does not rebuild, because `index rebuild` stamps today's date into
# kb/index.md and every `sync` would then end with a modified file.
@@ -1049,22 +1051,34 @@ def _resolve_generated_conflicts() -> Optional[str]:
def _rebase_resolving_generated(remote_ref: str) -> tuple[bool, str, bool]:
"""`git rebase --autostash <remote_ref>`, resolving each step whose conflicts are all in
generated files. Returns (succeeded, detail, resolved anything). Any other stop is aborted,
generated files. Returns (succeeded, detail, resolved a generated file other than the log - the
log's own conflicts are merged without needing a rebuild). Any other stop is aborted,
which also hands the autostash back."""
result = _run(["git", "rebase", "--autostash", remote_ref], env=_NO_EDITOR)
resolved = False
while result.returncode != 0:
if not _rebase_in_progress():
return False, result.stderr, resolved
listed = _run(["git", "diff", "--name-only", "--diff-filter=U", "-z"]).stdout
unmerged = [path for path in listed.split("\0") if path]
reason = _resolve_generated_conflicts()
if reason:
_run(["git", "rebase", "--abort"])
return False, f"{reason}\n{result.stderr}", resolved
resolved = True
# A conflict in the log alone is resolved by the append-merge and leaves the catalog
# as the incoming commits wrote it; only another generated file needs a rebuild.
resolved = resolved or any(path != GENERATED_LOG for path in unmerged)
result = _run(["git", "rebase", "--continue"], env=_NO_EDITOR)
return True, "", resolved
def append_log_suffix(log_suffix: bytes) -> None:
"""Append what only this side added to kb/log.md back onto the incoming version."""
if log_suffix:
with (config.ROOT / GENERATED_LOG).open("ab") as log:
log.write(log_suffix)
def regenerate_generated(log_suffix: bytes = b"") -> None:
"""Append `log_suffix` to kb/log.md, then rewrite the catalog and kb/provenance.md from the
pages in the working tree - what `index rebuild` and `sources rebuild-index` write. The log
@@ -1072,9 +1086,7 @@ def regenerate_generated(log_suffix: bytes = b"") -> None:
from chemenu.commands.index_build import write_index
from chemenu.commands.provenance_cmd import write_provenance_index
if log_suffix:
with (config.ROOT / GENERATED_LOG).open("ab") as log:
log.write(log_suffix)
append_log_suffix(log_suffix)
write_index(config.KB_DIR)
write_provenance_index(config.KB_DIR, config.RAW_DIR)
@@ -1084,9 +1096,13 @@ def _around_generated(generated: list[str], operation, regenerate: bool) -> tupl
(succeeded, detail, resolved generated conflicts) - and then either regenerate or put
everything back. Returns (succeeded, detail, regenerated).
Regenerates when something generated was set aside or resolved, or when `regenerate` says
both sides' commits touched a generated file - a textual merge of two catalogs can succeed
and still be wrong. The backup ref is dropped on success and kept, and named, on any
Regenerates the catalog and kb/provenance.md when a generated file other than kb/log.md
was set aside or had a conflict resolved, or when `regenerate` says both sides' commits
touched a generated file - a textual merge of two catalogs can succeed and still be wrong.
When kb/log.md was all that stood in the way, the catalog the incoming commits brought is
already right for the incoming pages, and only the local log entries are appended again:
rebuilding would stamp today's date into kb/index.md and leave a changed file behind that
no local change explains. The backup ref is dropped on success and kept, and named, on any
failure."""
backup = _back_up_working_tree()
if backup is None:
@@ -1105,9 +1121,11 @@ def _around_generated(generated: list[str], operation, regenerate: bool) -> tupl
f"{detail.rstrip()}\nThe working tree was restored; as it stood before, it is also "
f"kept at {BACKUP_REF} ({backup[:12]})."
), False
regenerated = bool(generated) or resolved or regenerate
regenerated = any(path != GENERATED_LOG for path in generated) or resolved or regenerate
if regenerated:
regenerate_generated(aside.log_suffix)
else:
append_log_suffix(aside.log_suffix)
_run(["git", "update-ref", "-d", BACKUP_REF])
return True, "", regenerated
@@ -1119,7 +1137,8 @@ class ReconcileOutcome:
"up-to-date", "fast-forwarded", "local-ahead", "rebased", "needs-review", "conflict".
The first three are the ways the fetch can fail; `detail` then carries the remote's URL.
`regenerated` says the catalog and kb/provenance.md were rewritten afterwards and are an
uncommitted change now."""
uncommitted change now. It stays False when only kb/log.md was set aside: its local entries
are appended again and the catalog is the incoming one."""
status: str
pulled_commits: list[str] = field(default_factory=list)
overlap_files: list[str] = field(default_factory=list)
@@ -1421,8 +1440,11 @@ def apply_reconcile(outcome: ReconcileOutcome, remote: str, branch: str, command
"what stops git - uncommitted ones in the way of a fast-forward or rebase, or a rebase "
"conflict in them - they are merged mechanically: `kb/log.md` becomes the incoming log "
"plus the entries only this side appended, and the catalog and `kb/provenance.md` are "
"regenerated from the merged pages and left as an uncommitted change. A reconcile git "
"can do on its own is unchanged and regenerates nothing.",
"regenerated from the merged pages and left as an uncommitted change. When only "
"`kb/log.md` stood in the way - an open log entry, nothing else generated - the "
"catalog is not rebuilt: the incoming one stays, and just the local entries are "
"appended again. A reconcile git can do on its own is unchanged and regenerates "
"nothing.",
"Uncommitted changes that no incoming commit touches do not stop a rebase: they are "
"carried through it (`git rebase --autostash`) and come back unchanged.",
"Before anything is set aside, the working tree is stored as a commit under "
+7 -2
View File
@@ -55,7 +55,7 @@ def ingests_since_last_lint(entries: list[tuple[str, str, str]]) -> int:
path="log append",
summary="Append a formatted entry to `kb/log.md`.",
synopsis=(cli_contract.Variant(
usage="log append --op ingest|query|lint|create|update|delete|rename|move "
usage="log append --op ingest|lint|create|update|delete|rename|move|query "
'--title "..." [--body "..."|--body-file path]',
),),
properties=cli_contract.Properties(
@@ -68,6 +68,9 @@ def ingests_since_last_lint(entries: list[tuple[str, str, str]]) -> int:
"Appends one entry to `kb/log.md`: a `## [YYYY-MM-DD] <op> | <title>` heading, the body "
"if one is given, and a `---` separator.",
"Not idempotent: every successful run appends a new entry, including a repeated one.",
"`query` is a legacy value: older versions wrote one entry per read-only query. It is "
"still accepted so existing logs and an instance's own instructions keep working, but "
"the stack no longer writes it - a read is not an operation on the wiki.",
),
failures=(
cli_contract.Failure(
@@ -94,7 +97,9 @@ def ingests_since_last_lint(entries: list[tuple[str, str, str]]) -> int:
),
))
def log_append(
op: str = typer.Option(..., "--op", help="|".join(VALID_OPS)),
op: str = typer.Option(
..., "--op", help="|".join(VALID_OPS) + " (query: legacy, no longer written by the stack)",
),
title: str = typer.Option(..., "--title", help="Brief description, e.g. a source path"),
body: str = typer.Option("", "--body", help="Optional multi-line details"),
body_file: Optional[Path] = typer.Option(None, "--body-file", help="Read the body from a file instead of --body"),
+1 -1
View File
@@ -13,6 +13,6 @@ Each entry records what happened, when, and with what result.
---
```
Operation types: `ingest`, `query`, `lint`, `create`, `update`, `delete`
Operation types: `ingest`, `lint`, `create`, `update`, `delete`, `rename`, `move`
---
+63
View File
@@ -1662,6 +1662,69 @@ def test_case_1_on_sync_leaves_the_regenerated_catalog_uncommitted(wiki_repo):
cwd=wiki_repo).returncode != 0 # dropped on success
def _log_only_entry(root, title="a question"):
"""What a read-only query used to leave behind: one appended log entry, nothing else."""
with (root / "kb/log.md").open("a", encoding="utf-8", newline="\n") as log:
log.write("\n" + format_log_entry("query", title, today="2026-10-06"))
def _backup_ref_exists(root):
return subprocess.run(["git", "show-ref", "--verify", "-q", git_publish.BACKUP_REF],
cwd=root).returncode == 0
def _assert_catalog_is_the_incoming_one(root):
"""kb/index.md and kb/provenance.md are byte for byte what origin/main holds."""
check = _check_tree(root)
for path in ("kb/index.md", "kb/provenance.md"):
assert (root / path).read_bytes() == (check / path).read_bytes(), path
def test_an_open_log_entry_alone_does_not_regenerate_the_catalog_on_a_fast_forward(wiki_repo):
writer = _clone_writer(wiki_repo)
_writer_ingest(writer, "Writer")
_log_only_entry(wiki_repo)
outcome = reconcile("origin", "main", None)
assert (outcome.status, outcome.regenerated) == ("fast-forwarded", False)
assert _status(wiki_repo).split() == ["M", "kb/log.md"]
_assert_catalog_is_the_incoming_one(wiki_repo)
assert _logged_titles(wiki_repo) == ["Base", "Writer", "a question"]
assert not _backup_ref_exists(wiki_repo)
def test_an_open_log_entry_alone_does_not_regenerate_the_catalog_on_a_rebase(wiki_repo):
"""A stranded commit that touches no generated file, an open log entry, and an ingest that
landed meanwhile: rebased, the catalog stays the incoming one, the log keeps both."""
writer = _clone_writer(wiki_repo)
_writer_ingest(writer, "Writer")
(wiki_repo / "local.md").write_text("local\n", encoding="utf-8")
_commit_all(wiki_repo, "stranded, touches no generated file")
_log_only_entry(wiki_repo)
outcome = reconcile("origin", "main", None)
assert (outcome.status, outcome.regenerated) == ("rebased", False)
assert _status(wiki_repo).split() == ["M", "kb/log.md"]
_assert_catalog_is_the_incoming_one(wiki_repo)
assert _logged_titles(wiki_repo) == ["Base", "Writer", "a question"]
assert (wiki_repo / "local.md").is_file()
assert not _backup_ref_exists(wiki_repo)
def test_an_open_log_entry_plus_an_uncommitted_page_still_regenerates(wiki_repo):
"""The catalog was locally changed too, so it is set aside and has to be rebuilt."""
writer = _clone_writer(wiki_repo)
_writer_ingest(writer, "Writer")
_ingest(wiki_repo, "Local")
outcome = reconcile("origin", "main", None)
assert (outcome.status, outcome.regenerated) == ("fast-forwarded", True)
_assert_generated_current(wiki_repo)
def test_case_2_two_committed_ingests_rebase_without_the_review_gate(wiki_repo):
writer = _clone_writer(wiki_repo)
_writer_ingest(writer, "Writer")