Files changed: - CHANGES.md - VERSION - tools/CONTRACT.md - tools/chemenu/commands/index_build.py - tools/chemenu/commands/log_append.py
157 lines
6.3 KiB
Python
157 lines
6.3 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|query|lint|create|update|delete|rename|move "
|
|
'--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.",
|
|
),
|
|
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` cannot be read",
|
|
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)),
|
|
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:
|
|
text = body_file.read_text(encoding="utf-8")
|
|
entry = format_log_entry(op, title, text)
|
|
with config.LOG_FILE.open("a", encoding="utf-8") 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)}.")
|