177c7e9ce8
Files changed: - .gitea/workflows/ci.yml - AGENTS.md - CHANGES.md - VERSION - instructions/CONTRACT.md - instructions/link-taxonomy.md - instructions/migrations/4.0.0-link-taxonomy.md - instructions/setup-instance.md - kb/CONTRACT.md - kb/CONVENTIONS.md - kb/CONVENTIONS.md.template - kb/comparisons/COLLECTION.md - kb/concepts/COLLECTION.md - kb/entities/COLLECTION.md - kb/sources/COLLECTION.md - tools/CONTRACT.md - tools/README.md - tools/chemenu/blocks.py - tools/chemenu/cli.py - tools/chemenu/commands/cite_cmd.py - tools/chemenu/commands/dist_cmd.py - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/doctor.py - tools/chemenu/commands/links_cmd.py - tools/chemenu/commands/migrate_cmd.py - tools/chemenu/commands/new_page.py - tools/chemenu/commands/page_ops.py - tools/chemenu/commands/run_budget.py - tools/chemenu/commands/xref.py - tools/chemenu/conventions.py - tools/chemenu/corpus_diff.py - tools/chemenu/frontmatter_io.py - tools/chemenu/kb_collections.py - tools/chemenu/kb_state.py - tools/chemenu/links.py - tools/chemenu/lint_core.py - tools/chemenu/provenance.py - tools/chemenu/sections.py - tools/chemenu/tests/conftest.py - tools/chemenu/tests/test_blocks.py - tools/chemenu/tests/test_cite_cmd.py - tools/chemenu/tests/test_conventions.py - tools/chemenu/tests/test_dist_cmd.py - tools/chemenu/tests/test_doctor.py - tools/chemenu/tests/test_migrate_cmd.py - tools/chemenu/tests/test_new_page.py - tools/chemenu/tests/test_pipeline_l0.py - tools/chemenu/tests/test_types_cmd.py - tools/chemenu/tests/test_xref.py - types/concept.schema.yaml - types/entity.md - types/entity.schema.yaml - types/instruction.schema.yaml - types/type-spec.md - work/link-taxonomy-migration/README.md - work/link-taxonomy-migration/plan.md
137 lines
5.0 KiB
Python
137 lines
5.0 KiB
Python
"""Generated regions inside a page body, found by delimiter rather than by prose.
|
|
|
|
`xref` owns the links block and `cite` owns the footnotes block. Both used to be
|
|
located by matching their **heading text** - `^## Beziehungen$` - which made a
|
|
translated heading a structural fact and put the KB language into the compiler.
|
|
It also made the region's *end* a guess: the footnotes block ran to the next
|
|
heading, and before that to the end of the file, which silently deleted whatever
|
|
sat after it on eight pages.
|
|
|
|
A marker pair answers both questions exactly:
|
|
|
|
<!-- wikitool:links -->
|
|
## Beziehungen
|
|
|
|
- **depends-on:** [[Hermes]]
|
|
<!-- /wikitool:links -->
|
|
|
|
Everything between the markers is generated and is replaced wholesale on the
|
|
next write - heading included, which is why the heading text is a *rendering*
|
|
value from `kb/CONVENTIONS.md` rather than something the tool searches for. An
|
|
author never edits inside the markers; anything they put there is overwritten
|
|
without warning, exactly like `kb/index.md`.
|
|
|
|
The markers are HTML comments: invisible in every renderer this corpus is read
|
|
through, and the same convention `dist:strip-start`/`-end` already uses in
|
|
`AGENTS.md`. They cost a reader nothing and cost an LLM about twenty tokens a
|
|
page - the price of not having to guess where a generated region ends.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Optional
|
|
|
|
# The two regions the tool owns. `see-also` is deliberately absent: it was the
|
|
# reciprocal half of the old bidirectional `xref add`, and under authored
|
|
# directional edges it is a *label* (`see-also`) inside the links block, not a
|
|
# section of its own.
|
|
LINKS = "links"
|
|
FOOTNOTES = "footnotes"
|
|
BLOCKS = (LINKS, FOOTNOTES)
|
|
|
|
_NAME = r"[a-z][a-z0-9-]*"
|
|
|
|
|
|
def open_marker(name: str) -> str:
|
|
return f"<!-- wikitool:{name} -->"
|
|
|
|
|
|
def close_marker(name: str) -> str:
|
|
return f"<!-- /wikitool:{name} -->"
|
|
|
|
|
|
def _region_re(name: str) -> re.Pattern[str]:
|
|
"""The whole region including both markers and the blank line around it."""
|
|
return re.compile(
|
|
r"\n*"
|
|
+ re.escape(open_marker(name))
|
|
+ r".*?"
|
|
+ re.escape(close_marker(name))
|
|
+ r"\n*",
|
|
re.DOTALL,
|
|
)
|
|
|
|
|
|
_ANY_OPEN_RE = re.compile(rf"<!-- wikitool:({_NAME}) -->")
|
|
_ANY_CLOSE_RE = re.compile(rf"<!-- /wikitool:({_NAME}) -->")
|
|
|
|
|
|
def find(body: str, name: str) -> Optional[str]:
|
|
"""The generated content of `name`'s region, markers excluded, or None."""
|
|
match = _region_re(name).search(body)
|
|
if not match:
|
|
return None
|
|
text = match.group(0)
|
|
start = text.index(open_marker(name)) + len(open_marker(name))
|
|
end = text.index(close_marker(name))
|
|
return text[start:end].strip("\n")
|
|
|
|
|
|
def render(name: str, heading: str, lines: list[str]) -> str:
|
|
"""A whole region, ready to place into a body. Empty `lines` renders "".
|
|
|
|
An empty region is no region at all rather than a heading with nothing under
|
|
it: a page that cites nothing should not carry an empty Footnotes section,
|
|
and the same holds for a page with no declared edges.
|
|
"""
|
|
if not lines:
|
|
return ""
|
|
parts = [open_marker(name), f"## {heading}", "", *lines, close_marker(name)]
|
|
return "\n".join(parts)
|
|
|
|
|
|
def replace(body: str, name: str, region: str) -> str:
|
|
"""Put `region` where `name`'s region is, or append it if there is none.
|
|
|
|
Appending at the end is right for both blocks: they are the page's trailing
|
|
machine-owned material, and an author's prose never follows them. A region
|
|
that is `""` removes what was there.
|
|
"""
|
|
existing = _region_re(name).search(body)
|
|
if existing:
|
|
replacement = f"\n\n{region}\n" if region else "\n"
|
|
return (body[: existing.start()] + replacement + body[existing.end():]).rstrip("\n") + "\n"
|
|
if not region:
|
|
return body
|
|
return body.rstrip("\n") + "\n\n" + region + "\n"
|
|
|
|
|
|
def strip(body: str, name: str) -> str:
|
|
"""The body with `name`'s region removed entirely."""
|
|
return replace(body, name, "")
|
|
|
|
|
|
def marker_pairs(body: str) -> dict[str, int]:
|
|
"""How many complete open/close pairs each region name has in `body`.
|
|
|
|
The invariant `migrate verify` checks. An agent rewriting prose next to a
|
|
boundary can drop or duplicate a marker, and the failure is otherwise silent:
|
|
a lost opening marker turns a generated region into ordinary prose that the
|
|
next write appends a second copy beside.
|
|
"""
|
|
opens = [m.group(1) for m in _ANY_OPEN_RE.finditer(body)]
|
|
closes = [m.group(1) for m in _ANY_CLOSE_RE.finditer(body)]
|
|
names = set(opens) | set(closes)
|
|
return {name: min(opens.count(name), closes.count(name)) for name in sorted(names)}
|
|
|
|
|
|
def unbalanced_markers(body: str) -> list[str]:
|
|
"""Region names whose open and close markers do not pair up."""
|
|
opens = [m.group(1) for m in _ANY_OPEN_RE.finditer(body)]
|
|
closes = [m.group(1) for m in _ANY_CLOSE_RE.finditer(body)]
|
|
return sorted(
|
|
name
|
|
for name in set(opens) | set(closes)
|
|
if opens.count(name) != closes.count(name)
|
|
)
|