Files changed: - .wikitool-kb.json - AGENTS.md - CHANGES.md - INSTALL-MCP.md - INSTALL.md - README.md - VERSION - instructions/capture-session.md - instructions/dev/issue-tracking.md - instructions/german-terminology.md - instructions/kb-profiles.md - instructions/migrate-corpus.md - instructions/migrations/5.0.0-confidence-removal.md - instructions/private-instance.md - instructions/setup-instance.md - instructions/wiki-lint/SKILL.md - instructions/wiki-manage/SKILL.md - instructions/wiki-query/SKILL.md - kb/CONTRACT.md - kb/CONVENTIONS.md - kb/CONVENTIONS.md.template - kb/concepts/architectures/Consolidation Tiers.md - kb/concepts/architectures/Context Isolation.md - kb/concepts/architectures/Cross-platform Agent Skills.md - kb/concepts/architectures/Episodic Memory.md - kb/concepts/architectures/Hybrid Search.md - kb/concepts/architectures/Implementation Spectrum.md - kb/concepts/architectures/Knowledge Graph.md - kb/concepts/architectures/LLM Wiki Pattern.md - kb/concepts/architectures/MCP-Leseserver.md - kb/concepts/architectures/Memory Lifecycle.md - kb/concepts/architectures/OKF Compatibility.md - kb/concepts/architectures/Optional Instance Context File.md - kb/concepts/architectures/Personalization Plane.md - kb/concepts/architectures/Procedural Memory.md - kb/concepts/architectures/RAG.md - kb/concepts/architectures/Scale Ceiling.md - kb/concepts/architectures/Semantic Memory.md - kb/concepts/architectures/Three-Layer Architecture.md - kb/concepts/architectures/Token Economics.md - kb/concepts/architectures/Working Memory.md - kb/concepts/decisions/Delete Rather Than Anonymize.md - kb/concepts/decisions/Denylist over Allowlist.md - kb/concepts/decisions/Diff-Reviewable Agent Edits.md - kb/concepts/decisions/Dual Licensing by File Plan.md - kb/concepts/decisions/Issue Label Scheme.md - kb/concepts/decisions/KB Stack Versioning.md - kb/concepts/decisions/Structural Enforcement over Documented Rule.md - kb/concepts/patterns/Audit Trail.md - kb/concepts/patterns/BM25.md - kb/concepts/patterns/Command Round-Trip Integrity.md - kb/concepts/patterns/Confidence Scoring.md - kb/concepts/patterns/Contradiction Resolution.md - kb/concepts/patterns/Entity Extraction.md - kb/concepts/patterns/Filter on Ingest.md - kb/concepts/patterns/Forgetting.md - kb/concepts/patterns/Graph Traversal.md - kb/concepts/patterns/Mesh Sync.md - kb/concepts/patterns/Quality Scoring.md - kb/concepts/patterns/Reciprocal Rank Fusion.md - kb/concepts/patterns/Self-Healing.md - kb/concepts/patterns/Shared vs Private.md - kb/concepts/patterns/Typed Relationships.md - kb/concepts/patterns/Vector Search.md - kb/concepts/patterns/Work Coordination.md - kb/concepts/problems/Ambient Environment Dependency.md - kb/concepts/problems/Detect-Repair Asymmetry.md - kb/concepts/problems/Green Suite Blind Spot.md - kb/concepts/problems/Naming Convention Conflict.md - kb/concepts/problems/Write-Once Frontmatter Fields.md - kb/concepts/protocols/CPPC.md - kb/concepts/protocols/Modbus.md - kb/concepts/protocols/SSD TRIM.md - kb/concepts/workflows/Anti-Cramming Heuristic.md - kb/concepts/workflows/Bulk Operations.md - kb/concepts/workflows/CI Integration.md - kb/concepts/workflows/Checkpoint Audit.md - kb/concepts/workflows/Claude Code Auto Mode.md - kb/concepts/workflows/Content Quality Control.md - kb/concepts/workflows/Crystallization.md - kb/concepts/workflows/Event-Driven Automation.md - kb/concepts/workflows/Hooks.md - kb/concepts/workflows/Index Scaling.md - kb/concepts/workflows/Iteration and Cost Limits.md - kb/concepts/workflows/KB Migration.md - kb/concepts/workflows/Knowledge Compounding.md - kb/concepts/workflows/Lint Workflow.md - kb/concepts/workflows/Mass-Update Gate.md - kb/concepts/workflows/Multi-Agent Collaboration.md - kb/concepts/workflows/Privacy and Governance.md - kb/concepts/workflows/Publish-Remote Gate.md - kb/concepts/workflows/Quality and Self-Correction.md - kb/concepts/workflows/Semantic Lint Automation.md - kb/concepts/workflows/Session Orientation.md - kb/concepts/workflows/Split Merge Reclassify.md - kb/concepts/workflows/Split Threshold.md - kb/concepts/workflows/Stub Threshold.md - kb/concepts/workflows/Supersession.md - kb/concepts/workflows/User Management.md - kb/concepts/workflows/Workflow Extraction.md - kb/concepts/workflows/Workflow Orchestration.md - kb/entities/people/Andrej Karpathy.md - kb/entities/people/E3DC GmbH.md - kb/entities/people/Rohit Gupta.md - kb/entities/people/Vannevar Bush.md - kb/entities/projects/BCDModule.md - kb/entities/projects/Chemenu.md - kb/entities/projects/andybalholm-edl.md - kb/entities/projects/goresponsiveness.md - kb/entities/projects/ha-core.md - kb/entities/projects/hacs-e3dc.md - kb/entities/projects/hacs-integration-blueprint.md - kb/entities/projects/llm-wiki-skills.md - kb/entities/projects/plugnburn-edl.md - kb/entities/projects/wiki-skills-vanillaflava.md - kb/entities/projects/wiki-skills.md - kb/entities/systems/AGENTS.md.md - kb/entities/systems/CLAUDE.md.md - kb/entities/systems/E3DC.md - kb/entities/systems/ENVIRONMENT.md.md - kb/entities/systems/Memex.md - kb/entities/systems/Tolkien Gateway.md - kb/entities/technologies/Arch Linux.md - kb/entities/technologies/Disk Encryption.md - kb/entities/technologies/Docker.md - kb/entities/technologies/GRUB.md - kb/entities/technologies/Gitea Actions.md - kb/entities/technologies/Gitea.md - kb/entities/technologies/Go.md - kb/entities/technologies/Home Assistant.md - kb/entities/technologies/Kernel PM Governors.md - kb/entities/technologies/LVM.md - kb/entities/technologies/Linux Kernel.md - kb/entities/technologies/MQTT.md - kb/entities/technologies/OPC UA.md - kb/entities/technologies/Python.md - kb/entities/technologies/Rust.md - kb/entities/technologies/Wine GE.md - kb/entities/technologies/Wine-Staging.md - kb/entities/technologies/acpi-cpufreq.md - kb/entities/technologies/amd-pstate.md - kb/entities/technologies/iii Engine.md - kb/entities/tools/AUR.md - kb/entities/tools/Act Runner.md - kb/entities/tools/Agent Memory.md - kb/entities/tools/Aura.md - kb/entities/tools/Bottles.md - kb/entities/tools/ChatGPT.md - kb/entities/tools/Claude Code.md - kb/entities/tools/Codex CLI.md - kb/entities/tools/Dataview.md - kb/entities/tools/GPG.md - kb/entities/tools/GitHub Copilot.md - kb/entities/tools/Gitea MCP Server.md - kb/entities/tools/Lutris.md - kb/entities/tools/Marp.md - kb/entities/tools/Mistral Vibe.md - kb/entities/tools/NotebookLM.md - kb/entities/tools/Obsidian Web Clipper.md - kb/entities/tools/Obsidian.md - kb/entities/tools/OpenAI Codex.md - kb/entities/tools/OpenCode.md - kb/entities/tools/Pi.md - kb/entities/tools/Proton.md - kb/entities/tools/Steam.md - kb/entities/tools/Wine.md - kb/entities/tools/awesome-llm-wiki.md - kb/entities/tools/farzaa gist.md - kb/entities/tools/gdeploy.md - kb/entities/tools/makepkg.md - kb/entities/tools/pascalandy schema.md - kb/entities/tools/qmd.md - kb/entities/tools/wikitool.md - kb/index.md - kb/log.md - raw/CONTRACT.md - tools/CONTRACT.md - tools/README.md - tools/chemenu/api.py - tools/chemenu/cli.py - tools/chemenu/commands/confidence_decay.py - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/doctor.py - tools/chemenu/commands/index_build.py - tools/chemenu/commands/new_page.py - tools/chemenu/commands/search.py - tools/chemenu/commands/touch.py - tools/chemenu/commands/version_cmd.py - tools/chemenu/conventions.py - tools/chemenu/corpus_diff.py - tools/chemenu/frontmatter_io.py - tools/chemenu/lint_core.py - tools/chemenu/mcp/server.py - tools/chemenu/page.py - tools/chemenu/search/base.py - tools/chemenu/search/filters.py - tools/chemenu/search/ripgrep.py - tools/chemenu/search/service.py - tools/chemenu/search/types.py - tools/chemenu/tests/conftest.py - tools/chemenu/tests/test_api.py - tools/chemenu/tests/test_confidence_decay.py - tools/chemenu/tests/test_corpus_diff.py - tools/chemenu/tests/test_docs_verify.py - tools/chemenu/tests/test_frontmatter_io.py - tools/chemenu/tests/test_index_build.py - tools/chemenu/tests/test_kb_scan.py - tools/chemenu/tests/test_lint.py - tools/chemenu/tests/test_mcp_server.py - tools/chemenu/tests/test_new_page.py - tools/chemenu/tests/test_page_ops.py - tools/chemenu/tests/test_provenance.py - tools/chemenu/tests/test_raw_cmd.py - tools/chemenu/tests/test_search.py - tools/chemenu/tests/test_touch.py - tools/chemenu/tests/test_type_resolver.py - tools/chemenu/tests/test_version_cmd.py - tools/chemenu/tests/test_xref.py - tools/chemenu/version.py - types/concept.md - types/concept.schema.yaml - types/entity.md - types/entity.schema.yaml - types/instruction.md - types/type-spec.md
347 lines
15 KiB
Python
347 lines
15 KiB
Python
"""Scaffold new wiki pages from type-spec templates.
|
|
|
|
These commands produce structurally-correct frontmatter and a body
|
|
skeleton with TODO placeholders by loading templates from type-spec files.
|
|
The prose (Description, Summary, Key Takeaways, ...) is still written by the
|
|
LLM afterwards with its normal edit tool.
|
|
|
|
The split between type-spec templates and LLM-provided prose is intentional:
|
|
type definitions (naming, frontmatter shape, directory placement, templates) are
|
|
deterministic and stored in /types/; the content is judgment and provided by the LLM.
|
|
|
|
Frontmatter defaults, enum validity, and required-ness all come from the
|
|
type's `.schema.yaml` (via `TypeResolver`) - nothing here re-declares them.
|
|
Directory placement for subtype-driven types (currently just entities) also
|
|
comes from the type-spec, via its `layout:` frontmatter (see
|
|
`TypeResolver.get_layout`) - not a hand-maintained Python dict.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import datetime
|
|
from typing import Any, Dict, Optional
|
|
import re
|
|
|
|
import typer
|
|
|
|
from chemenu import config
|
|
from chemenu.commands._util import (
|
|
check_collision,
|
|
check_raw_files_exist,
|
|
fail,
|
|
parse_set_fields,
|
|
rel_path,
|
|
success,
|
|
)
|
|
from chemenu.frontmatter_io import write_page
|
|
from chemenu.type_resolver import resolver
|
|
|
|
|
|
def _default_summary(summary: str) -> str:
|
|
"""Scaffold-time placeholder for an unfilled --summary, so schema
|
|
validation's `summary` minLength requirement doesn't block page creation.
|
|
Matches the same "TODO: add summary" text index_build.py already falls
|
|
back to when a page has no frontmatter summary."""
|
|
return summary.strip() or "TODO: add summary"
|
|
|
|
|
|
def _resolve_type_and_get_template(type_path: str, source_dir: Path = None):
|
|
"""Resolve a type path, load the type-spec, and extract its template."""
|
|
type_spec = resolver.load_type_spec(type_path, source_dir)
|
|
template = resolver.extract_template(type_spec)
|
|
return type_spec, template
|
|
|
|
|
|
def _enum_help(type_path: str, field_name: str) -> str:
|
|
"""Build a help string from the schema's own enum, so any text listing a
|
|
field's valid values can never drift from what the schema accepts."""
|
|
return f"One of: {'|'.join(resolver.get_enum(type_path, field_name))}"
|
|
|
|
|
|
def _parse_date(field_name: str, text: str) -> datetime.date:
|
|
"""A `--set <date field>=<value>` as a real date, or a refusal naming it."""
|
|
try:
|
|
return datetime.date.fromisoformat(text)
|
|
except ValueError:
|
|
fail(f"--set {field_name}={text!r} must be YYYY-MM-DD.")
|
|
|
|
|
|
def _build_frontmatter(
|
|
type_path: str, schema: Optional[Dict[str, Any]], today: datetime.date, explicit: Dict[str, Any]
|
|
) -> Dict[str, Any]:
|
|
"""Build a page's frontmatter dict in schema-declaration order.
|
|
|
|
`explicit` supplies every CLI-derived value the caller already has;
|
|
fields not in `explicit` get a type-appropriate default (today's date for
|
|
date-formatted fields, the scaffold placeholder for `summary`, the
|
|
schema's own `default:` where declared, an empty list for arrays), or are
|
|
omitted entirely if optional with no sensible default (e.g.
|
|
`source_url`). This is what lets frontmatter shape - and scaffold-time
|
|
defaults like `provenance: general` - follow the schema instead of being
|
|
hand-declared per CLI command.
|
|
"""
|
|
frontmatter: Dict[str, Any] = {"type": type_path}
|
|
for field_name, field_schema in (schema or {}).get("properties", {}).items():
|
|
if field_name == "type":
|
|
continue
|
|
if field_name in explicit:
|
|
value = explicit[field_name]
|
|
# A `--set date=2026-08-23` arrives as a string; the corpus stores
|
|
# dates as `datetime.date`, and the schema already says which fields
|
|
# those are. Converting here keeps one representation on disk instead
|
|
# of leaving it to whoever wrote the CLI call.
|
|
if field_schema.get("format") == "date" and isinstance(value, str):
|
|
value = _parse_date(field_name, value)
|
|
frontmatter[field_name] = value
|
|
elif field_name == "summary":
|
|
frontmatter[field_name] = _default_summary("")
|
|
elif field_name == "author":
|
|
resolved_author = config.default_author()
|
|
if resolved_author is None:
|
|
fail(
|
|
"No author configured for this instance. Set `git config user.name`, "
|
|
"or export WIKI_AUTHOR to override it, then retry."
|
|
)
|
|
frontmatter[field_name] = resolved_author
|
|
elif field_schema.get("format") == "date":
|
|
frontmatter[field_name] = today
|
|
elif "default" in field_schema:
|
|
frontmatter[field_name] = field_schema["default"]
|
|
elif field_schema.get("type") == "array":
|
|
frontmatter[field_name] = []
|
|
|
|
# Carry through any caller-supplied field the schema doesn't declare,
|
|
# rather than silently dropping it: a typo'd `--set` must surface as a
|
|
# validation error (via `additionalProperties: false`) instead of being
|
|
# quietly ignored, and a schema that does allow extra properties should
|
|
# keep them.
|
|
for field_name, value in explicit.items():
|
|
if field_name not in frontmatter:
|
|
frontmatter[field_name] = value
|
|
return frontmatter
|
|
|
|
|
|
def _filter_bullets(value: Any) -> str:
|
|
"""Render a list frontmatter field as `- [[item]]` bullet lines."""
|
|
items = value or []
|
|
return "\n".join(f"- [[{item}]]" for item in items) if items else "- None identified"
|
|
|
|
|
|
def _filter_join(value: Any) -> str:
|
|
"""Comma-join a list frontmatter field."""
|
|
return ", ".join(value or [])
|
|
|
|
|
|
def _filter_capitalize(value: Any) -> str:
|
|
return str(value).capitalize()
|
|
|
|
|
|
def _filter_table_header(value: Any) -> str:
|
|
"""Render an array field as wikilinked markdown table column headers."""
|
|
return " | ".join(f"[[{item}]]" for item in (value or []))
|
|
|
|
|
|
def _filter_table_sep(value: Any) -> str:
|
|
"""Render the markdown table separator row for an array field, one
|
|
column per item."""
|
|
return "|".join("--------" for _ in (value or [])) or "--------"
|
|
|
|
|
|
def _filter_table_cells(value: Any) -> str:
|
|
"""Render a placeholder table body row, one cell per array item."""
|
|
return " | ".join("..." for _ in (value or []))
|
|
|
|
|
|
_TEMPLATE_FILTERS = {
|
|
"bullets": _filter_bullets,
|
|
"join": _filter_join,
|
|
"capitalize": _filter_capitalize,
|
|
"table_header": _filter_table_header,
|
|
"table_sep": _filter_table_sep,
|
|
"table_cells": _filter_table_cells,
|
|
}
|
|
|
|
|
|
def _apply_template_variables(template: str, variables: Dict[str, Any]) -> str:
|
|
"""Apply variable substitutions to a template string.
|
|
|
|
Supports:
|
|
- `{field}` - plain substitution from `variables[field]`
|
|
- `{field|filter}` - apply a named filter (bullets, join, capitalize)
|
|
to `variables[field]`'s value, so templates can render list/enum
|
|
frontmatter fields directly instead of the caller precomputing a
|
|
separate display-only variable for each one
|
|
- `{field|literal text}` - literal fallback if `field` isn't in
|
|
`variables` at all and the suffix isn't a recognized filter name
|
|
"""
|
|
def replace_match(match: re.Match) -> str:
|
|
full_match = match.group(0)
|
|
var_name = match.group(1)
|
|
if '|' in var_name:
|
|
var_name, suffix = var_name.split('|', 1)
|
|
var_name = var_name.strip()
|
|
suffix = suffix.strip()
|
|
if var_name not in variables:
|
|
return suffix
|
|
value = variables[var_name]
|
|
filter_fn = _TEMPLATE_FILTERS.get(suffix)
|
|
return filter_fn(value) if filter_fn else str(value)
|
|
return str(variables.get(var_name, full_match))
|
|
|
|
pattern = r'\{([^}]+)\}'
|
|
return re.sub(pattern, replace_match, template)
|
|
|
|
|
|
def _page_subdir(subtype: Optional[str], type_path: str) -> Optional[str]:
|
|
"""Return the subtype-driven subdirectory under a type's `base_dir`, from
|
|
the type-spec's own `layout:` frontmatter. Thin wrapper over
|
|
`TypeResolver.subtype_dir` - the one place this computation lives, shared
|
|
with `move` and `lint`'s misplaced-page finding."""
|
|
return resolver.subtype_dir(type_path, subtype)
|
|
|
|
|
|
def _target_dir(type_path: str, frontmatter: Dict[str, Any]) -> Path:
|
|
"""Resolve where an instance of this type is written: `<root>/<base_dir>`,
|
|
plus a subtype subdirectory when the type declares a `layout:`.
|
|
|
|
Thin wrapper over `TypeResolver.compute_target_dir` - the one placement
|
|
rule, also used by `move` and `lint`'s misplaced-page finding -
|
|
converting its `ValueError` into the CLI's normal friendly-failure path.
|
|
|
|
`base_dir` is resolved against `config.KB_DIR` by default, so tests that
|
|
point KB_DIR at a temporary fixture wiki can never write into the real
|
|
`kb/`. A type-spec declaring `root: repo` resolves against `config.ROOT`
|
|
instead - for artifacts that are agent-directed material rather than
|
|
knowledge, and so live outside the knowledge layer."""
|
|
try:
|
|
return resolver.compute_target_dir(type_path, frontmatter)
|
|
except ValueError as exc:
|
|
fail(str(exc))
|
|
|
|
|
|
def _validate_or_fail(frontmatter: Dict[str, Any], type_path: str, source_dir: Path) -> None:
|
|
"""Validate frontmatter against its type-spec schema, converting a
|
|
ValueError into the CLI's normal friendly-failure path instead of an
|
|
uncaught traceback. The schema's own error message (which already names
|
|
the offending field and, for enums, lists the valid values) is shown
|
|
as-is - there is no separate hand-maintained validity check to keep in
|
|
sync with it."""
|
|
try:
|
|
resolver.validate_frontmatter(frontmatter, type_path, source_dir)
|
|
except ValueError as exc:
|
|
fail(str(exc))
|
|
|
|
|
|
def _load_type_or_fail(type_path: str, source_dir: Path):
|
|
"""Resolve a type path and load its type-spec + template, converting an
|
|
unresolvable/invalid `--type` into the CLI's normal friendly-failure path."""
|
|
try:
|
|
return _resolve_type_and_get_template(type_path, source_dir)
|
|
except ValueError as exc:
|
|
fail(str(exc))
|
|
|
|
|
|
def new_page_command(
|
|
type_name: str = typer.Argument(
|
|
...,
|
|
help="Type name, e.g. entity|concept|source|comparison (see `wikitool types list`)",
|
|
),
|
|
name: str = typer.Option(..., "--name", help="Page title (a type's title_prefix is added automatically)"),
|
|
type_path_override: str = typer.Option(
|
|
"", "--type", help="Override the type-spec path (defaults to the one named by TYPE_NAME)"
|
|
),
|
|
set_fields: Optional[list[str]] = typer.Option(
|
|
None,
|
|
"--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",
|
|
),
|
|
):
|
|
"""Scaffold a new wiki page of any type.
|
|
|
|
The type's own type-spec drives everything: which frontmatter fields
|
|
exist and are required (its `.schema.yaml`), their scaffold defaults
|
|
(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.
|
|
"""
|
|
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)}")
|
|
|
|
today = datetime.date.today()
|
|
|
|
try:
|
|
title_prefix = resolver.get_title_prefix(type_path)
|
|
root = resolver.get_root(type_path)
|
|
except ValueError as exc:
|
|
fail(str(exc))
|
|
page_title = f"{title_prefix}{name}"
|
|
if root == "kb":
|
|
# Title collisions matter because wikilinks resolve by title alone, so
|
|
# two pages sharing a stem are indistinguishable to every link in the
|
|
# wiki. Artifacts outside kb/ are not addressed by title and are not
|
|
# part of that namespace, so the check does not apply to them.
|
|
check_collision(page_title)
|
|
|
|
schema = resolver.get_schema(type_path)
|
|
explicit = parse_set_fields(set_fields, schema)
|
|
declared = (schema or {}).get("properties", {})
|
|
if "summary" in declared:
|
|
explicit.setdefault("summary", _default_summary(""))
|
|
if "name" in declared:
|
|
# The CLI already has this value; a type that stores its own name in
|
|
# frontmatter should not have to be told it twice.
|
|
explicit.setdefault("name", name)
|
|
if "description" in declared:
|
|
explicit.setdefault("description", "TODO: add description")
|
|
|
|
frontmatter = _build_frontmatter(type_path, schema, today, explicit)
|
|
|
|
# Capture fields (Gitea #67, e.g. `fidelity`/`authority` on a source page)
|
|
# are deliberately absent from `required:` - putting them there would make
|
|
# every existing instance's source pages stop validating, a
|
|
# boundary-crossing change (version-parts.md). The requirement is instead
|
|
# enforced here, in the tool, exactly like `source_type`'s no-default
|
|
# refusal (#66) reads from the schema alone: without a `default:` and
|
|
# omitted from `explicit`, a capture field simply never lands in
|
|
# `frontmatter`, so its absence has to be caught before it is silently
|
|
# written as a page with no capture record at all.
|
|
try:
|
|
capture_fields = resolver.get_capture_fields(type_path)
|
|
except ValueError as exc:
|
|
fail(str(exc))
|
|
missing_capture = [f for f in capture_fields if not frontmatter.get(f)]
|
|
if missing_capture:
|
|
fail(
|
|
f"Type {type_path} requires capture field(s) {', '.join(missing_capture)} - pass them "
|
|
"explicitly, e.g. --set fidelity=verbatim --set authority=reporting. "
|
|
"new does not guess them (see raw/CONTRACT.md)."
|
|
)
|
|
guessed_unknown = [f for f in capture_fields if frontmatter.get(f) == "unknown"]
|
|
if guessed_unknown:
|
|
fail(
|
|
f"Capture field(s) {', '.join(guessed_unknown)} cannot be set to 'unknown' here - that "
|
|
"value is backfill-only, written only by `wikitool touch` on a page predating this rule."
|
|
)
|
|
|
|
target_dir = _target_dir(type_path, frontmatter)
|
|
_type_spec, template = _load_type_or_fail(type_path, target_dir)
|
|
_validate_or_fail(frontmatter, type_path, target_dir)
|
|
|
|
if "raw_files" in frontmatter:
|
|
check_raw_files_exist(frontmatter["raw_files"])
|
|
|
|
path = target_dir / f"{page_title}.md"
|
|
body = _apply_template_variables(
|
|
template,
|
|
{
|
|
**frontmatter,
|
|
"name": name,
|
|
"today": today.isoformat(),
|
|
},
|
|
)
|
|
|
|
write_page(path, frontmatter, body)
|
|
success(f"Created {rel_path(path)}")
|