diff --git a/CHANGES.md b/CHANGES.md index e31f33b..e83ccad 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 +### 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/.schema.yaml.template` into + `types/.schema.yaml`. +2. Add `sources` to `page_ref_fields:` in `types/.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 diff --git a/VERSION b/VERSION index 54e2684..6afa336 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.0.0-beta.39 +8.0.0-beta.40 diff --git a/instructions/wiki-ingest/SKILL.md b/instructions/wiki-ingest/SKILL.md index 0f11928..292d5ba 100644 --- a/instructions/wiki-ingest/SKILL.md +++ b/instructions/wiki-ingest/SKILL.md @@ -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 - " + --source "<older source>"` - `kb/CONTRACT.md` § Provenance and citation has the rule. + 10. **Check coverage.** ```bash diff --git a/kb/CONTRACT.md b/kb/CONTRACT.md index 818b23a..3028003 100644 --- a/kb/CONTRACT.md +++ b/kb/CONTRACT.md @@ -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. diff --git a/tools/CONTRACT.md b/tools/CONTRACT.md index 945e53f..9c274d0 100644 --- a/tools/CONTRACT.md +++ b/tools/CONTRACT.md @@ -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. diff --git a/tools/chemenu/commands/cite_cmd.py b/tools/chemenu/commands/cite_cmd.py index 556ed0a..7894b53 100644 --- a/tools/chemenu/commands/cite_cmd.py +++ b/tools/chemenu/commands/cite_cmd.py @@ -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) diff --git a/tools/chemenu/commands/xref.py b/tools/chemenu/commands/xref.py index b24c1ee..d42c125 100644 --- a/tools/chemenu/commands/xref.py +++ b/tools/chemenu/commands/xref.py @@ -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)) diff --git a/tools/chemenu/tests/test_cite_cmd.py b/tools/chemenu/tests/test_cite_cmd.py index 465b08c..3abcc92 100644 --- a/tools/chemenu/tests/test_cite_cmd.py +++ b/tools/chemenu/tests/test_cite_cmd.py @@ -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 diff --git a/tools/chemenu/tests/test_page_ops.py b/tools/chemenu/tests/test_page_ops.py index 9e373bc..adc987f 100644 --- a/tools/chemenu/tests/test_page_ops.py +++ b/tools/chemenu/tests/test_page_ops.py @@ -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 diff --git a/tools/chemenu/tests/test_type_resolver.py b/tools/chemenu/tests/test_type_resolver.py index c23c150..42db545 100644 --- a/tools/chemenu/tests/test_type_resolver.py +++ b/tools/chemenu/tests/test_type_resolver.py @@ -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(): diff --git a/tools/chemenu/tests/test_xref.py b/tools/chemenu/tests/test_xref.py index 4dc3810..2f63817 100644 --- a/tools/chemenu/tests/test_xref.py +++ b/tools/chemenu/tests/test_xref.py @@ -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 ------------------------------------------------------ diff --git a/types/comparison.md b/types/comparison.md index bbe09cf..5b0d1f7 100644 --- a/types/comparison.md +++ b/types/comparison.md @@ -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 diff --git a/types/comparison.schema.yaml b/types/comparison.schema.yaml index b18d6fb..86a3bdd 100644 --- a/types/comparison.schema.yaml +++ b/types/comparison.schema.yaml @@ -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 diff --git a/types/source.md b/types/source.md index 54d86d8..ebd0974 100644 --- a/types/source.md +++ b/types/source.md @@ -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 diff --git a/types/source.schema.yaml b/types/source.schema.yaml index da1985a..6937aec 100644 --- a/types/source.schema.yaml +++ b/types/source.schema.yaml @@ -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