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

+10
View File
@@ -14,6 +14,16 @@ place only: `instructions/dev/version-parts.md` § The candidate model.
--- ---
## 10.1.1-beta.1 - 2026-10-16 - Reine Lesevorgänge schreiben keinen Log-Eintrag mehr, und ein offener Log-Eintrag allein lässt den Katalog beim Abgleich unangetastet
**Author:** Torben Nehmer
<!-- wikitool:bumps -->
- Reine Lesevorgänge schreiben keinen Log-Eintrag mehr, und ein offener Log-Eintrag allein lässt den Katalog beim Abgleich unangetastet
<!-- /wikitool:bumps -->
---
## 10.1.0 - 2026-10-16 - Große PDFs und lange Quellen beim Ingest, vollständiger Export, Konverter-Tests gegen echte Dokumente ## 10.1.0 - 2026-10-16 - Große PDFs und lange Quellen beim Ingest, vollständiger Export, Konverter-Tests gegen echte Dokumente
**Author:** Torben Nehmer **Author:** Torben Nehmer
+1 -1
View File
@@ -1 +1 @@
10.1.0 10.1.1-beta.1
+1 -1
View File
@@ -108,7 +108,7 @@ chemenu/
├── kb/ # OUTPUT: compiled knowledge. A namespace, not a collection ├── kb/ # OUTPUT: compiled knowledge. A namespace, not a collection
│ ├── CONTRACT.md # Collections, naming, tone, linking, provenance │ ├── CONTRACT.md # Collections, naming, tone, linking, provenance
│ ├── index.md # Generated catalog *map*: counts and pointers │ ├── index.md # Generated catalog *map*: counts and pointers
│ ├── log.md # Generated chronological audit log │ ├── log.md # Generated audit log, in the order entries arrived
│ ├── provenance.md # Generated raw-file reverse index │ ├── provenance.md # Generated raw-file reverse index
│ ├── entities/ # COLLECTION.md + INDEX.md + areas below │ ├── entities/ # COLLECTION.md + INDEX.md + areas below
│ │ ├── codebases/ │ │ ├── codebases/
+4 -3
View File
@@ -66,13 +66,13 @@ Append a formatted entry to `kb/log.md`.
**Synopsis** **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]`
**Options** **Options**
```text ```text
--op <str> ingest|query|lint|create|update|delete|rename|move --op <str> ingest|query|lint|create|update|delete|rename|move (query:
[required] legacy, no longer written by the stack) [required]
--title <str> Brief description, e.g. a source path [required] --title <str> Brief description, e.g. a source path [required]
--body <str> Optional multi-line details --body <str> Optional multi-line details
--body-file <path> Read the body from a file instead of --body --body-file <path> Read the body from a file instead of --body
@@ -90,6 +90,7 @@ tools/wikitool log append --op lint --title "2026-09-26" --body-file lint-summar
- Appends one entry to `kb/log.md`: a `## [YYYY-MM-DD] <op> | <title>` heading, the body if one is given, and a `---` separator. - 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. - 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.
**Properties** **Properties**
+1 -1
View File
@@ -42,7 +42,7 @@ tools/wikitool sync --confirm-rebase <token> # re-run after exit 42, once the
**Behaviour** **Behaviour**
- 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. - 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. - 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. - 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. - A refused call performs no rebase attempt and leaves the branch where it was.
+1 -1
View File
@@ -30,7 +30,7 @@ consistent.
3. **Append the audit entry** - one per operation: 3. **Append the audit entry** - one per operation:
```bash ```bash
tools/wikitool log append --op ingest|query|lint|create|update|delete|rename|move \ tools/wikitool log append --op ingest|lint|create|update|delete|rename|move \
--title "<what>" --body "<outcome>" --title "<what>" --body "<outcome>"
``` ```
+2 -1
View File
@@ -88,7 +88,8 @@ the session instead of discovering the drift only at the very end.
never commits and never pushes. Files `wikitool` generates are never a reason to stop: when the never commits and never pushes. Files `wikitool` generates are never a reason to stop: when the
catalog or `kb/log.md` changed on both sides, `sync` keeps both sides' log entries and catalog or `kb/log.md` changed on both sides, `sync` keeps both sides' log entries and
regenerates the catalog and `kb/provenance.md`, which it leaves as an uncommitted change for the regenerates the catalog and `kb/provenance.md`, which it leaves as an uncommitted change for the
next `publish`. Uncommitted work that the incoming commits do not touch stays where it is. next `publish`. An open log entry alone is no such case: the incoming catalog stays and only the
entry is appended again. Uncommitted work that the incoming commits do not touch stays where it is.
**Exit 42 (rebase-review)?** Same as any exit 42 - read the **Exit 42 (rebase-review)?** Same as any exit 42 - read the
diff it prints, judge whether it conflicts with what you are about to do, summarize that to the diff it prints, judge whether it conflicts with what you are about to do, summarize that to the
user, then `tools/wikitool sync --confirm-rebase <token>` before continuing. See user, then `tools/wikitool sync --confirm-rebase <token>` before continuing. See
+11 -13
View File
@@ -12,10 +12,11 @@ description: Answers a question using the LLM wiki's compiled knowledge - read-o
**Before the first `wikitool` call:** `instructions/session-setup.md`. **Before the first `wikitool` call:** `instructions/session-setup.md`.
**Hard rule:** read-only with respect to wiki *content*. Never modify, hand-edit, or scaffold a **Hard rule:** read-only with respect to wiki *content*. Never modify, hand-edit, or scaffold a
page while answering. Two exceptions, both mechanical: step 7 (filing a valuable answer through page while answering. One exception, mechanical: step 7 (filing a valuable answer through
`wikitool new`, never by hand) and step 8 (one audit entry via `wikitool log append`). If the `wikitool new`, never by hand). A query writes no `kb/log.md` entry - a read changes nothing the
wiki has no confident source, say so - per AGENTS.md's "never file an unsourced answer" wiki learned. If the wiki has no confident source, say so - per AGENTS.md's "never file an
invariant - rather than synthesizing a plausible-sounding answer from general knowledge. unsourced answer" invariant - rather than synthesizing a plausible-sounding answer from general
knowledge.
## Steps ## Steps
@@ -98,19 +99,16 @@ invariant - rather than synthesizing a plausible-sounding answer from general kn
find again than a sentence in a chat log. find again than a sentence in a chat log.
7. **File back what survived.** Scaffold it - `tools/wikitool new ...` - and follow 7. **File back what survived.** Scaffold it - `tools/wikitool new ...` - and follow
`wiki-manage`. Never write the page by hand, and never file an answer no source backs. `wiki-manage`, which logs it as a `create` through `publish-cycle.md`. Never write the page by
hand, and never file an answer no source backs.
8. **Log it.**
```bash
tools/wikitool log append --op query --title "<question>" --body "<outcome>"
```
## Decision points ## Decision points
- **Nothing found?** Try the structural query before concluding the wiki is silent - a page may - **Nothing found?** Try the structural query before concluding the wiki is silent - a page may
exist under different words. Then say the wiki has no confident source, and offer to ingest exist under different words. Then say the wiki has no confident source, and offer to ingest
one. one. If the user wants the gap kept, file it as an inbox item on their say-so:
`tools/wikitool task new --inbox --title "<the question>"`. Without a configured tracker the
offer stays in the chat. Never log it.
- **The question asks for the wording** - "what exactly does rule 16:3e say", "which value is - **The question asks for the wording** - "what exactly does rule 16:3e say", "which value is
in table 4"? Find the page that carries the substance first (steps 2-3), then step 4's route in table 4"? Find the page that carries the substance first (steps 2-3), then step 4's route
to the passage. A paraphrase from the page is not the wording, and searching `raw/` is not a to the passage. A paraphrase from the page is not the wording, and searching `raw/` is not a
@@ -129,7 +127,7 @@ invariant - rather than synthesizing a plausible-sounding answer from general kn
## wikitool commands used ## wikitool commands used
`search`, `log append`. If filing an answer back: `new`, `xref add`, `sources rebuild-index`, `search`. If filing an answer back: `new`, `xref add`, `sources rebuild-index`,
`index rebuild`, and `publish` only if asked. `index rebuild`, and `publish` only if asked.
## Output ## Output
+3 -2
View File
@@ -1035,7 +1035,7 @@ Append a formatted entry to `kb/log.md`.
**SYNOPSIS** **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** **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. - 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. - 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** **SEE ALSO**
@@ -2142,7 +2143,7 @@ Fetch `<remote>/<branch>` and bring the local branch up to date with it.
**NOTES** **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. - 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. - 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. - 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. - 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 # 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, # 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 - # 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 # 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. # 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]: def _rebase_resolving_generated(remote_ref: str) -> tuple[bool, str, bool]:
"""`git rebase --autostash <remote_ref>`, resolving each step whose conflicts are all in """`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.""" which also hands the autostash back."""
result = _run(["git", "rebase", "--autostash", remote_ref], env=_NO_EDITOR) result = _run(["git", "rebase", "--autostash", remote_ref], env=_NO_EDITOR)
resolved = False resolved = False
while result.returncode != 0: while result.returncode != 0:
if not _rebase_in_progress(): if not _rebase_in_progress():
return False, result.stderr, resolved 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() reason = _resolve_generated_conflicts()
if reason: if reason:
_run(["git", "rebase", "--abort"]) _run(["git", "rebase", "--abort"])
return False, f"{reason}\n{result.stderr}", resolved 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) result = _run(["git", "rebase", "--continue"], env=_NO_EDITOR)
return True, "", resolved 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: 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 """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 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.index_build import write_index
from chemenu.commands.provenance_cmd import write_provenance_index from chemenu.commands.provenance_cmd import write_provenance_index
if log_suffix: append_log_suffix(log_suffix)
with (config.ROOT / GENERATED_LOG).open("ab") as log:
log.write(log_suffix)
write_index(config.KB_DIR) write_index(config.KB_DIR)
write_provenance_index(config.KB_DIR, config.RAW_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 (succeeded, detail, resolved generated conflicts) - and then either regenerate or put
everything back. Returns (succeeded, detail, regenerated). everything back. Returns (succeeded, detail, regenerated).
Regenerates when something generated was set aside or resolved, or when `regenerate` says Regenerates the catalog and kb/provenance.md when a generated file other than kb/log.md
both sides' commits touched a generated file - a textual merge of two catalogs can succeed was set aside or had a conflict resolved, or when `regenerate` says both sides' commits
and still be wrong. The backup ref is dropped on success and kept, and named, on any 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.""" failure."""
backup = _back_up_working_tree() backup = _back_up_working_tree()
if backup is None: 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"{detail.rstrip()}\nThe working tree was restored; as it stood before, it is also "
f"kept at {BACKUP_REF} ({backup[:12]})." f"kept at {BACKUP_REF} ({backup[:12]})."
), False ), False
regenerated = bool(generated) or resolved or regenerate regenerated = any(path != GENERATED_LOG for path in generated) or resolved or regenerate
if regenerated: if regenerated:
regenerate_generated(aside.log_suffix) regenerate_generated(aside.log_suffix)
else:
append_log_suffix(aside.log_suffix)
_run(["git", "update-ref", "-d", BACKUP_REF]) _run(["git", "update-ref", "-d", BACKUP_REF])
return True, "", regenerated return True, "", regenerated
@@ -1119,7 +1137,8 @@ class ReconcileOutcome:
"up-to-date", "fast-forwarded", "local-ahead", "rebased", "needs-review", "conflict". "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. 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 `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 status: str
pulled_commits: list[str] = field(default_factory=list) pulled_commits: list[str] = field(default_factory=list)
overlap_files: 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 " "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 " "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 " "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 " "regenerated from the merged pages and left as an uncommitted change. When only "
"can do on its own is unchanged and regenerates nothing.", "`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 " "Uncommitted changes that no incoming commit touches do not stop a rebase: they are "
"carried through it (`git rebase --autostash`) and come back unchanged.", "carried through it (`git rebase --autostash`) and come back unchanged.",
"Before anything is set aside, the working tree is stored as a commit under " "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", path="log append",
summary="Append a formatted entry to `kb/log.md`.", summary="Append a formatted entry to `kb/log.md`.",
synopsis=(cli_contract.Variant( 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]', '--title "..." [--body "..."|--body-file path]',
),), ),),
properties=cli_contract.Properties( 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 " "Appends one entry to `kb/log.md`: a `## [YYYY-MM-DD] <op> | <title>` heading, the body "
"if one is given, and a `---` separator.", "if one is given, and a `---` separator.",
"Not idempotent: every successful run appends a new entry, including a repeated one.", "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=( failures=(
cli_contract.Failure( cli_contract.Failure(
@@ -94,7 +97,9 @@ def ingests_since_last_lint(entries: list[tuple[str, str, str]]) -> int:
), ),
)) ))
def log_append( 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"), title: str = typer.Option(..., "--title", help="Brief description, e.g. a source path"),
body: str = typer.Option("", "--body", help="Optional multi-line details"), 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"), 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 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): def test_case_2_two_committed_ingests_rebase_without_the_review_gate(wiki_repo):
writer = _clone_writer(wiki_repo) writer = _clone_writer(wiki_repo)
_writer_ingest(writer, "Writer") _writer_ingest(writer, "Writer")