Files
chemenu/tools/chemenu/commands/log_append.py
T
torbenandClaude Sonnet 5.5 05c6697d3d
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
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
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
2026-10-16 19:49:58 +02:00

165 lines
6.8 KiB
Python

"""Append correctly-formatted entries to kb/log.md."""
from __future__ import annotations
import re
from pathlib import Path
from typing import Optional
import typer
from chemenu import cli_contract, config
from chemenu.commands._util import fail, rel_path, success, today_iso
app = typer.Typer(help="Manage kb/log.md.")
VALID_OPS = ["ingest", "query", "lint", "create", "update", "delete", "rename", "move"]
# Matches the "## [YYYY-MM-DD] op | title" heading `format_log_entry` writes,
# in file order (oldest first, since entries are appended).
LOG_ENTRY_RE = re.compile(r"^## \[(\d{4}-\d{2}-\d{2})\] (\S+) \| (.+)$", re.MULTILINE)
def format_log_entry(op: str, title: str, body: str = "", today: Optional[str] = None) -> str:
today = today or today_iso()
entry = f"## [{today}] {op} | {title}\n"
if body.strip():
entry += f"\n{body.strip()}\n"
entry += "\n---\n"
return entry
def parse_log_entries(text: str) -> list[tuple[str, str, str]]:
"""Return every logged (date, op, title) entry in file order."""
return [(m.group(1), m.group(2), m.group(3)) for m in LOG_ENTRY_RE.finditer(text)]
def ingests_since_last_lint(entries: list[tuple[str, str, str]]) -> int:
"""Count `ingest` entries logged after the most recent `lint` entry (or
since the start of the log, if it has never been linted).
This is the deterministic count behind the Maintenance Schedule's "every
10 sources" full-lint cadence: nothing else in the system tracks it, so
without this the claim was prose with no enforcement - an agent (or user)
had to remember to count."""
count = 0
for _date, op, _title in entries:
if op == "lint":
count = 0
elif op == "ingest":
count += 1
return count
@app.command("append")
@cli_contract.record(cli_contract.CommandRecord(
path="log append",
summary="Append a formatted entry to `kb/log.md`.",
synopsis=(cli_contract.Variant(
usage="log append --op ingest|lint|create|update|delete|rename|move|query "
'--title "..." [--body "..."|--body-file path]',
),),
properties=cli_contract.Properties(
effect=cli_contract.Effect.WRITE,
idempotent=cli_contract.Idempotent.NO,
atomic="Yes - single append",
budget=cli_contract.Budget.COUNTED,
),
notes=(
"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(
cause="`--op` is not one of ingest, query, lint, create, update, delete, rename, move",
reaction="Nothing was written - fix the argument and retry once",
),
cli_contract.Failure(
cause="`--body-file` is missing, not a readable file, or not valid UTF-8",
reaction="Nothing was written - fix the path and retry once",
),
),
examples=(
'tools/wikitool log append --op ingest --title "raw/articles/docker-cheatsheet.md" '
'--body "Created [[Docker]]; updated [[Container]]."',
'tools/wikitool log append --op lint --title "2026-09-26" --body-file lint-summary.md',
),
never=(
"Never re-run after an uncertain outcome without first checking the tail of "
"`kb/log.md` - a second run appends a second entry.",
),
see_also=(
"`wikitool log status` - counts the ingests logged since the last lint",
"`instructions/publish-cycle.md` - where a write session runs this",
),
))
def log_append(
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"),
):
"""Append a formatted entry to `kb/log.md`."""
if op not in VALID_OPS:
fail(f"--op must be one of {VALID_OPS}")
text = body
if body_file:
try:
text = body_file.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
fail(f"Cannot read --body-file {body_file}: {exc}")
entry = format_log_entry(op, title, text)
with config.LOG_FILE.open("a", encoding="utf-8", newline="\n") as f:
f.write("\n" + entry)
success(f"Appended log entry to {rel_path(config.LOG_FILE)}")
@app.command("status")
@cli_contract.record(cli_contract.CommandRecord(
path="log status",
summary="Read-only: count `ingest` entries logged since the last `lint` entry.",
synopsis=(cli_contract.Variant(usage="log status"),),
properties=cli_contract.Properties(
effect=cli_contract.Effect.READ,
idempotent=cli_contract.Idempotent.YES,
atomic="Read-only",
budget=cli_contract.Budget.COUNTED,
),
notes=(
"Counts the `ingest` entries in `kb/log.md` after the most recent `lint` entry, or "
"since the start of the log if it was never linted, and the total number of entries.",
"At 10 or more it prints that the `wiki-lint` skill is due next - the every-10-sources "
"full lint of the Maintenance Schedule.",
"Read-only; safe to retry freely.",
),
failures=(cli_contract.Failure(
cause="`kb/log.md` is missing or empty - reported as nothing logged, not a failure",
reaction="",
code=0,
),),
examples=("tools/wikitool log status",),
see_also=(
"`wikitool log append` - writes the entries this counts",
"`wiki-lint` skill - what the threshold asks for",
"`tools/CONTRACT.md` § Maintenance schedule - the cadence this reports on",
),
))
def log_status():
"""Report how many `ingest` operations have been logged since the last
`lint` - the deterministic trigger for the Maintenance Schedule's "every
10 sources" full-lint cadence. Read-only."""
if not config.LOG_FILE.exists():
success("No kb/log.md yet; nothing logged.")
return
entries = parse_log_entries(config.LOG_FILE.read_text(encoding="utf-8"))
count = ingests_since_last_lint(entries)
typer.echo(f"Ingests since last lint: {count}")
if count >= 10:
typer.echo("Threshold reached (>=10) - run the wiki-lint skill (`tools/wikitool lint ...`) next.")
success(f"{len(entries)} total log entries in {rel_path(config.LOG_FILE)}.")