build: wikitool new project - Seite und Tracker-Projekt unter einem Namen (#126)
CI / verify (push) Successful in 47s
Release / release (push) Successful in 35s

Files changed:
- CHANGES.md
- VERSION
- tools/CONTRACT.md
- tools/chemenu/commands/new_page.py
- tools/chemenu/errors.py
- tools/chemenu/review.py
- tools/chemenu/tasks/__init__.py
- tools/chemenu/tests/test_new_page.py
This commit is contained in:
torben committed 2026-09-20 07:32:03 +02:00
1 parent 80b57e0d01
commit e4260fc2de
8 files changed
+342 -24

No files matched your search

+105 -3
View File
@@ -28,18 +28,29 @@ import re
import typer
from chemenu import config
from chemenu import config, tasks
from chemenu.commands._util import (
check_collision,
check_raw_files_exist,
fail,
needs_clearance,
parse_set_fields,
rel_path,
success,
)
from chemenu.errors import HumanInterventionRequired, ValidationError
from chemenu.frontmatter_io import write_page
from chemenu.tasks import config as tasks_config
from chemenu.tasks.protocol import find_project
from chemenu.type_resolver import resolver
# The one type name for which `new` also touches the task tracker (Gitea
# #126, #119 D8/D16/D31) - the same literal `chemenu.review._load_kb_projects`
# already matches `page.kind` against, and the one `docs verify`'s
# `check_stack_required_types` (`kb_collections.STACK_REQUIRED_TYPES`) makes
# sure some type-spec actually declares `name: project`.
PROJECT_TYPE_NAME = "project"
def _default_summary(summary: str) -> str:
"""Scaffold-time placeholder for an unfilled --summary, so schema
@@ -255,6 +266,57 @@ def _load_type_or_fail(type_path: str, source_dir: Path):
fail(str(exc))
def _ensure_tracker_project(page_title: str, *, resume: bool) -> Optional[str]:
"""Step 1+2 of `new project` (Gitea #126, #119 D8/D31): make sure a
tracker project named `page_title` exists before the caller writes the
kb/ page for it, and never touch the page itself.
Returns a one-line status to fold into the success message, or `None`
when no tracker is configured at all - `page_title` is the page's actual
title (title_prefix already applied), the same value `check_collision`
checked against `kb/` moments earlier, because it is what the join in
`chemenu.review` keys on (#119 D8: the name is the sole coupling).
Leaves through `fail()`/`needs_clearance()` (never returns) for every
outcome that must not proceed to page creation - a collision, a
dependency failure, or a human still owing the manual step - so that by
the time this returns normally, either nothing was created (page-only or
a genuine refusal) or a tracker project now provably exists for this
exact name, and the caller's next step is the only one left: write the
page.
"""
cfg = tasks_config.read_config(config.ROOT)
if cfg is None:
return None
reader = tasks.build_reader(cfg)
existing = find_project(reader, page_title)
if existing is not None:
if resume:
return f"tracker project '{existing.name}' already existed (--resume)"
fail(
f"A project named '{page_title}' (case-insensitively) already exists in the "
f"tracker ('{existing.name}') - nothing was created (neither the tracker project "
"nor the kb/ page). If an earlier run of this exact command asked you to create it "
"by hand and you just did, re-run with --resume to continue to page creation "
"instead of being refused here."
)
writer = tasks.build_writer(cfg, reader)
try:
writer.create_project(page_title)
except HumanInterventionRequired as exc:
needs_clearance(
f"{exc}\n\nNothing was created yet for '{page_title}' (neither the tracker "
"project nor the kb/ page). Once you have done the above, re-run this exact "
"command with --resume to verify it and continue to page creation - do not assume "
"confirming here is enough."
)
except ValidationError as exc:
fail(str(exc))
return f"tracker project '{page_title}' created"
def new_page_command(
type_name: str = typer.Argument(
...,
@@ -269,6 +331,13 @@ def new_page_command(
"--set",
help="Frontmatter field, repeatable: --set entity_type=tool --set tags=a,b. Array values split on commas (escape a literal one as \\,); repeating --set for an array field appends instead of replacing",
),
resume: bool = typer.Option(
False,
"--resume",
help="`project` only: confirm a human has completed the manual tracker step an earlier "
"HumanInterventionRequired refusal asked for, so this run continues to page creation "
"instead of refusing the now-existing tracker project as a collision (Gitea #126).",
),
):
"""Scaffold a new wiki page of any type.
@@ -277,12 +346,23 @@ def new_page_command(
(schema `default:`), where the page is written (`base_dir` + `layout`),
what prefixes its title (`title_prefix`), and its body skeleton (the
type-spec's template). Adding a new type therefore needs no change here.
For `type_name == "project"` specifically, this also ensures a
same-named tracker project exists (Gitea #126, #119 D8/D31) before the
page is written - see `_ensure_tracker_project`.
"""
type_path = type_path_override or resolver.find_type_by_name(type_name)
if not type_path:
available = sorted(fm.get("name") for _, fm in resolver.list_type_specs())
fail(f"No type-spec named '{type_name}'. Available: {', '.join(available)}")
try:
is_project = resolver.get_type_name(type_path) == PROJECT_TYPE_NAME
except ValueError as exc:
fail(str(exc))
if resume and not is_project:
fail("--resume only applies to `new project` (Gitea #126) - it has no effect on any other type.")
today = datetime.date.today()
try:
@@ -356,5 +436,27 @@ def new_page_command(
},
)
write_page(path, frontmatter, body)
success(f"Created {rel_path(path)}")
# Everything above only validates - nothing has touched disk or the
# tracker yet. Tracker before page (Gitea #126's own "Reihenfolge ist die
# Fehlerbehandlung"): a page-write failure past this point leaves a
# tracker project with no page, a state check 3 (#125) already reports;
# the reverse order would instead leave a kb/ page claiming an
# initiative nobody can act on, which is worse and unreported.
tracker_note = _ensure_tracker_project(page_title, resume=resume) if is_project else None
try:
write_page(path, frontmatter, body)
except OSError as exc:
if is_project and tracker_note is not None:
fail(
f"Could not write {rel_path(path)} ({exc}). The kb/ page was NOT created, but "
f"the tracker project was already confirmed to exist ({tracker_note}). Fix the "
"write error and re-run with --resume to finish - a plain re-run would otherwise "
"be refused as a tracker collision."
)
raise
msg = f"Created {rel_path(path)}"
if is_project:
msg += f" ({tracker_note or 'no task tracker configured - page only'})"
success(msg)