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
+42
-1
@@ -59,7 +59,7 @@ concern - readable here, never shipped as something to parse.
|
||||
|
||||
---
|
||||
|
||||
## 8.0.0-beta.39 - 2026-10-04 - wiki-ingest and wiki-manage call xref add with --rel, not the --rel-a/--rel-b removed in 4.0.0
|
||||
## 8.0.0-beta.40 - 2026-10-04 - Comparison and source pages accept the sources: that cite add writes; sources may cite sources
|
||||
|
||||
**Author:** Torben Nehmer
|
||||
|
||||
@@ -107,6 +107,7 @@ concern - readable here, never shipped as something to parse.
|
||||
- Link-Katalog: Beteiligungs- und RACI-Label von der Projektseite aus
|
||||
- Organisationsseiten: Personen als Abschnitt mit Aufstieg, entity_type organization, member-of, Lint-Befund broken_anchors
|
||||
- lint: Unfilled Template Sections - a section still holding only its template's TODO placeholders (advisory)
|
||||
- Comparison and source pages accept the sources: that cite add writes; sources may cite sources
|
||||
|
||||
**Low impact**
|
||||
- version bump no longer points at version release in its output
|
||||
@@ -152,6 +153,46 @@ concern - readable here, never shipped as something to parse.
|
||||
- wiki-ingest and wiki-manage call xref add with --rel, not the --rel-a/--rel-b removed in 4.0.0
|
||||
<!-- /wikitool:bumps -->
|
||||
|
||||
### Comparison and source pages accept the sources: that cite add writes; sources may cite sources
|
||||
|
||||
`cite add` writes the cited source into the page's `sources:`, but neither the `comparison` nor
|
||||
the `source` type declared that field, and both schemas set `additionalProperties: false`. So a
|
||||
comparison page cited the way `kb/comparisons/COLLECTION.md` asks for, or a source page citing one
|
||||
of its own raw files with `--file`, failed `lint` with `'sources' was unexpected`. Both types now
|
||||
declare `sources` as an optional array, in the schema and in `page_ref_fields:`. The second part
|
||||
matters: `rename`/`rm` carry only declared reference fields, and `lint` resolves only declared
|
||||
fields. A fix to the schema alone would leave dead `sources:` entries behind after a source is
|
||||
renamed, and nothing would report them.
|
||||
|
||||
That also makes a citation between two sources legal: `cite add --page "Source - A" --source
|
||||
"Source - B"` records `Source - B` on A and leaves B alone. Two changes keep that edge directed:
|
||||
|
||||
- `cite add` no longer writes a page's own title into its `sources:`. A source page citing its own
|
||||
raw file gets the footnote definition and leaves `sources:` untouched, not even as an empty
|
||||
list. `lint` and `citing_pages()` already ignored the self-edge.
|
||||
- `xref link-source` refuses a target that is itself a source page. It writes nothing for that
|
||||
target, names it with the `cite add` command to use instead, links the others and exits 1. It
|
||||
used to write `B.sources += A`, claiming the citation in the wrong direction, onto a page whose
|
||||
schema then rejected it.
|
||||
|
||||
`kb/CONTRACT.md` § Provenance and citation and `wiki-ingest` step 9 describe both.
|
||||
|
||||
**What an existing instance has to do by hand.** The schema and type-spec of `comparison` and
|
||||
`source` belong to the instance once adopted. `dist upgrade` only renews the `.template` beside
|
||||
them, so the type half of this change does not arrive on its own. For each of the two types:
|
||||
|
||||
1. Copy the `sources` property from `types/<type>.schema.yaml.template` into
|
||||
`types/<type>.schema.yaml`.
|
||||
2. Add `sources` to `page_ref_fields:` in `types/<type>.md`. Without it a rename leaves the
|
||||
entries pointing at the old title.
|
||||
3. Copy the `sources` row of the frontmatter table into the same file.
|
||||
|
||||
An instance that has already loosened only the schema still needs step 2. The `cite add` and
|
||||
`link-source` changes are under `tools/` and arrive with the normal upgrade. Until the steps
|
||||
are done, `lint` keeps reporting a cited comparison or source page as a schema error, exactly as
|
||||
before. Nothing that worked before breaks. Found while writing the first comparison page of an
|
||||
instance on 7.0.0 (Gitea #173).
|
||||
|
||||
### wiki-ingest and wiki-manage call xref add with --rel, not the --rel-a/--rel-b removed in 4.0.0
|
||||
|
||||
Since 4.0.0 `xref add` declares one directed edge and takes a single `--rel`, but the
|
||||
|
||||
@@ -351,6 +351,10 @@ validator complains - and the ticked list is the only record that they happened.
|
||||
call of its own, is `kb/CONTRACT.md` § Linking. The second links the new source to everything
|
||||
it backs in one pass.
|
||||
|
||||
An older source page the new one cites does not go into `--entities`: `link-source` refuses
|
||||
it. Record it as a citation instead, `tools/wikitool cite add --page "Source - <Title>"
|
||||
--source "<older source>"` - `kb/CONTRACT.md` § Provenance and citation has the rule.
|
||||
|
||||
10. **Check coverage.**
|
||||
|
||||
```bash
|
||||
|
||||
+11
-4
@@ -300,7 +300,8 @@ Every claim is either traceable to a raw file or explicitly marked as not.
|
||||
[--file <qualifier>]` mints the id, upserts its `[[Source - X]]` (or
|
||||
`[[Source - X|storage-model.md]]` for a multi-file source) definition in the page's trailing
|
||||
Footnotes block (named per [Section headings](#section-headings)), and adds `Source - X` to
|
||||
`sources:` - it prints the marker to paste at
|
||||
`sources:` - unless the page *is* `Source - X`, citing one of its own raw files, since a page
|
||||
never lists its own title there. It prints the marker to paste at
|
||||
the fact; placing it is still manual. Never hand-type a cite-id (AGENTS.md invariant 1). This
|
||||
differs from a plain `[[Source - X]]` link, which only means "related to".
|
||||
- **Notation inside code is notation, not a reference.** A `[^cite-id]` or a `[[wikilink]]`
|
||||
@@ -309,13 +310,19 @@ Every claim is either traceable to a raw file or explicitly marked as not.
|
||||
means a marker appended to a line *inside* a fence cites nothing - put it on a source line
|
||||
under the block (`<source-word>: [^cite-id]`, in the KB language), where it renders as a
|
||||
footnote instead of travelling with the command when someone copies it.
|
||||
- A source cited inline must also appear in the page's frontmatter `sources:` list;
|
||||
`wikitool lint` checks this in both directions, and hard-errors on a leftover pre-migration
|
||||
- **A source page may cite another source page**, the same way any page does: `cite add --page
|
||||
"Source - A" --source "Source - B"` writes `Source - B` into A's `sources:` and leaves B
|
||||
untouched. A citation between two sources has a direction, and `cite add` is the only command
|
||||
that records it - `xref link-source` refuses a target that is itself a source page.
|
||||
- A source cited inline must also appear in the page's frontmatter `sources:` list - except a
|
||||
source page's citation of itself, which never does; `wikitool lint` checks this in both
|
||||
directions, and hard-errors on a leftover pre-migration
|
||||
`^[[...]]` marker, an undefined `[^cite-id]` reference, or an orphaned Footnotes definition.
|
||||
`tools/wikitool cite sync` reconciles a page's block after a prose edit changes which ids are
|
||||
actually referenced.
|
||||
- `tools/wikitool xref link-source --source "Source - X" --entities A,B,C` adds a new source
|
||||
to every page it backs in one pass.
|
||||
to every page it backs in one pass. Its targets are the pages the source *mentions*, never
|
||||
another source page.
|
||||
- Every raw file is expected to be claimed by some source page;
|
||||
`tools/wikitool sources coverage` lists the ones that are not.
|
||||
|
||||
|
||||
+5
-1
@@ -755,12 +755,14 @@ Batch-link a source page to every entity/concept it mentions.
|
||||
- 0 success
|
||||
- 1 Source page not found
|
||||
- 1 A page in `--entities` does not exist, or a target could not be written; the others were linked
|
||||
- 1 A page in `--entities` is itself a source page; the others were linked
|
||||
- 1 The source page itself could not be written after its targets were
|
||||
|
||||
**ON FAILURE**
|
||||
|
||||
- Source page not found -> Fix the title and retry once
|
||||
- A page in `--entities` does not exist, or a target could not be written; the others were linked -> Fix the named pages and re-run - safe, since every write is idempotent. `sources trace --page "<Title>"` shows who is already linked
|
||||
- A page in `--entities` is itself a source page; the others were linked -> Drop it from `--entities` and record the citation with `cite add --page "<citing source>" --source "<cited source>"` instead
|
||||
- The source page itself could not be written after its targets were -> Fix the write failure and re-run (idempotent)
|
||||
|
||||
**NOTES**
|
||||
@@ -769,6 +771,7 @@ Batch-link a source page to every entity/concept it mentions.
|
||||
- Which field a target lands in follows its collection (`kb/entities/` -> `entities:`), so a new collection needs no code change.
|
||||
- A target whose collection matches no reference field the source type declares is linked one way and named in the output.
|
||||
- A target that does not exist is skipped and named; the others are still linked, and the run then exits 1.
|
||||
- A target that is itself a source page is refused, named with the `cite add` command to use instead, and handled like a missing page: nothing is written for it, the others are still linked, and the run exits 1. `link-source` means "A mentions X" and writes `X.sources += A`, which for X = another source would claim the citation in the wrong direction; a citation between two sources has a direction only `cite add` records.
|
||||
- Idempotent in both directions. `--dry-run` reports what would be linked.
|
||||
|
||||
**SEE ALSO**
|
||||
@@ -897,7 +900,8 @@ Upsert a `[^cite-id]: [[Source - X]]` definition in a page's footnotes region.
|
||||
|
||||
- Upserts a `[^cite-id]: [[Source - X]]` definition in the page's generated footnotes region, creating the region between `<!-- wikitool:footnotes -->` markers if absent.
|
||||
- Reuses the id when the page already cites this exact source/file pair, so a repeat changes nothing.
|
||||
- Adds `Source - X` to the page's frontmatter `sources:`.
|
||||
- Adds `Source - X` to the page's frontmatter `sources:` - except on `Source - X` itself: a source page citing one of its own raw files (`--file`) gets the definition and leaves `sources:` untouched, since a page never lists its own title there.
|
||||
- A source page may cite another source page this way - `cite add` is the command that records a citation between two sources, in the direction it was made.
|
||||
- Prints the `[^cite-id]` marker; pasting it into the prose is a manual, editorial step.
|
||||
- `--dry-run` reports without writing.
|
||||
|
||||
|
||||
@@ -88,7 +88,12 @@ def upsert_citation(page: Page, source_title: str, qualifier: Optional[str]) ->
|
||||
"""Ensure `page` has a Footnotes definition for (source_title, qualifier)
|
||||
and that source_title is in its frontmatter `sources:`. Returns
|
||||
(cite_id_to_use, new_body, changed) - reuses an existing definition for
|
||||
the same pair instead of minting a duplicate id."""
|
||||
the same pair instead of minting a duplicate id.
|
||||
|
||||
A source page citing itself (one of its own raw files, via `--file`) gets
|
||||
the definition but no `sources:` entry - not even an empty list. An edge
|
||||
to itself carries nothing, and `lint` and `provenance.citing_pages()`
|
||||
already ignore it."""
|
||||
head, definitions = split_cite_block(page.body)
|
||||
|
||||
existing_id = next(
|
||||
@@ -103,10 +108,12 @@ def upsert_citation(page: Page, source_title: str, qualifier: Optional[str]) ->
|
||||
definitions[marker_id] = (source_title, qualifier)
|
||||
block_changed = True
|
||||
|
||||
sources = page.frontmatter.setdefault("sources", [])
|
||||
sources_changed = source_title not in sources
|
||||
if sources_changed:
|
||||
sources.append(source_title)
|
||||
sources_changed = False
|
||||
if source_title != page.title:
|
||||
sources = page.frontmatter.setdefault("sources", [])
|
||||
sources_changed = source_title not in sources
|
||||
if sources_changed:
|
||||
sources.append(source_title)
|
||||
|
||||
new_body = render_page_body(head, definitions)
|
||||
changed = block_changed or sources_changed or new_body != page.body
|
||||
@@ -131,7 +138,11 @@ def upsert_citation(page: Page, source_title: str, qualifier: Optional[str]) ->
|
||||
"region, creating the region between `<!-- wikitool:footnotes -->` markers if absent.",
|
||||
"Reuses the id when the page already cites this exact source/file pair, so a repeat "
|
||||
"changes nothing.",
|
||||
"Adds `Source - X` to the page's frontmatter `sources:`.",
|
||||
"Adds `Source - X` to the page's frontmatter `sources:` - except on `Source - X` "
|
||||
"itself: a source page citing one of its own raw files (`--file`) gets the definition "
|
||||
"and leaves `sources:` untouched, since a page never lists its own title there.",
|
||||
"A source page may cite another source page this way - `cite add` is the command that "
|
||||
"records a citation between two sources, in the direction it was made.",
|
||||
"Prints the `[^cite-id]` marker; pasting it into the prose is a manual, editorial "
|
||||
"step.",
|
||||
"`--dry-run` reports without writing.",
|
||||
@@ -168,7 +179,7 @@ def cite_add(
|
||||
):
|
||||
"""Upsert a Footnotes definition for `--source` (reusing it if the page
|
||||
already cites the same source/file pair) and ensure `--source` is in the
|
||||
page's frontmatter `sources:`. Prints the `[^cite-id]` marker to paste
|
||||
page's frontmatter `sources:` (unless the page is `--source` itself). Prints the `[^cite-id]` marker to paste
|
||||
into the prose - placing it is still the caller's job.
|
||||
"""
|
||||
pages = load_kb_pages(config.KB_DIR)
|
||||
|
||||
@@ -389,6 +389,11 @@ def xref_remove(
|
||||
"linked one way and named in the output.",
|
||||
"A target that does not exist is skipped and named; the others are still linked, and "
|
||||
"the run then exits 1.",
|
||||
"A target that is itself a source page is refused, named with the `cite add` command to "
|
||||
"use instead, and handled like a missing page: nothing is written for it, the others "
|
||||
"are still linked, and the run exits 1. `link-source` means \"A mentions X\" and writes "
|
||||
"`X.sources += A`, which for X = another source would claim the citation in the wrong "
|
||||
"direction; a citation between two sources has a direction only `cite add` records.",
|
||||
"Idempotent in both directions. `--dry-run` reports what would be linked.",
|
||||
),
|
||||
failures=(
|
||||
@@ -402,6 +407,11 @@ def xref_remove(
|
||||
reaction="Fix the named pages and re-run - safe, since every write is idempotent. "
|
||||
"`sources trace --page \"<Title>\"` shows who is already linked",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A page in `--entities` is itself a source page; the others were linked",
|
||||
reaction="Drop it from `--entities` and record the citation with "
|
||||
"`cite add --page \"<citing source>\" --source \"<cited source>\"` instead",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="The source page itself could not be written after its targets were",
|
||||
reaction="Fix the write failure and re-run (idempotent)",
|
||||
@@ -431,6 +441,7 @@ def xref_link_source(
|
||||
|
||||
linked: list[str] = []
|
||||
skipped: list[str] = []
|
||||
refused: list[str] = []
|
||||
failed: list[str] = []
|
||||
unrouted: list[str] = []
|
||||
source_changed = False
|
||||
@@ -439,6 +450,13 @@ def xref_link_source(
|
||||
if page is None:
|
||||
skipped.append(name)
|
||||
continue
|
||||
if page.kind == "source":
|
||||
# Refused rather than linked: this would write `name.sources +=
|
||||
# source` ("name cites source" - the wrong direction), and the
|
||||
# back-reference would add `source.sources += name` as well. Which
|
||||
# of two sources cites the other is only said by `cite add`.
|
||||
refused.append(name)
|
||||
continue
|
||||
sources = page.frontmatter.setdefault("sources", [])
|
||||
if source not in sources:
|
||||
sources.append(source)
|
||||
@@ -489,6 +507,12 @@ def xref_link_source(
|
||||
typer.echo(f"{verb} source '{source}' to: {', '.join(linked)}")
|
||||
if skipped:
|
||||
typer.echo(f"Skipped (page not found): {', '.join(skipped)}")
|
||||
for name in refused:
|
||||
typer.echo(
|
||||
f"Refused (a source page - record a citation between sources with "
|
||||
f"`wikitool cite add --page \"{source}\" --source \"{name}\"` if '{source}' cites it, "
|
||||
f"or the other way round): {name}"
|
||||
)
|
||||
if failed:
|
||||
typer.echo(f"Failed to write (fix and re-run for just these names): {', '.join(failed)}")
|
||||
|
||||
@@ -496,10 +520,12 @@ def xref_link_source(
|
||||
typer.echo("No files written (--dry-run).")
|
||||
return
|
||||
|
||||
if skipped or failed:
|
||||
if skipped or refused or failed:
|
||||
parts = []
|
||||
if skipped:
|
||||
parts.append(f"page(s) not found: {', '.join(skipped)}")
|
||||
if refused:
|
||||
parts.append(f"source page(s) refused as targets: {', '.join(refused)}")
|
||||
if failed:
|
||||
parts.append(f"page(s) failed to write: {', '.join(failed)}")
|
||||
fail(f"Linked {len(linked)}/{len(names)} page(s); " + "; ".join(parts))
|
||||
|
||||
@@ -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 ------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -4,7 +4,7 @@ name: comparison
|
||||
description: Structured type for comparison pages that set several entities or approaches against one another
|
||||
schema: types/comparison.schema.yaml
|
||||
base_dir: comparisons
|
||||
page_ref_fields: [entities, related]
|
||||
page_ref_fields: [entities, related, sources]
|
||||
guidance: types/comparison.guidance.md
|
||||
---
|
||||
|
||||
@@ -24,6 +24,7 @@ and how to write a conforming page is [types/comparison.guidance.md](comparison.
|
||||
| `created` | Yes | Creation date (YYYY-MM-DD) |
|
||||
| `entities` | Yes | Titles of the entities compared |
|
||||
| `related` | No | Declared outbound edges - one `compares-with` edge per subject, written by `wikitool xref add` |
|
||||
| `sources` | No | Titles of the source pages cited on this page, written by `wikitool cite add` |
|
||||
| `summary` | Yes | One-liner for `kb/index.md` |
|
||||
|
||||
## Template
|
||||
|
||||
@@ -38,6 +38,13 @@ properties:
|
||||
provenance-style list, so without this one the edge the page exists to
|
||||
make would live only in hand-written prose - which is the thing
|
||||
instructions/link-taxonomy.md was built to end.
|
||||
sources:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: >-
|
||||
Source page titles cited on this page, written by `wikitool cite add`.
|
||||
Optional, so a comparison that cites nothing keeps validating without it.
|
||||
summary:
|
||||
type: string
|
||||
description: 1-line summary for index.md
|
||||
|
||||
+2
-1
@@ -6,7 +6,7 @@ schema: types/source.schema.yaml
|
||||
subtype_field: source_type
|
||||
base_dir: sources
|
||||
title_prefix: "Source - "
|
||||
page_ref_fields: [entities, concepts]
|
||||
page_ref_fields: [entities, concepts, sources]
|
||||
capture_fields: [fidelity, authority]
|
||||
guidance: types/source.guidance.md
|
||||
layout:
|
||||
@@ -42,6 +42,7 @@ how to write a conforming page is [types/source.guidance.md](source.guidance.md)
|
||||
| `tags` | No | Navigation tags for categorization |
|
||||
| `entities` | No | Titles of the entities mentioned in this source |
|
||||
| `concepts` | No | Titles of the concepts mentioned in this source |
|
||||
| `sources` | No | Titles of *other* source pages this source cites, written by `wikitool cite add` - never its own title |
|
||||
| `summary` | Yes | One-liner for `kb/index.md` |
|
||||
|
||||
## Template
|
||||
|
||||
@@ -72,6 +72,15 @@ properties:
|
||||
items:
|
||||
type: string
|
||||
description: Concept titles mentioned in this source
|
||||
sources:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: >-
|
||||
Other source pages this source cites, written by `wikitool cite add`.
|
||||
Never this page's own title - a citation of one of its own raw files
|
||||
leaves the field untouched. Optional, so a source that cites no other
|
||||
source keeps validating without it.
|
||||
summary:
|
||||
type: string
|
||||
description: 1-line summary for index.md
|
||||
|
||||
Reference in new issue
Block a user