feat: people live on their organization's page until promoted; organization subtype, member-of, broken_anchors lint (#172)
Files changed: - CHANGES.md - README.md - VERSION - instructions/kb-profiles.md - instructions/link-taxonomy.md - instructions/page-lifecycle.md - instructions/wiki-ingest/SKILL.md - instructions/wiki-lint/SKILL.md - kb/entities/COLLECTION.md - kb/entities/INDEX.md - kb/entities/organizations/E3DC GmbH.md - kb/entities/people/E3DC GmbH.md - kb/index.md - kb/log.md - tools/CONTRACT.md - tools/chemenu/commands/lint.py - tools/chemenu/kb_scan.py - tools/chemenu/lint_core.py - tools/chemenu/tests/test_lint.py - tools/chemenu/tests/test_new_page.py - tools/chemenu/tests/test_type_resolver.py - tools/chemenu/tests/test_types_cmd.py - types/entity.md - types/entity.organization.md - types/entity.person.md - types/entity.schema.yaml Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
1 parent
f3ccbd86f9
commit
4ec22d376d
25 files changed
+368
-30
No files matched your search
@@ -103,6 +103,82 @@ def test_a_wrapped_link_inside_code_is_no_finding(kb_dir):
|
||||
assert report["broken_links"] == []
|
||||
|
||||
|
||||
def _organization_with_people(kb_dir) -> None:
|
||||
"""An organization page holding its people as sections - the shape
|
||||
`broken_anchors` exists for - plus a heading that only appears inside a
|
||||
fence, which must not count as one."""
|
||||
write_page(
|
||||
kb_dir / "entities/organizations/Kunde X.md",
|
||||
{"type": "types/entity.md", "entity_type": "organization", "tags": [],
|
||||
"created": "2026-10-04", "modified": "2026-10-04", "related": [], "sources": []},
|
||||
"\n# Kunde X\n\n## Personen\n\n### Anna Müller\n\nEinkauf.\n\n"
|
||||
"#### `Bob` Builder ##\n\nBetrieb.\n\n```\n## Fenced\n```\n",
|
||||
)
|
||||
|
||||
|
||||
def test_an_anchor_naming_an_existing_heading_is_no_finding(kb_dir):
|
||||
"""Any heading level, any capitalisation, inline-code backticks and closing
|
||||
hashes ignored, an alias after the anchor, a nested anchor whose every
|
||||
segment is a heading."""
|
||||
_organization_with_people(kb_dir)
|
||||
_gdeploy_saying(
|
||||
kb_dir,
|
||||
"[[Kunde X#Anna Müller]], [[Kunde X#anna MÜLLER|Anna]], [[Kunde X#Bob Builder]], "
|
||||
"[[Kunde X#Personen#Anna Müller]], [[Kunde X#Kunde X]].",
|
||||
)
|
||||
report = run_lint(kb_dir)
|
||||
assert report["broken_anchors"] == []
|
||||
assert report["broken_links"] == []
|
||||
|
||||
|
||||
def test_an_anchor_naming_no_heading_is_a_finding(kb_dir):
|
||||
"""The promoted-person case: the page is still there, the section is not.
|
||||
The anchor is reported as written, not in its normalized form."""
|
||||
_organization_with_people(kb_dir)
|
||||
_gdeploy_saying(kb_dir, "Ask [[Kunde X#Carla Neu|Carla]] or [[Kunde X#Personen#Dora]].")
|
||||
report = run_lint(kb_dir)
|
||||
assert report["broken_anchors"] == [
|
||||
{"page": "gdeploy", "target": "Kunde X", "anchor": "Carla Neu"},
|
||||
{"page": "gdeploy", "target": "Kunde X", "anchor": "Personen#Dora"},
|
||||
]
|
||||
assert report["broken_links"] == []
|
||||
assert "has no such heading" in render_markdown(report)
|
||||
|
||||
|
||||
def test_a_heading_inside_a_fence_is_not_a_section(kb_dir):
|
||||
_organization_with_people(kb_dir)
|
||||
_gdeploy_saying(kb_dir, "See [[Kunde X#Fenced]].")
|
||||
assert run_lint(kb_dir)["broken_anchors"] == [
|
||||
{"page": "gdeploy", "target": "Kunde X", "anchor": "Fenced"},
|
||||
]
|
||||
|
||||
|
||||
def test_an_anchor_on_a_missing_page_is_only_a_broken_link(kb_dir):
|
||||
_gdeploy_saying(kb_dir, "See [[Nonexistent Page#Somewhere]].")
|
||||
report = run_lint(kb_dir)
|
||||
assert {"page": "gdeploy", "target": "Nonexistent Page"} in report["broken_links"]
|
||||
assert report["broken_anchors"] == []
|
||||
|
||||
|
||||
def test_an_anchor_inside_code_and_a_bare_self_anchor_are_no_finding(kb_dir):
|
||||
_organization_with_people(kb_dir)
|
||||
_gdeploy_saying(
|
||||
kb_dir,
|
||||
"Write `[[Kunde X#Nowhere]]` like this, or:\n\n```\n[[Kunde X#Nowhere]]\n```\n\n"
|
||||
"Jump to [[#Nowhere]].",
|
||||
)
|
||||
assert run_lint(kb_dir)["broken_anchors"] == []
|
||||
|
||||
|
||||
def test_a_broken_anchor_alone_keeps_lint_green():
|
||||
"""Advisory: the link still reaches its page, and a hard finding would turn
|
||||
an existing instance's lint red on the upgrade that shipped the check."""
|
||||
assert "broken_anchors" not in HARD_ERROR_KEYS
|
||||
report = {key: [] for key in HARD_ERROR_KEYS}
|
||||
report["broken_anchors"] = [{"page": "a", "target": "b", "anchor": "c"}]
|
||||
assert not has_hard_errors(report)
|
||||
|
||||
|
||||
def test_lint_fixture_has_no_dangling_frontmatter_refs(kb_dir):
|
||||
assert run_lint(kb_dir)["dangling_frontmatter_refs"] == []
|
||||
|
||||
|
||||
@@ -1127,6 +1127,19 @@ def test_new_person_scaffolds_the_person_template(monkeypatch, kb_dir):
|
||||
assert "{" not in body
|
||||
|
||||
|
||||
def test_new_organization_scaffolds_the_organization_template(monkeypatch, kb_dir):
|
||||
"""An organization is its own subtype with its own area, and its skeleton
|
||||
carries the section its people live in until they earn a page."""
|
||||
result = _invoke_new(monkeypatch, kb_dir, [
|
||||
"new", "entity", "--name", "Kunde X", "--set", "entity_type=organization",
|
||||
])
|
||||
assert result.exit_code == 0, result.output
|
||||
_fm, body = read_page(kb_dir / "entities/organizations/Kunde X.md")
|
||||
assert body.strip() == _rendered("entity", "entity_type", "organization", "Kunde X")
|
||||
assert "## Personen" in body
|
||||
assert "{" not in body
|
||||
|
||||
|
||||
def test_new_decision_scaffolds_the_decision_template(monkeypatch, kb_dir):
|
||||
result = _invoke_new(monkeypatch, kb_dir, [
|
||||
"new", "concept", "--name", "Flat Subtype Files", "--set", "concept_type=decision",
|
||||
|
||||
@@ -8,7 +8,7 @@ def test_get_enum_returns_schema_declared_values():
|
||||
"""entity_type's valid values come from entity.schema.yaml's enum - this
|
||||
is what lets config.py and new_page.py stop hand-maintaining that list."""
|
||||
values = resolver.get_enum("types/entity.md", "entity_type")
|
||||
assert values == ["codebase", "system", "tool", "technology", "person"]
|
||||
assert values == ["codebase", "system", "tool", "technology", "person", "organization"]
|
||||
|
||||
|
||||
def test_get_enum_shared_across_types():
|
||||
@@ -147,10 +147,11 @@ def test_get_layout_reads_entity_type_specs_own_layout_field():
|
||||
"tool": "tools",
|
||||
"technology": "technologies",
|
||||
"person": "people",
|
||||
"organization": "organizations",
|
||||
}
|
||||
assert all(spec.get("title") for spec in layout.values())
|
||||
# Order drives kb/index.md section order.
|
||||
assert list(layout) == ["codebase", "system", "tool", "technology", "person"]
|
||||
assert list(layout) == ["codebase", "system", "tool", "technology", "person", "organization"]
|
||||
|
||||
|
||||
def test_concept_layout_covers_every_declared_concept_type():
|
||||
|
||||
@@ -41,7 +41,7 @@ def test_types_describe_entity_reports_schema_and_body():
|
||||
fields_by_name = {f["field"]: f for f in data["fields"]}
|
||||
assert fields_by_name["entity_type"]["required"] is True
|
||||
assert fields_by_name["entity_type"]["enum"] == [
|
||||
"codebase", "system", "tool", "technology", "person",
|
||||
"codebase", "system", "tool", "technology", "person", "organization",
|
||||
]
|
||||
assert fields_by_name["tags"]["required"] is False
|
||||
# The body must carry the page skeleton an authoring LLM works from...
|
||||
|
||||
Reference in new issue
Block a user