feat: Autorenkonventionen nach Eigentum geschnitten - kb/CONVENTIONS.md, deklarierte Collections (3.0.0)
Files changed: - .gitea/workflows/ci.yml - .wikitool-kb.json - AGENTS.md - CHANGES.md - INSTALL.md - README.md - VERSION - instructions/CONTRACT.md - instructions/dev/testing-conventions.md - instructions/german-terminology.md - instructions/kb-profiles.md - instructions/migrations/3.0.0-authoring-conventions.md - instructions/private-instance.md - instructions/setup-instance.md - instructions/wiki-ingest/SKILL.md - instructions/wiki-manage/SKILL.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/commands/dist_cmd.py - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/doctor.py - tools/chemenu/commands/new_page.py - tools/chemenu/conventions.py - tools/chemenu/kb_collections.py - tools/chemenu/kb_scan.py - tools/chemenu/provenance.py - tools/chemenu/sections.py - tools/chemenu/tests/conftest.py - tools/chemenu/tests/test_conventions.py - tools/chemenu/tests/test_dist_cmd.py - tools/chemenu/tests/test_doctor.py - tools/chemenu/tests/test_new_page.py - tools/chemenu/tests/test_types_cmd.py - types/comparison.md - types/concept.md - types/entity.md - types/source.md - types/type-spec.md
This commit is contained in:
1 parent
9843df99d3
commit
502971d147
45 files changed
+1817
-232
No files matched your search
@@ -2,9 +2,13 @@
|
||||
repo's machinery.
|
||||
|
||||
`export` copies the pipeline's schema/compiler/control-plane layers (types/,
|
||||
tools/, instructions/, the stage contracts, every kb/*/COLLECTION.md) into an
|
||||
empty target, with no kb/ pages, no raw/ content, and no git history - see
|
||||
instructions/setup-instance.md for what happens after. It never calls git.
|
||||
tools/, instructions/, the stage contracts) into an empty target, with no kb/
|
||||
pages, no raw/ content, and no git history - see instructions/setup-instance.md
|
||||
for what happens after. It never calls git.
|
||||
|
||||
The two binding-but-instance-owned documents under kb/ - each collection's
|
||||
COLLECTION.md and kb/CONVENTIONS.md - cross as `.template` and are adopted by a
|
||||
rename, the same split USER.md/SOUL.md use at the repo root.
|
||||
|
||||
Three independent exclusion mechanisms feed the plan, for three different
|
||||
shapes of "does not belong in someone else's instance":
|
||||
@@ -35,7 +39,7 @@ from typing import Callable, NamedTuple, Optional, Union
|
||||
|
||||
import typer
|
||||
|
||||
from chemenu import config, kb_collections, kb_state, version as version_mod
|
||||
from chemenu import config, conventions, kb_collections, kb_state, version as version_mod
|
||||
from chemenu.commands._util import fail, rel_path, success, today_iso
|
||||
|
||||
app = typer.Typer(help="Build a distributable copy of the wiki machinery.")
|
||||
@@ -289,12 +293,32 @@ def build_plan(origin: Optional[Origin] = None) -> dict[str, PlannedFile]:
|
||||
for hook_dir in HOOK_DIRS:
|
||||
plan.update(_copy_tree(config.ROOT / hook_dir, hook_dir, frozenset()))
|
||||
|
||||
# `kb/CONTRACT.md` is stack-owned and ships verbatim; everything beside it
|
||||
# under `kb/` is the instance's own and ships only as a `.template`. That is
|
||||
# the personalization split (`USER.md`/`SOUL.md`) one directory down, and
|
||||
# the reason for it is the same: a distribution can say what the file
|
||||
# decides, never what this instance decided.
|
||||
kb_contract = config.KB_DIR / "CONTRACT.md"
|
||||
if kb_contract.is_file():
|
||||
plan["kb/CONTRACT.md"] = _read_planned_file(kb_contract, "kb/CONTRACT.md")
|
||||
|
||||
conventions_template = config.KB_DIR / conventions.CONVENTIONS_TEMPLATE
|
||||
if conventions_template.is_file():
|
||||
rel = f"kb/{conventions.CONVENTIONS_TEMPLATE}"
|
||||
plan[rel] = _read_planned_file(conventions_template, rel)
|
||||
|
||||
# A collection contract is instance-owned too, but unlike `USER.md` the
|
||||
# shipped content is not *wrong* for the receiver - it is the profile this
|
||||
# repo's own collections adopted, and a fine starting point. So the file
|
||||
# itself ships, under the template name: one source of truth here, and a
|
||||
# receiving instance that has to rename it before it counts. Keeping a
|
||||
# separate `.template` beside each contract would have meant maintaining two
|
||||
# near-identical copies of the same text, which is the drift AGENTS.md
|
||||
# invariant 8 exists to prevent.
|
||||
for collection in kb_collections.iter_kb_collections():
|
||||
rel = f"kb/{collection.name}/COLLECTION.md"
|
||||
plan[rel] = _read_planned_file(collection / "COLLECTION.md", rel)
|
||||
source = collection / kb_collections.CONTRACT_NAME
|
||||
rel = f"kb/{collection.name}/{kb_collections.CONTRACT_NAME}.template"
|
||||
plan[rel] = _read_planned_file(source, rel)
|
||||
|
||||
for relative in CONTRACT_ONLY_STAGES:
|
||||
source = config.ROOT / relative
|
||||
@@ -339,8 +363,21 @@ def build_plan(origin: Optional[Origin] = None) -> dict[str, PlannedFile]:
|
||||
# IP literals) was considered and rejected - the project's own host legitimately
|
||||
# appears in INSTALL.md and version.py, so such a scan would either whitelist
|
||||
# the very string it is looking for or cry wolf on every export.
|
||||
#
|
||||
# `COLLECTION.md` and `CONVENTIONS.md` are deliberately *not* on the allowed
|
||||
# list any more. Both bind, and both are the instance's to write, so they cross
|
||||
# the boundary as `.template` and are adopted by a rename - a plan carrying the
|
||||
# filled name would hand a new instance this one's authoring conventions as
|
||||
# though they were the stack's.
|
||||
_CONTENT_PREFIXES = ("kb/", "raw/")
|
||||
_CONTENT_ALLOWED_NAMES = ("CONTRACT.md", "COLLECTION.md", "log.md", ".gitkeep")
|
||||
_CONTENT_ALLOWED_NAMES = (
|
||||
"CONTRACT.md",
|
||||
f"{kb_collections.CONTRACT_NAME}.template",
|
||||
conventions.CONVENTIONS_TEMPLATE,
|
||||
"log.md",
|
||||
".gitkeep",
|
||||
)
|
||||
_INSTANCE_OWNED_KB_FILES = (kb_collections.CONTRACT_NAME, conventions.CONVENTIONS_FILENAME)
|
||||
|
||||
|
||||
def find_leaks(plan: dict[str, PlannedFile]) -> list[str]:
|
||||
@@ -350,6 +387,8 @@ def find_leaks(plan: dict[str, PlannedFile]) -> list[str]:
|
||||
name = relative.rsplit("/", 1)[-1]
|
||||
if name in config.PERSONALIZATION_FILES or name == config.ENVIRONMENT_FILE:
|
||||
leaks.append(f"{relative} (one instance's own personalization)")
|
||||
elif relative.startswith("kb/") and name in _INSTANCE_OWNED_KB_FILES:
|
||||
leaks.append(f"{relative} (this instance's authoring conventions; ship the .template)")
|
||||
elif relative.startswith("instructions/dev/"):
|
||||
leaks.append(f"{relative} (stack-development only)")
|
||||
elif relative.startswith(_CONTENT_PREFIXES) and name not in _CONTENT_ALLOWED_NAMES:
|
||||
@@ -394,7 +433,8 @@ def export_command(
|
||||
AGENTS.md/README.md (dev-instance-only marker blocks removed),
|
||||
instructions/ (no instructions/dev/), types/, tools/ (no venv/caches),
|
||||
the .github/hooks/+.vibe session-tracing config plus .claude/settings.json,
|
||||
every kb/*/COLLECTION.md (no pages, no areas), empty
|
||||
kb/CONTRACT.md plus a COLLECTION.md.template per collection and
|
||||
kb/CONVENTIONS.md.template (no pages, no areas), empty
|
||||
raw/{articles,documents,notes,assets}/, VERSION, the USER.md/SOUL.md
|
||||
personalization templates (never the filled files), and a
|
||||
.wikitool-release.json stamp. The --source-*/--release-url/--update-url
|
||||
|
||||
@@ -37,7 +37,7 @@ from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from chemenu import config, kb_collections, version as version_mod
|
||||
from chemenu import config, conventions, kb_collections, version as version_mod
|
||||
from chemenu.commands._util import fail, rel_path, success
|
||||
|
||||
app = typer.Typer(help="Verify documentation that mirrors the code or repo layout.")
|
||||
@@ -114,6 +114,12 @@ REQUIRED_TRACKED_PATHS = (
|
||||
"instructions/CONTRACT.md",
|
||||
"instructions/wiki-query/SKILL.md",
|
||||
"ENVIRONMENT.md.template",
|
||||
# The one `.template` that lives under a content directory. It is what a
|
||||
# distribution ships in place of this instance's own `kb/CONVENTIONS.md`, so
|
||||
# an ignore rule reaching it would produce exports whose receiving instance
|
||||
# has nothing to fill in - and `find_leaks` refuses to substitute the filled
|
||||
# file, correctly, so the export would simply be missing it.
|
||||
"kb/CONVENTIONS.md.template",
|
||||
)
|
||||
|
||||
CLI_README = config.ROOT / "tools" / "CONTRACT.md"
|
||||
@@ -207,13 +213,22 @@ def check_cli_readme() -> list[str]:
|
||||
|
||||
|
||||
def check_collection_contracts() -> list[str]:
|
||||
"""The three structural rules that define what a collection is.
|
||||
"""The structural rules that define what a collection is, plus what each one
|
||||
has to declare about itself.
|
||||
|
||||
Collections are discovered by contract presence rather than listed here, so
|
||||
`mkdir kb/<name>` + a COLLECTION.md is all it takes to add one. That only
|
||||
works if the inverse is also checked: a directory under kb/ *without* a
|
||||
contract is an unclaimed subtree whose pages obey no local rules, and a
|
||||
contract outside kb/ quietly widens "collection" back out to "any directory".
|
||||
|
||||
Presence alone stopped being enough once the contracts became
|
||||
instance-owned. A `COLLECTION.md` an instance wrote can be about anything,
|
||||
so the two facts the stack still needs from it - which profile it adopted,
|
||||
and whether the stack resolves against it by name - are declared in its
|
||||
frontmatter and checked here (`kb_collections.declaration_issues`), together
|
||||
with the shape of `kb/CONVENTIONS.md`, whose section names the compiler
|
||||
reads.
|
||||
"""
|
||||
issues = []
|
||||
|
||||
@@ -243,6 +258,9 @@ def check_collection_contracts() -> list[str]:
|
||||
if not (config.ROOT / relative_path).exists():
|
||||
issues.append(f"{relative_path} is missing - it is the authoring contract for its stage")
|
||||
|
||||
issues += kb_collections.declaration_issues()
|
||||
issues += conventions.declaration_issues()
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from typing import Optional
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from chemenu import config, kb_collections, version as version_mod
|
||||
from chemenu import config, conventions, kb_collections, version as version_mod
|
||||
from chemenu.commands import git_publish, instructions_cmd
|
||||
from chemenu.commands._util import rel_path
|
||||
from chemenu.session import ENV_VAR as SESSION_ENV_VAR
|
||||
@@ -213,6 +213,47 @@ def check_personalization() -> Check:
|
||||
return Check("personalization", "OK", f"{', '.join(config.PERSONALIZATION_FILES)} present and filled")
|
||||
|
||||
|
||||
def check_conventions() -> Check:
|
||||
"""Whether this instance has said how its own pages are written.
|
||||
|
||||
`kb/CONVENTIONS.md` carries the decisions `kb/CONTRACT.md` deliberately no
|
||||
longer makes: the KB language and its three tool-owned section headings, the
|
||||
relationship-label vocabulary, the tone examples, the confidence rubric, the
|
||||
ADR prefix. The compiler reads the section names out of it, so an instance
|
||||
without one is not merely undocumented - `xref add` and `cite add` fall back
|
||||
to the names this stack hardcoded before the file existed, which is right
|
||||
only for a corpus that was written under them.
|
||||
|
||||
Hence `FAIL` rather than `WARN`, and hence the same two failure modes the
|
||||
personalization pair has: the distribution can ship the template but never
|
||||
the filled file, so a template renamed and left unanswered looks present and
|
||||
decides nothing.
|
||||
"""
|
||||
path = conventions.conventions_file()
|
||||
fix = (
|
||||
"Copy kb/CONVENTIONS.md.template to kb/CONVENTIONS.md and answer it - the KB-language "
|
||||
"step of instructions/setup-instance.md walks it, and instructions/kb-profiles.md has "
|
||||
"the ready-made profiles to adopt"
|
||||
)
|
||||
if not path.is_file():
|
||||
return Check(
|
||||
"conventions", "FAIL",
|
||||
f"kb/{conventions.CONVENTIONS_FILENAME} is missing - this instance has not "
|
||||
"declared how its pages are written",
|
||||
fix,
|
||||
)
|
||||
issues = conventions.declaration_issues()
|
||||
if issues:
|
||||
return Check("conventions", "FAIL", "; ".join(issues), fix)
|
||||
declared = conventions.language() or "unspecified"
|
||||
headings = ", ".join(conventions.canonical(slot) for slot in conventions.SLOTS)
|
||||
return Check(
|
||||
"conventions", "OK",
|
||||
f"kb/{conventions.CONVENTIONS_FILENAME} present, language {declared}, "
|
||||
f"sections {headings}",
|
||||
)
|
||||
|
||||
|
||||
def check_environment() -> Check:
|
||||
"""Whether this checkout records the environment it works through.
|
||||
|
||||
@@ -399,6 +440,7 @@ def run_doctor() -> list[Check]:
|
||||
check_skills(),
|
||||
check_structure(),
|
||||
check_personalization(),
|
||||
check_conventions(),
|
||||
check_environment(),
|
||||
check_publish_remotes(),
|
||||
check_generated_files(),
|
||||
@@ -411,9 +453,9 @@ def doctor_command(
|
||||
json_out: bool = typer.Option(False, "--json", help="Print the checks as JSON"),
|
||||
):
|
||||
"""Check that this instance is correctly configured: dependencies, author,
|
||||
git identity/remote, published skills, structure, personalization,
|
||||
generated files, and session scoping. Read-only. Exits 1 only if a check
|
||||
FAILs."""
|
||||
git identity/remote, published skills, structure, personalization, KB
|
||||
conventions, generated files, and session scoping. Read-only. Exits 1 only
|
||||
if a check FAILs."""
|
||||
checks = run_doctor()
|
||||
|
||||
if json_out:
|
||||
|
||||
@@ -24,7 +24,7 @@ import re
|
||||
|
||||
import typer
|
||||
|
||||
from chemenu import config
|
||||
from chemenu import config, conventions
|
||||
from chemenu.commands._util import (
|
||||
check_collision,
|
||||
check_raw_files_exist,
|
||||
@@ -166,7 +166,11 @@ 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}` - plain substitution from `variables[field]`, including the
|
||||
`{section.<slot>}` names this instance gave the three tool-owned
|
||||
headings (see chemenu.conventions). Those are what took the KB language
|
||||
out of `types/*.md`: a template writes `## {section.relationships}`, so
|
||||
scaffolding a page in another language needs no edit under `types/`
|
||||
- `{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
|
||||
@@ -324,7 +328,13 @@ def new_page_command(
|
||||
|
||||
path = target_dir / f"{page_title}.md"
|
||||
body = _apply_template_variables(
|
||||
template, {**frontmatter, "name": name, "today": today.isoformat()}
|
||||
template,
|
||||
{
|
||||
**frontmatter,
|
||||
"name": name,
|
||||
"today": today.isoformat(),
|
||||
**conventions.section_variables(),
|
||||
},
|
||||
)
|
||||
|
||||
write_page(path, frontmatter, body)
|
||||
|
||||
Reference in new issue
Block a user