build: wikitool new project - Seite und Tracker-Projekt unter einem Namen (#126)
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:
1 parent
80b57e0d01
commit
e4260fc2de
8 files changed
+342
-24
No files matched your search
@@ -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)
|
||||
Reference in new issue
Block a user