fix: comparison and source pages accept the sources: cite add writes; sources may cite sources (#173)
Files changed: - CHANGES.md - VERSION - instructions/wiki-ingest/SKILL.md - kb/CONTRACT.md - tools/CONTRACT.md - tools/chemenu/commands/cite_cmd.py - tools/chemenu/commands/xref.py - tools/chemenu/tests/test_cite_cmd.py - tools/chemenu/tests/test_page_ops.py - tools/chemenu/tests/test_type_resolver.py - tools/chemenu/tests/test_xref.py - types/comparison.md - types/comparison.schema.yaml - types/source.md - types/source.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
662a844cf1
commit
862bc04d9c
15 files changed
+310
-19
No files matched your search
@@ -203,3 +203,121 @@ def test_cite_sync_command_over_kb(kb_dir, raw_dir, monkeypatch):
|
||||
result = runner.invoke(app, ["cite", "sync", "--all", "--dry-run"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "No pages needed a Footnotes block change." in result.output
|
||||
|
||||
|
||||
# --- citing on comparison and source pages ----------------------------------
|
||||
#
|
||||
# Both types once declared no `sources:` field, while `cite add` wrote one onto
|
||||
# every page it touched - so a correctly cited comparison or source page failed
|
||||
# `lint` with `'sources' was unexpected`, under `additionalProperties: false`.
|
||||
|
||||
|
||||
def _write_source(kb_dir, title, raw_file):
|
||||
"""A source page the shipped schema accepts as it stands - the fixture's own
|
||||
`Source - Aurora` carries a stray `source:` key and would mask the finding."""
|
||||
from chemenu.frontmatter_io import write_page
|
||||
|
||||
path = kb_dir / "sources/notes" / f"{title}.md"
|
||||
write_page(
|
||||
path,
|
||||
{
|
||||
"type": "types/source.md", "source_type": "notes", "author": "Fixture Author",
|
||||
"raw_files": [raw_file], "date": "2026-08-02", "summary": f"{title} summary",
|
||||
},
|
||||
f"\n# Source: {title}\n\n## Summary\n\nNotes.\n",
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
def _schema_errors_for(kb_dir, title):
|
||||
from chemenu.lint_core import run_lint
|
||||
|
||||
return [e for e in run_lint(kb_dir)["schema_validation_errors"] if e["page"] == title]
|
||||
|
||||
|
||||
def test_cite_add_on_a_comparison_page_keeps_it_schema_valid(kb_dir, raw_dir):
|
||||
from chemenu.cli import app
|
||||
from chemenu.frontmatter_io import write_page
|
||||
|
||||
path = kb_dir / "comparisons/aurora vs Borealis.md"
|
||||
write_page(
|
||||
path,
|
||||
{
|
||||
"type": "types/comparison.md", "created": "2026-08-02",
|
||||
"entities": ["aurora", "Borealis"], "summary": "Server against workstation",
|
||||
},
|
||||
"\n# Comparison: aurora vs Borealis\n\n## Überblick\n\nTwo machines.\n",
|
||||
)
|
||||
assert _schema_errors_for(kb_dir, "aurora vs Borealis") == []
|
||||
|
||||
result = runner.invoke(
|
||||
app, ["cite", "add", "--page", "aurora vs Borealis", "--source", "Source - Aurora"]
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
fm, _body = read_page(path)
|
||||
assert fm["sources"] == ["Source - Aurora"]
|
||||
assert _schema_errors_for(kb_dir, "aurora vs Borealis") == []
|
||||
|
||||
|
||||
def test_a_source_page_may_cite_another_source_page(kb_dir, raw_dir):
|
||||
"""The citation lands on the citing side only, in the direction it was made."""
|
||||
from chemenu.cli import app
|
||||
from chemenu.provenance import citing_pages
|
||||
|
||||
path_a = _write_source(kb_dir, "Source - A", "raw/notes/Aurora.md")
|
||||
path_b = _write_source(kb_dir, "Source - B", "raw/notes/Uningested.md")
|
||||
before_b = path_b.read_bytes()
|
||||
|
||||
result = runner.invoke(app, ["cite", "add", "--page", "Source - A", "--source", "Source - B"])
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
fm_a, _body = read_page(path_a)
|
||||
assert fm_a["sources"] == ["Source - B"]
|
||||
assert path_b.read_bytes() == before_b
|
||||
assert _schema_errors_for(kb_dir, "Source - A") == []
|
||||
assert "Source - A" in citing_pages(load_kb_pages(kb_dir), "Source - B")
|
||||
|
||||
|
||||
def test_a_source_citing_its_own_file_writes_no_sources_entry(kb_dir, raw_dir):
|
||||
"""A self-citation names one of the page's own raw files. The definition is
|
||||
written; `sources:` is left exactly as it was - not even an empty list."""
|
||||
path = _write_source(kb_dir, "Source - A", "raw/notes/Aurora.md")
|
||||
page = _reload(kb_dir, "Source - A")
|
||||
assert "sources" not in page.frontmatter
|
||||
|
||||
marker_id, body, changed = upsert_citation(page, "Source - A", "Aurora.md")
|
||||
assert changed is True
|
||||
assert "sources" not in page.frontmatter
|
||||
assert f"[^{marker_id}]: [[Source - A|Aurora.md]]" in body
|
||||
|
||||
page.body = body
|
||||
_id, _body, changed_again = upsert_citation(page, "Source - A", "Aurora.md")
|
||||
assert changed_again is False
|
||||
assert path.exists()
|
||||
|
||||
|
||||
def test_a_self_citation_leaves_an_existing_sources_list_unchanged(kb_dir, raw_dir):
|
||||
_write_source(kb_dir, "Source - A", "raw/notes/Aurora.md")
|
||||
page = _reload(kb_dir, "Source - A")
|
||||
page.frontmatter["sources"] = ["Source - Aurora"]
|
||||
|
||||
upsert_citation(page, "Source - A", "Aurora.md")
|
||||
assert page.frontmatter["sources"] == ["Source - Aurora"]
|
||||
|
||||
|
||||
def test_cite_add_self_citation_on_disk(kb_dir, raw_dir):
|
||||
from chemenu.cli import app
|
||||
|
||||
path = _write_source(kb_dir, "Source - A", "raw/notes/Aurora.md")
|
||||
args = ["cite", "add", "--page", "Source - A", "--source", "Source - A", "--file", "Aurora.md"]
|
||||
result = runner.invoke(app, args)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
fm, body = read_page(path)
|
||||
assert "sources" not in fm
|
||||
assert f"[^{cite_id('Source - A', 'Aurora.md')}]: [[Source - A|Aurora.md]]" in body
|
||||
assert _schema_errors_for(kb_dir, "Source - A") == []
|
||||
|
||||
again = runner.invoke(app, args)
|
||||
assert again.exit_code == 0, again.output
|
||||
assert "Already up to date" in again.output
|
||||
@@ -193,6 +193,36 @@ def test_rename_fixes_a_dangling_source_reference(patched_wiki):
|
||||
assert frontmatter["sources"] == ["Source - Aurora Notes"]
|
||||
|
||||
|
||||
def test_rename_carries_sources_on_comparison_and_source_pages(patched_wiki):
|
||||
"""Both types declare `sources` in `page_ref_fields:`. A schema-only fix
|
||||
would have let them validate, but a rename would then have left a dead
|
||||
`sources:` entry behind on each - unreported, since `lint` resolves only
|
||||
the declared fields too."""
|
||||
from chemenu.lint_core import run_lint
|
||||
|
||||
write_page(
|
||||
patched_wiki / "comparisons/aurora vs Borealis.md",
|
||||
{"type": "types/comparison.md", "created": "2026-08-02",
|
||||
"entities": ["aurora", "Borealis"], "sources": ["Source - Aurora"],
|
||||
"summary": "Server against workstation"},
|
||||
"\n# Comparison: aurora vs Borealis\n",
|
||||
)
|
||||
write_page(
|
||||
patched_wiki / "sources/notes/Source - B.md",
|
||||
{"type": "types/source.md", "source_type": "notes", "author": "Fixture Author",
|
||||
"raw_files": ["raw/notes/Uningested.md"], "date": "2026-08-02",
|
||||
"sources": ["Source - Aurora"], "summary": "B"},
|
||||
"\n# Source: B\n",
|
||||
)
|
||||
page_ops.rename_command(old="Source - Aurora", new="Source - Aurora Notes", dry_run=False)
|
||||
|
||||
for path in ("comparisons/aurora vs Borealis.md", "sources/notes/Source - B.md"):
|
||||
frontmatter, _body = read_page(patched_wiki / path)
|
||||
assert frontmatter["sources"] == ["Source - Aurora Notes"]
|
||||
dangling = {e["page"] for e in run_lint(patched_wiki)["dangling_frontmatter_refs"]}
|
||||
assert not dangling & {"aurora vs Borealis", "Source - B"}
|
||||
|
||||
|
||||
def test_rename_stops_before_renaming_the_file_if_a_reference_write_fails(patched_wiki, monkeypatch):
|
||||
"""A write failing partway through must not still rename the target file -
|
||||
that would leave some references pointing at the new title and others
|
||||
|
||||
@@ -45,8 +45,8 @@ def test_get_page_ref_fields_reads_the_type_spec():
|
||||
so `lint`/`rename`/`rm` need no hardcoded list to update for a new type."""
|
||||
assert resolver.get_page_ref_fields("types/entity.md") == ["related", "sources"]
|
||||
assert resolver.get_page_ref_fields("types/concept.md") == ["related", "sources"]
|
||||
assert resolver.get_page_ref_fields("types/source.md") == ["entities", "concepts"]
|
||||
assert resolver.get_page_ref_fields("types/comparison.md") == ["entities", "related"]
|
||||
assert resolver.get_page_ref_fields("types/source.md") == ["entities", "concepts", "sources"]
|
||||
assert resolver.get_page_ref_fields("types/comparison.md") == ["entities", "related", "sources"]
|
||||
|
||||
|
||||
def test_page_ref_fields_exist_in_the_type_schema():
|
||||
|
||||
@@ -482,6 +482,38 @@ def test_xref_link_source_dry_run_leaves_the_source_page_alone(kb_dir):
|
||||
assert path.read_text(encoding="utf-8") == before
|
||||
|
||||
|
||||
def test_xref_link_source_refuses_a_target_that_is_itself_a_source(kb_dir):
|
||||
"""`link-source` means "A mentions X" and writes `X.sources += A`; for X =
|
||||
another source that claims "B cites A", and the back-reference would add
|
||||
`A.sources += B` too. A citation between sources has a direction, and only
|
||||
`cite add` says which one - so the target is refused, the rest still linked."""
|
||||
from chemenu.frontmatter_io import write_page
|
||||
|
||||
runner, app = _runner_env(kb_dir)
|
||||
path_b = kb_dir / "sources/notes/Source - B.md"
|
||||
write_page(
|
||||
path_b,
|
||||
{"type": "types/source.md", "source_type": "notes", "author": "Fixture Author",
|
||||
"raw_files": ["raw/notes/Uningested.md"], "date": "2026-08-02", "summary": "B"},
|
||||
"\n# Source: B\n",
|
||||
)
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["xref", "link-source", "--source", "Source - Aurora", "--entities", "gdeploy,Source - B"],
|
||||
)
|
||||
assert result.exit_code == 1
|
||||
assert "Source - B" in result.output
|
||||
assert 'cite add --page "Source - Aurora" --source "Source - B"' in result.output
|
||||
|
||||
pages = load_kb_pages(kb_dir)
|
||||
assert "Source - Aurora" in pages["gdeploy"].frontmatter["sources"]
|
||||
source_a = pages["Source - Aurora"].frontmatter
|
||||
source_b = pages["Source - B"].frontmatter
|
||||
for field in ("sources", "entities", "concepts"):
|
||||
assert "Source - B" not in (source_a.get(field) or [])
|
||||
assert "Source - Aurora" not in (source_b.get(field) or [])
|
||||
|
||||
|
||||
# --- the inbound view ------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user