export: export run writes selected kb/ pages as a derived, read-only version into configured target folders (markdown and command handlers, manifest, after_publish), raw accept also refuses manifest-listed files, doctor checks the section (#214); 10.0.0-beta.13
CI / verify (push) Successful in 9m2s
CI / pwsh (push) Successful in 2m48s
Release / release (push) Successful in 34s
Release / wiki-mirror (push) Skipped

Files changed:
- CHANGES.md
- VERSION
- docs/how-to/README.md
- docs/how-to/back-up-to-a-synced-folder.md
- docs/how-to/collect-from-a-synced-folder.md
- docs/how-to/export-guidelines.md
- docs/how-to/export-to-a-synced-folder.md
- docs/reference/commands/README.md
- docs/reference/commands/exports.md
- docs/reference/commands/git.md
- docs/reference/commands/guideline-export.md
- docs/reference/commands/instance-health.md
- docs/reference/commands/raw-material-and-uploads.md
- docs/reference/configuration.md
- docs/reference/exit-codes.md
- instructions/setup-instance.md
- raw/CONTRACT.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/backup.py
- tools/chemenu/cli_contract.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/commands/export_cmd.py
- tools/chemenu/commands/git_publish.py
- tools/chemenu/commands/raw/_common.py
- tools/chemenu/commands/raw/accept.py
- tools/chemenu/commands/raw/capture.py
- tools/chemenu/config_shapes.py
- tools/chemenu/guideline_export.py
- tools/chemenu/page_export.py
- tools/chemenu/reference_doc.py
- tools/chemenu/repo_capture.py
- tools/chemenu/tests/test_config_shapes.py
- tools/chemenu/tests/test_export_run.py

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
torbenandClaude Code committed 2026-10-13 05:53:48 +02:00
1 parent ecefe0a033
commit 99945c4841
33 files changed
+2316 -96

No files matched your search

+129 -3
View File
@@ -1,6 +1,8 @@
"""`wikitool export guidelines` - the instance's guideline pages as one
generated `GUIDELINES.md`, printed, or written into the captured repositories
behind the Guideline Push Gate.
behind the Guideline Push Gate - and `wikitool export run`, the derived reading
copy of `kb/` in the folders the `export` section configures
(`chemenu/page_export.py`; no gate, since a target is a folder the user named).
The core - selection, rendering, the header - is `chemenu/guideline_export.py`;
the git write is `repo_capture.commit_file`/`push_commit`. This module decides
@@ -42,12 +44,12 @@ from typing import Optional
import typer
from rich.markup import escape
from chemenu import cli_contract, config, guideline_export, repo_capture
from chemenu import cli_contract, config, guideline_export, page_export, repo_capture
from chemenu.commands._util import EXIT_NEEDS_CLEARANCE, console, fail, rel_path
from chemenu.errors import BackendError, ValidationError
from chemenu.search import filters
app = typer.Typer(help="Generate files from the wiki for other repositories.")
app = typer.Typer(help="Generate files from the wiki for other repositories and folders.")
GATE = "guideline-push"
@@ -427,3 +429,127 @@ def export_guidelines_command(
"written; run the command again for the rest."
)
raise typer.Exit(code=1)
@app.command("run")
@cli_contract.record(cli_contract.CommandRecord(
path="export run",
summary="Write the derived, read-only reading copy of `kb/` into the folders the `export` "
"section configures - the selected pages as Markdown, or converted by a program of this "
"checkout.",
synopsis=(
cli_contract.Variant(
usage="export run [<target> ...]",
notes="Every configured target, or the named ones. Needs an `export` section in "
f"`{config.LOCAL_SETTINGS_FILENAME}`; the user writes it by hand.",
),
),
properties=cli_contract.Properties(
effect=cli_contract.Effect.WRITE,
idempotent=cli_contract.Idempotent.YES,
atomic="Per file within a target (a temporary file in the target, then a move over its "
"place), the manifest last; targets are independent, so a failed target does not undo "
"another",
budget=cli_contract.Budget.COUNTED,
),
notes=(
"An export is derived: regenerated from `kb/` on every run, never edited, never read back "
"into the wiki. Each target names its pages with an allowlist of `search --field` "
"predicates (`include`: a list of selections, AND within one, OR across them), a handler "
"(`markdown`, or `command` for the operator's converter) and the folder (`path`, outside "
"the instance). Nothing is exported that no selection names.",
"Every Markdown file starts with a frontmatter block whose `wikitool_export` line says "
"`kind=pages instance=<origin URL without userinfo, or local> commit=<newest commit "
"touching a selected page>`, followed by `title`, `type`, `summary`, `modified` where the "
"page has them (`frontmatter: summary`, the default; `minimal` keeps only the marker, "
"`full` the whole frontmatter), then the page text. A file keeps its path below `kb/`. A "
"wikilink to an exported page becomes a relative Markdown link (`links: relative`, the "
"default) or plain text (`links: text`); a link to a page that is not exported is always "
"plain text. Footnote markers stay; code is never rewritten. LF endings, no timestamp: "
"the same pages give the same bytes.",
"Handler `command`: the Markdown is rendered into a staging directory, the program of "
"`export.commands` runs with no shell, stdin closed and a timeout of 300 s, and receives "
"the Markdown directory and an empty output directory as its last two arguments. On exit "
"0 the regular files in the output directory (recursively) become the target's content; "
"any other end leaves the target unchanged and prints the end of its stderr. The staging "
"directory is always removed.",
"The folder is only ever touched by what it holds in its manifest, `.wikitool-export.json`: "
"the files an earlier run wrote, each with its sha256. A run removes only manifest files "
"that are no longer exported (and folders it created, once empty), rewrites a manifest "
"file that is missing, leaves every other file byte for byte as it was, and writes "
"nothing - not even a modification time - for a file that did not change. A run that "
"changes nothing writes nothing at all.",
"Refuses a target, before writing anything into it: a non-empty folder with no manifest "
"(it is somebody's, not an export), a manifest file that was changed in the folder since "
"the last run (naming it), a foreign file where an exported one belongs, an empty "
"selection (it would delete the earlier export), an unknown field in a predicate. A "
"missing folder is created.",
"Refuses everything, before any target: uncommitted changes in `kb/` or `types/` "
"(untracked files included), no git checkout, a page under `kb/` with unreadable "
"frontmatter, an unknown target name, a broken `export` section.",
"Output per target: `new` (with the page titles), `changed`, `removed`, the count "
"`unchanged`, or `failed` with the reason. A page that newly enters a selection is "
"reported as new, not gated. Exit 0 when every target ran; exit 1 when any failed - the "
"others were still served.",
"A target with `after_publish: true` is also refreshed by every `publish` that "
"committed, `--no-push` included; a failure there is a warning, the commit stands, and "
"this command is the repeat.",
"`raw accept` refuses a file whose sha256 a target's manifest lists, and one that carries "
"the marker: an export never goes back into `raw/`.",
),
failures=(
cli_contract.Failure(
cause="No `export` section, an unknown target name, or a broken section (a predicate "
"that does not parse, a path inside the instance or in the way of another target, an "
"`intake` folder or the backup target)",
reaction="Show the message to the user; the section is theirs to write by hand, never "
"yours",
),
cli_contract.Failure(
cause="`kb/` or `types/` has uncommitted changes, the instance is not a git checkout, "
"or a page under `kb/` has unreadable frontmatter",
reaction="Fix the named pages, or publish the changes first, then retry once",
),
cli_contract.Failure(
cause="A target failed: a foreign folder, a file changed in the target, an empty "
"selection, an unknown field, a converter that did not exit 0",
reaction="Show the target's reason to the user; the other targets were still served. "
"Never delete or edit files in a target folder to make a run pass",
),
),
examples=(
"tools/wikitool export run",
"tools/wikitool export run work-notes",
),
never=(
"Never edit a file in a target folder to change an export - the next run refuses or "
"overwrites it; change the page, or the target's `include`, instead.",
"Never put an export folder into `incoming/` or `raw/`.",
),
see_also=(
"`wikitool search` - the same predicates, to preview a selection",
"`wikitool backup run` - the same kind of folder, for this machine's own files",
"`docs/how-to/export-to-a-synced-folder.md`",
),
))
def export_run_command(
names: Optional[list[str]] = typer.Argument(
None, metavar="[TARGET]...", help="Targets to run; default: every configured target."
),
):
"""Write the reading copy of kb/ into the configured export folders."""
try:
targets = page_export.choose(page_export.require_config(), names or [])
results = page_export.run(targets)
except ValidationError as exc:
fail(escape(str(exc)))
return
failed = 0
for result in results:
for line in page_export.report_lines(result):
typer.echo(line)
failed += not result.ok
if failed:
console.print(f"[bold red]ERROR[/bold red] {failed} of {len(results)} target(s) failed - the "
"others were served; fix the reason above and run again.")
raise typer.Exit(code=1)