feat: Prosa ist kein Identifier - Link-Taxonomie als Enum, generierte Regionen mit Markern (4.0.0)
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
This commit is contained in:
@@ -16,7 +16,7 @@ from __future__ import annotations
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from chemenu import config
|
||||
from chemenu import blocks, config, kb_collections, links
|
||||
from chemenu.frontmatter_io import frontmatter_error
|
||||
from chemenu.markdown_code import strip_code_spans
|
||||
from chemenu.provenance import broken_raw_refs as find_broken_raw_refs
|
||||
@@ -164,7 +164,16 @@ def run_lint(kb_dir: Path) -> dict:
|
||||
# propagated, a deleted page, or a URL pasted where a title belongs - used
|
||||
# to pass every check. Which fields hold page titles is declared by each
|
||||
# type-spec's `page_ref_fields:`, not hardcoded here.
|
||||
def _collection_of(page):
|
||||
try:
|
||||
return page.path.relative_to(config.KB_DIR).parts[0]
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
|
||||
dangling_frontmatter_refs = []
|
||||
malformed_edges: list[dict] = []
|
||||
unlabelled_edges: list[dict] = []
|
||||
unauthorised_labels: list[dict] = []
|
||||
for title, page in sorted(pages.items()):
|
||||
type_path = page.frontmatter.get("type")
|
||||
if not type_path:
|
||||
@@ -174,11 +183,53 @@ def run_lint(kb_dir: Path) -> dict:
|
||||
except ValueError:
|
||||
continue # unresolvable type is already reported as type_resolution_errors
|
||||
for field in ref_fields:
|
||||
for target in page.frontmatter.get(field) or []:
|
||||
# Through `links` so a labelled edge (`- depends-on: Hermes`) is read
|
||||
# as its target rather than as a mapping - the entry carries the
|
||||
# label alongside the title now, and comparing the whole entry would
|
||||
# report every declared edge as dangling.
|
||||
for target in links.targets(page.frontmatter, field):
|
||||
if target not in pages:
|
||||
dangling_frontmatter_refs.append(
|
||||
{"page": title, "field": field, "target": target}
|
||||
)
|
||||
for entry in links.malformed(page.frontmatter, field):
|
||||
malformed_edges.append(
|
||||
{"page": title, "field": field, "entry": str(entry)}
|
||||
)
|
||||
# Labels are checked on `related:` only. `sources:`/`entities:`/
|
||||
# `concepts:` are the provenance path, unlabelled by construction.
|
||||
if "related" in ref_fields:
|
||||
source_collection = _collection_of(page)
|
||||
for edge in links.edges(page.frontmatter, "related"):
|
||||
if not edge.is_labelled:
|
||||
unlabelled_edges.append({"page": title, "target": edge.target})
|
||||
continue
|
||||
target_page = pages.get(edge.target)
|
||||
if source_collection is None or target_page is None:
|
||||
continue
|
||||
destination = _collection_of(target_page)
|
||||
if destination is None:
|
||||
continue
|
||||
allowed = kb_collections.authorised_labels(source_collection, destination)
|
||||
if edge.label not in allowed:
|
||||
unauthorised_labels.append(
|
||||
{
|
||||
"page": title,
|
||||
"target": edge.target,
|
||||
"label": edge.label,
|
||||
"destination": destination,
|
||||
}
|
||||
)
|
||||
|
||||
# A generated region whose markers do not pair up is not a tidiness problem:
|
||||
# the next write appends a second region beside it instead of replacing it,
|
||||
# and the page then carries two. An agent rewriting prose at the boundary is
|
||||
# how a marker goes missing, which is why this is a hard error.
|
||||
unbalanced_marker_findings = [
|
||||
{"page": title, "region": name}
|
||||
for title, page in sorted(pages.items())
|
||||
for name in blocks.unbalanced_markers(page.body)
|
||||
]
|
||||
|
||||
quote_limit_violations = []
|
||||
for title, page in sorted(pages.items()):
|
||||
@@ -238,6 +289,10 @@ def run_lint(kb_dir: Path) -> dict:
|
||||
"undefined_footnote_refs": undefined_footnote_refs,
|
||||
"orphan_footnote_defs": orphan_footnote_defs,
|
||||
"dangling_frontmatter_refs": dangling_frontmatter_refs,
|
||||
"malformed_edges": malformed_edges,
|
||||
"unlabelled_edges": unlabelled_edges,
|
||||
"unauthorised_labels": unauthorised_labels,
|
||||
"unbalanced_markers": unbalanced_marker_findings,
|
||||
"quote_limit_violations": quote_limit_violations,
|
||||
"invalid_type_paths": invalid_type_paths,
|
||||
"type_resolution_errors": type_resolution_errors,
|
||||
@@ -322,6 +377,23 @@ def render_markdown(report: dict) -> str:
|
||||
lines, "Orphan Footnote Definitions", report["orphan_footnote_defs"],
|
||||
lambda i: f"[[{i['page']}]] defines `[^{i['id']}]` (-> [[{i['source']}]]) but nothing references it - run `wikitool cite sync`",
|
||||
)
|
||||
_section(
|
||||
lines, "Malformed Edges", report.get("malformed_edges", []),
|
||||
lambda i: f"[[{i['page']}]] `{i['field']}`: {i['entry']}",
|
||||
)
|
||||
_section(
|
||||
lines, "Unbalanced Generated-Region Markers", report.get("unbalanced_markers", []),
|
||||
lambda i: f"[[{i['page']}]]: `{i['region']}`",
|
||||
)
|
||||
_section(
|
||||
lines, "Unlabelled Edges", report.get("unlabelled_edges", []),
|
||||
lambda i: f"[[{i['page']}]] -> [[{i['target']}]]",
|
||||
)
|
||||
_section(
|
||||
lines, "Labels Not Authorised by the Source Collection",
|
||||
report.get("unauthorised_labels", []),
|
||||
lambda i: f"[[{i['page']}]] `{i['label']}` -> kb/{i['destination']}/ ([[{i['target']}]])",
|
||||
)
|
||||
_section(
|
||||
lines, "Dangling Frontmatter References", report["dangling_frontmatter_refs"],
|
||||
lambda i: f"[[{i['page']}]] `{i['field']}:` names `{i['target']}`, which is not a page",
|
||||
@@ -406,6 +478,16 @@ def default_report_path(report: dict) -> Path:
|
||||
# through the index or navigation only. `quote_limit_violations` is advisory
|
||||
# too - it flags a habit, not a broken tree.
|
||||
#
|
||||
# `unlabelled_edges` and `unauthorised_labels` are advisory **for now**, and
|
||||
# that is a dated decision rather than a judgment about severity: they describe
|
||||
# exactly the state a corpus is in between the 4.0.0 machinery landing and the
|
||||
# migration reaching each page, which is the window `.wikitool-kb.json` exists
|
||||
# to represent. They become hard errors once the migration is recorded - the
|
||||
# same path `legacy_citation_markers` took.
|
||||
#
|
||||
# `malformed_edges` and `unbalanced_markers` are hard from the start: neither
|
||||
# describes an unconverted page, only a broken one.
|
||||
#
|
||||
# One definition, used by `lint --fail-on-error` and by the eval scorecard: if
|
||||
# the two disagreed, a run could pass its score while lint refused it.
|
||||
HARD_ERROR_KEYS = (
|
||||
@@ -421,6 +503,8 @@ HARD_ERROR_KEYS = (
|
||||
"undefined_footnote_refs",
|
||||
"orphan_footnote_defs",
|
||||
"dangling_frontmatter_refs",
|
||||
"malformed_edges",
|
||||
"unbalanced_markers",
|
||||
"invalid_type_paths",
|
||||
"type_resolution_errors",
|
||||
"schema_validation_errors",
|
||||
|
||||
Reference in New Issue
Block a user