tools: command records, Links and citations group - one line per cause, examples, prohibitions (#142)
Files changed: - CHANGES.md - VERSION - tools/CONTRACT.md - tools/chemenu/commands/cite_cmd.py - tools/chemenu/commands/links_cmd.py - tools/chemenu/commands/xref.py
This commit is contained in:
1 parent
d0a740acc9
commit
a1f3c47e62
6 files changed
+360
-91
No files matched your search
+12
-1
@@ -59,7 +59,7 @@ concern - readable here, never shipped as something to parse.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7.1.0-beta.10 - 2026-09-26 - Command records, Pages group: one line per cause, examples, prohibitions
|
## 7.1.0-beta.11 - 2026-09-26 - Command records, Links and citations group: one line per cause, examples, prohibitions
|
||||||
|
|
||||||
**Author:** Torben Nehmer
|
**Author:** Torben Nehmer
|
||||||
|
|
||||||
@@ -79,6 +79,7 @@ concern - readable here, never shipped as something to parse.
|
|||||||
- Command records, Catalog and log group: bullets, examples, prohibitions
|
- Command records, Catalog and log group: bullets, examples, prohibitions
|
||||||
- Command records, Distribution and versioning group: one line per cause, examples, prohibitions
|
- Command records, Distribution and versioning group: one line per cause, examples, prohibitions
|
||||||
- Command records, Pages group: one line per cause, examples, prohibitions
|
- Command records, Pages group: one line per cause, examples, prohibitions
|
||||||
|
- Command records, Links and citations group: one line per cause, examples, prohibitions
|
||||||
<!-- /wikitool:bumps -->
|
<!-- /wikitool:bumps -->
|
||||||
|
|
||||||
### CalDAV task-tracker provider (Nextcloud Tasks, iOS Reminders); review reports unknown-value findings instead of skipping them
|
### CalDAV task-tracker provider (Nextcloud Tasks, iOS Reminders); review reports unknown-value findings instead of skipping them
|
||||||
@@ -239,6 +240,16 @@ its page-write failure after the tracker project was confirmed, and the partial-
|
|||||||
42, and behind `task close` taking an id rather than a title, moved into `task_cmd.py`'s module
|
42, and behind `task close` taking an id rather than a title, moved into `task_cmd.py`'s module
|
||||||
docstring.
|
docstring.
|
||||||
|
|
||||||
|
### Command records, Links and citations group: one line per cause, examples, prohibitions
|
||||||
|
|
||||||
|
`xref add`/`remove`/`link-source`, `links show` and `cite id`/`add`/`sync` rewritten the same
|
||||||
|
way; text only. `xref add`'s label refusal, previously described only in NOTES, is now an exit
|
||||||
|
line of its own, and its exit text says which page's type is checked for `related:` (A's - the
|
||||||
|
only one the code looks at). `xref link-source` states that a missing target is skipped while the
|
||||||
|
rest are still linked, before the run exits 1, and loses the history of the See Also bullet it
|
||||||
|
no longer writes. The two `cite` write commands and `cite id` carry AGENTS.md invariant 1's rule
|
||||||
|
on citation ids as NEVER, where an agent looking up one command finds it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7.0.0 - 2026-09-22 - Task-Tracker-Anbindung: Vorhaben als Seitenart, Verpflichtungsschicht, Weekly Review als Read-Time-Join
|
## 7.0.0 - 2026-09-22 - Task-Tracker-Anbindung: Vorhaben als Seitenart, Verpflichtungsschicht, Weekly Review als Read-Time-Join
|
||||||
|
|||||||
+140
-16
@@ -613,18 +613,44 @@ Declare that A <rel> B.
|
|||||||
- budget: counted
|
- budget: counted
|
||||||
- network: no
|
- network: no
|
||||||
|
|
||||||
|
**EXAMPLES**
|
||||||
|
|
||||||
|
- `tools/wikitool xref add --a "Gitea Actions" --b "Act Runner" --rel depends-on`
|
||||||
|
- `tools/wikitool xref add --a "Gitea Actions" --b "Act Runner" --rel depends-on --dry-run`
|
||||||
|
|
||||||
**EXIT STATUS**
|
**EXIT STATUS**
|
||||||
|
|
||||||
- 0 success
|
- 0 success
|
||||||
- 1 Page A or B not found, or a page's type declares no `related:` field
|
- 1 Page A or B not found
|
||||||
|
- 1 A's type declares no `related:` field
|
||||||
|
- 1 `<label>` is not authorised for this collection pair, or the source collection authorises no labels into the target's collection at all
|
||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Page A or B not found, or a page's type declares no `related:` field -> Safe to retry once as-is; re-running never duplicates a link. Never create the missing page just to force the link through, and never hand-write a reference field the type does not declare
|
- Page A or B not found -> Fix the title and retry once - re-running never duplicates a link
|
||||||
|
- A's type declares no `related:` field -> For a source page use `xref link-source`; otherwise there is nothing to link from this page
|
||||||
|
- `<label>` is not authorised for this collection pair, or the source collection authorises no labels into the target's collection at all -> Pick a label from the authorised set the refusal lists, then retry once
|
||||||
|
|
||||||
|
**NEVER**
|
||||||
|
|
||||||
|
- Never create the missing page just to force the link through.
|
||||||
|
- Never hand-write a reference field the type does not declare.
|
||||||
|
- Never edit a collection's `outbound:` block just to get past a label refusal - authorising a further label is a deliberate edit of its own.
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
Declare **one** edge: `A <label> B`, written into A's `related:` as `- <label>: B` and rendered into A's generated links region. B is not touched and does not point back - its inbound view is rendered from the graph. Idempotent, and re-running with a different label *relabels* rather than appending, since one page asserts one thing about another. Refuses before writing when the type does not declare `related:` (a source page declares `entities:`/`concepts:` - the refusal names them and points at `link-source`), and when `<label>` is not authorised by the source collection's `outbound:` block for the target's collection; that refusal lists the authorised set and points at `instructions/link-taxonomy.md`
|
- Declares **one** edge, `A <label> B`: written into A's `related:` as `- <label>: B` and rendered into A's generated links region.
|
||||||
|
- B is not touched and does not point back - its inbound view is rendered from the graph.
|
||||||
|
- Idempotent; re-running with a different label *relabels* rather than appending, since one page asserts one thing about another.
|
||||||
|
- Refuses before writing when A's type does not declare `related:` - a source page declares `entities:`/`concepts:` instead, and the refusal names them and points at `xref link-source`.
|
||||||
|
- Refuses before writing when `<label>` is not authorised by the source collection's `outbound:` block for the target's collection; the refusal lists the authorised set and points at `instructions/link-taxonomy.md`.
|
||||||
|
- `--dry-run` reports the edge without writing.
|
||||||
|
|
||||||
|
**SEE ALSO**
|
||||||
|
|
||||||
|
- `wikitool xref remove` - removes a reference
|
||||||
|
- `wikitool links show` - the edges out of and into a page
|
||||||
|
- `instructions/link-taxonomy.md` - the labels and what each asserts
|
||||||
|
|
||||||
#### `xref remove`
|
#### `xref remove`
|
||||||
|
|
||||||
@@ -642,6 +668,11 @@ Remove a cross-reference: the inverse of `xref add`.
|
|||||||
- budget: counted
|
- budget: counted
|
||||||
- network: no
|
- network: no
|
||||||
|
|
||||||
|
**EXAMPLES**
|
||||||
|
|
||||||
|
- `tools/wikitool xref remove --a "Gitea Actions" --b "act_runner"`
|
||||||
|
- `tools/wikitool xref remove --a "Gitea Actions" --b "Act Runner" --dry-run`
|
||||||
|
|
||||||
**EXIT STATUS**
|
**EXIT STATUS**
|
||||||
|
|
||||||
- 0 success
|
- 0 success
|
||||||
@@ -649,11 +680,24 @@ Remove a cross-reference: the inverse of `xref add`.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Page A not found (B is allowed not to exist) -> Safe to retry freely; removing an absent link is a no-op
|
- Page A not found (B is allowed not to exist) -> Fix the title; safe to retry freely
|
||||||
|
|
||||||
|
**NEVER**
|
||||||
|
|
||||||
|
- Never hand-edit a page-ref array to remove a reference.
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
Clears the reference in **both** directions - it is the cleanup command for a deleted or hand-renamed page rather than the strict inverse of a one-directional `add`. Clears `<B>` from every page-ref frontmatter field `<A>`'s type declares (`related:`, `sources:`, `entities:`, `concepts:`) plus the matching bullets. It also sweeps a field the type does *not* declare but some other type does, and drops that key outright once empty - a leftover written before the check above existed has to stay repairable, or the page is a dead end. `--b` need not still exist as a page, so this is how a reference left by a hand-deleted or hand-renamed page gets cleared without hand-editing frontmatter. Idempotent.
|
- Clears the reference in **both** directions: the cleanup command for a deleted or hand-renamed page rather than the strict inverse of a one-directional `xref add`.
|
||||||
|
- Clears `<B>` from every page-ref frontmatter field `<A>`'s type declares (`related:`, `sources:`, `entities:`, `concepts:`) plus the matching bullets.
|
||||||
|
- Also sweeps a page-ref field the type does *not* declare but some other type does, and drops that key once empty.
|
||||||
|
- `--b` need not still exist as a page, so this clears a reference left by a hand-deleted or hand-renamed page without hand-editing frontmatter.
|
||||||
|
- Idempotent: removing an absent link is a no-op. `--dry-run` reports without writing.
|
||||||
|
|
||||||
|
**SEE ALSO**
|
||||||
|
|
||||||
|
- `wikitool xref add` - declares an edge
|
||||||
|
- `wikitool rm` - deletes a page and de-links it
|
||||||
|
|
||||||
#### `xref link-source`
|
#### `xref link-source`
|
||||||
|
|
||||||
@@ -671,18 +715,37 @@ Batch-link a source page to every entity/concept it mentions.
|
|||||||
- budget: counted
|
- budget: counted
|
||||||
- network: no
|
- network: no
|
||||||
|
|
||||||
|
**EXAMPLES**
|
||||||
|
|
||||||
|
- `tools/wikitool xref link-source --source "Source - Docker Cheatsheet" --entities Docker,Podman --dry-run`
|
||||||
|
- `tools/wikitool xref link-source --source "Source - Docker Cheatsheet" --entities Docker,Podman`
|
||||||
|
|
||||||
**EXIT STATUS**
|
**EXIT STATUS**
|
||||||
|
|
||||||
- 0 success
|
- 0 success
|
||||||
- 1 Source page not found, an entity in `--entities` doesn't exist, or the source page itself could not be written after its targets were
|
- 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 The source page itself could not be written after its targets were
|
||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Source page not found, an entity in `--entities` doesn't exist, or the source page itself could not be written after its targets were -> Use `--dry-run` first; safe to retry. `sources trace --page "<Title>"` shows who was already linked
|
- 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
|
||||||
|
- The source page itself could not be written after its targets were -> Fix the write failure and re-run (idempotent)
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
Each target gets `sources:`, and the source page records each target in its own `entities:`/`concepts:`. No body bullet is written on either side - `sources:` *is* the record, and the See Also bullet this used to add was the reciprocal half of a model that no longer exists. Which of the two is chosen follows the target's collection (`kb/entities/` -> `entities:`), so a new collection needs no code change here. A target whose collection matches no reference field the source type declares is linked one-way and named in the output. Idempotent in both directions
|
- Each target gets the source page in its `sources:`, and the source page records each target in its own `entities:` or `concepts:`. No body bullet is written on either side - `sources:` *is* the record.
|
||||||
|
- 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.
|
||||||
|
- Idempotent in both directions. `--dry-run` reports what would be linked.
|
||||||
|
|
||||||
|
**SEE ALSO**
|
||||||
|
|
||||||
|
- `wikitool sources trace` - who a source is already linked to
|
||||||
|
- `wikitool xref add` - one labelled edge between two pages
|
||||||
|
- `wiki-ingest` skill - where a source is linked after it is written
|
||||||
|
|
||||||
#### `links show`
|
#### `links show`
|
||||||
|
|
||||||
@@ -700,6 +763,11 @@ Show the edges out of and into a page.
|
|||||||
- budget: exempt
|
- budget: exempt
|
||||||
- network: no
|
- network: no
|
||||||
|
|
||||||
|
**EXAMPLES**
|
||||||
|
|
||||||
|
- `tools/wikitool links show --page "Act Runner"`
|
||||||
|
- `tools/wikitool links show --page "Act Runner" --json`
|
||||||
|
|
||||||
**EXIT STATUS**
|
**EXIT STATUS**
|
||||||
|
|
||||||
- 0 success
|
- 0 success
|
||||||
@@ -711,7 +779,15 @@ Show the edges out of and into a page.
|
|||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
The declared graph around one page in both directions: the edges it asserts (from its own `related:`, with labels) and the edges other pages assert about it (computed across the corpus). The inbound half is derived rather than stored - that is what makes it complete, and it is the answer authored directional edges would otherwise have nowhere to come from. Read-only, exempt from the Iteration Budget Gate
|
- Shows the declared graph around one page in both directions: the edges it asserts (from its own `related:`, with labels) and the edges other pages assert about it.
|
||||||
|
- The inbound half is computed across the corpus on every call, never stored, so it is complete.
|
||||||
|
- `--json` prints both halves as JSON.
|
||||||
|
- Read-only; exempt from the Iteration Budget Gate.
|
||||||
|
|
||||||
|
**SEE ALSO**
|
||||||
|
|
||||||
|
- `wikitool xref add` / `wikitool xref remove` - change the outbound edges
|
||||||
|
- `wikitool search` - finds the exact title
|
||||||
|
|
||||||
#### `cite id`
|
#### `cite id`
|
||||||
|
|
||||||
@@ -729,13 +805,27 @@ Print the deterministic footnote id `cite add` would use for this (title, file)
|
|||||||
- budget: exempt
|
- budget: exempt
|
||||||
- network: no
|
- network: no
|
||||||
|
|
||||||
|
**EXAMPLES**
|
||||||
|
|
||||||
|
- `tools/wikitool cite id --title "Source - Docker Cheatsheet"`
|
||||||
|
|
||||||
**EXIT STATUS**
|
**EXIT STATUS**
|
||||||
|
|
||||||
- 0 success
|
- 0 success
|
||||||
|
|
||||||
|
**NEVER**
|
||||||
|
|
||||||
|
- Never paste an id from here into a page by hand - `cite add` writes the definition and prints the marker to paste.
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
Read-only preview - does not check the id is actually free on any given page. Never fails. Safe to retry freely. Exempt from the Iteration Budget Gate
|
- Prints the footnote id `cite add` would use for this (title, file) pair.
|
||||||
|
- A preview only: it does not check that the id is free on any given page.
|
||||||
|
- Never fails; safe to retry freely. Read-only and exempt from the Iteration Budget Gate.
|
||||||
|
|
||||||
|
**SEE ALSO**
|
||||||
|
|
||||||
|
- `wikitool cite add` - writes the definition
|
||||||
|
|
||||||
#### `cite add`
|
#### `cite add`
|
||||||
|
|
||||||
@@ -753,18 +843,38 @@ Upsert a `[^cite-id]: [[Source - X]]` definition in a page's footnotes region.
|
|||||||
- budget: counted
|
- budget: counted
|
||||||
- network: no
|
- network: no
|
||||||
|
|
||||||
|
**EXAMPLES**
|
||||||
|
|
||||||
|
- `tools/wikitool cite add --page "Docker" --source "Source - Docker Cheatsheet"`
|
||||||
|
- `tools/wikitool cite add --page "Docker" --source "Source - Docker Cheatsheet" --file part-2`
|
||||||
|
|
||||||
**EXIT STATUS**
|
**EXIT STATUS**
|
||||||
|
|
||||||
- 0 success
|
- 0 success
|
||||||
- 1 Page or source not found
|
- 1 The page is not found
|
||||||
|
- 1 The source page is not found - citing it would be a dangling reference
|
||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Page or source not found -> Safe to retry; upserting the same (page, source, file) pair twice reuses the existing id and changes nothing the second time
|
- The page is not found -> Fix the title and retry once
|
||||||
|
- The source page is not found - citing it would be a dangling reference -> Fix the source title, or create the source page first, then retry once
|
||||||
|
|
||||||
|
**NEVER**
|
||||||
|
|
||||||
|
- Never compute or type a `[^cite-id]` or its definition by hand - paste the marker this prints.
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
Upsert a `[^cite-id]: [[Source - X]]` definition in the page's generated footnotes region, creating it between `<!-- wikitool:footnotes -->` markers if absent (reusing the id if the page already cites this exact source/file pair) and add `Source - X` to frontmatter `sources:`. Prints the `[^cite-id]` marker - pasting it into the prose is still a manual, editorial step
|
- 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:`.
|
||||||
|
- Prints the `[^cite-id]` marker; pasting it into the prose is a manual, editorial step.
|
||||||
|
- `--dry-run` reports without writing.
|
||||||
|
|
||||||
|
**SEE ALSO**
|
||||||
|
|
||||||
|
- `wikitool cite sync` - prunes and re-orders the region
|
||||||
|
- `wikitool cite id` - previews an id
|
||||||
|
|
||||||
#### `cite sync`
|
#### `cite sync`
|
||||||
|
|
||||||
@@ -782,18 +892,32 @@ Reconcile each page's footnotes region against its actual `[^id]` references.
|
|||||||
- budget: counted
|
- budget: counted
|
||||||
- network: no
|
- network: no
|
||||||
|
|
||||||
|
**EXAMPLES**
|
||||||
|
|
||||||
|
- `tools/wikitool cite sync --page "Docker"`
|
||||||
|
- `tools/wikitool cite sync --all --dry-run`
|
||||||
|
|
||||||
**EXIT STATUS**
|
**EXIT STATUS**
|
||||||
|
|
||||||
- 0 success
|
- 0 success
|
||||||
- 1 Neither or both of `--page`/`--all` given, or page not found
|
- 1 Neither or both of `--page`/`--all` given, or the page is not found
|
||||||
|
- 1 A page write failed partway
|
||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Neither or both of `--page`/`--all` given, or page not found -> Safe to retry freely. An undefined-reference report is not a failure - fix the reference (or run `cite add`) and re-run
|
- Neither or both of `--page`/`--all` given, or the page is not found -> Fix the arguments and retry once
|
||||||
|
- A page write failed partway -> Resolve the write failure and re-run - each page's re-render is idempotent
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
Prune definitions nothing references any more, re-render the region in first-reference order, and report any `[^id]` reference left with no definition. A page still carrying the pre-4.0.0 undelimited block is converted to a marked region in the same pass - the marker carries the region's identity now, so re-rendering it under this instance's heading is a repair rather than a rename
|
- Prunes definitions nothing references any more, re-renders the region in first-reference order, and reports every `[^id]` reference left with no definition.
|
||||||
|
- An undefined-reference report is not a failure: fix the reference, or run `cite add`, and re-run.
|
||||||
|
- A page still carrying the pre-4.0.0 undelimited footnote block is converted to a marked region in the same pass.
|
||||||
|
- Each page's re-render is idempotent; safe to retry freely. `--dry-run` reports without writing.
|
||||||
|
|
||||||
|
**SEE ALSO**
|
||||||
|
|
||||||
|
- `wikitool cite add` - writes a definition
|
||||||
|
|
||||||
### Catalog and log
|
### Catalog and log
|
||||||
|
|
||||||
|
|||||||
@@ -53,9 +53,23 @@ def _find_page(pages: dict[str, Page], title: str) -> Page:
|
|||||||
atomic="Read-only",
|
atomic="Read-only",
|
||||||
budget=cli_contract.Budget.EXEMPT,
|
budget=cli_contract.Budget.EXEMPT,
|
||||||
),
|
),
|
||||||
notes="Read-only preview - does not check the id is actually free on any given page. Never "
|
notes=(
|
||||||
"fails. Safe to retry freely. Exempt from the Iteration Budget Gate",
|
"Prints the footnote id `cite add` would use for this (title, file) pair.",
|
||||||
|
"A preview only: it does not check that the id is free on any given page.",
|
||||||
|
"Never fails; safe to retry freely. Read-only and exempt from the Iteration Budget "
|
||||||
|
"Gate.",
|
||||||
|
),
|
||||||
failures=(),
|
failures=(),
|
||||||
|
examples=(
|
||||||
|
'tools/wikitool cite id --title "Source - Docker Cheatsheet"',
|
||||||
|
),
|
||||||
|
never=(
|
||||||
|
"Never paste an id from here into a page by hand - `cite add` writes the definition and "
|
||||||
|
"prints the marker to paste.",
|
||||||
|
),
|
||||||
|
see_also=(
|
||||||
|
"`wikitool cite add` - writes the definition",
|
||||||
|
),
|
||||||
))
|
))
|
||||||
def cite_id_command(
|
def cite_id_command(
|
||||||
title: str = typer.Option(..., "--title", help="Source page title, e.g. 'Source - Docker Cheatsheet'"),
|
title: str = typer.Option(..., "--title", help="Source page title, e.g. 'Source - Docker Cheatsheet'"),
|
||||||
@@ -112,17 +126,39 @@ def upsert_citation(page: Page, source_title: str, qualifier: Optional[str]) ->
|
|||||||
atomic="Yes - single file write",
|
atomic="Yes - single file write",
|
||||||
budget=cli_contract.Budget.COUNTED,
|
budget=cli_contract.Budget.COUNTED,
|
||||||
),
|
),
|
||||||
notes="Upsert a `[^cite-id]: [[Source - X]]` definition in the page's generated footnotes "
|
notes=(
|
||||||
"region, creating it between `<!-- wikitool:footnotes -->` markers if absent (reusing the "
|
"Upserts a `[^cite-id]: [[Source - X]]` definition in the page's generated footnotes "
|
||||||
"id if the page already cites this exact source/file pair) and add `Source - X` to "
|
"region, creating the region between `<!-- wikitool:footnotes -->` markers if absent.",
|
||||||
"frontmatter `sources:`. Prints the `[^cite-id]` marker - pasting it into the prose is still "
|
"Reuses the id when the page already cites this exact source/file pair, so a repeat "
|
||||||
"a manual, editorial step",
|
"changes nothing.",
|
||||||
failures=(cli_contract.Failure(
|
"Adds `Source - X` to the page's frontmatter `sources:`.",
|
||||||
label="",
|
"Prints the `[^cite-id]` marker; pasting it into the prose is a manual, editorial "
|
||||||
cause="Page or source not found",
|
"step.",
|
||||||
reaction="Safe to retry; upserting the same (page, source, file) pair twice reuses the "
|
"`--dry-run` reports without writing.",
|
||||||
"existing id and changes nothing the second time",
|
),
|
||||||
),),
|
failures=(
|
||||||
|
cli_contract.Failure(
|
||||||
|
cause="The page is not found",
|
||||||
|
reaction="Fix the title and retry once",
|
||||||
|
),
|
||||||
|
cli_contract.Failure(
|
||||||
|
cause="The source page is not found - citing it would be a dangling reference",
|
||||||
|
reaction="Fix the source title, or create the source page first, then retry once",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
examples=(
|
||||||
|
'tools/wikitool cite add --page "Docker" --source "Source - Docker Cheatsheet"',
|
||||||
|
'tools/wikitool cite add --page "Docker" --source "Source - Docker Cheatsheet" '
|
||||||
|
"--file part-2",
|
||||||
|
),
|
||||||
|
never=(
|
||||||
|
"Never compute or type a `[^cite-id]` or its definition by hand - paste the marker this "
|
||||||
|
"prints.",
|
||||||
|
),
|
||||||
|
see_also=(
|
||||||
|
"`wikitool cite sync` - prunes and re-orders the region",
|
||||||
|
"`wikitool cite id` - previews an id",
|
||||||
|
),
|
||||||
))
|
))
|
||||||
def cite_add(
|
def cite_add(
|
||||||
page_title: str = typer.Option(..., "--page", help="Exact title of the page to add a citation on"),
|
page_title: str = typer.Option(..., "--page", help="Exact title of the page to add a citation on"),
|
||||||
@@ -166,6 +202,11 @@ def sync_page(page: Page) -> tuple[str, bool, list[str], list[str]]:
|
|||||||
the block in first-reference order. Never mints or recomputes an id from
|
the block in first-reference order. Never mints or recomputes an id from
|
||||||
a title - a reference with no definition is reported, not guessed at.
|
a title - a reference with no definition is reported, not guessed at.
|
||||||
|
|
||||||
|
A page still carrying the pre-4.0.0 undelimited block is converted to a
|
||||||
|
marked region in the same pass: the marker pair, not the heading text,
|
||||||
|
carries the region's identity now, so re-rendering it under this
|
||||||
|
instance's heading is a repair rather than a rename.
|
||||||
|
|
||||||
Returns (new_body, changed, pruned_ids, undefined_ref_ids).
|
Returns (new_body, changed, pruned_ids, undefined_ref_ids).
|
||||||
"""
|
"""
|
||||||
head, definitions = split_cite_block(page.body)
|
head, definitions = split_cite_block(page.body)
|
||||||
@@ -207,17 +248,33 @@ def sync_page(page: Page) -> tuple[str, bool, list[str], list[str]]:
|
|||||||
atomic="No - one write per page, each idempotent",
|
atomic="No - one write per page, each idempotent",
|
||||||
budget=cli_contract.Budget.COUNTED,
|
budget=cli_contract.Budget.COUNTED,
|
||||||
),
|
),
|
||||||
notes="Prune definitions nothing references any more, re-render the region in "
|
notes=(
|
||||||
"first-reference order, and report any `[^id]` reference left with no definition. A page "
|
"Prunes definitions nothing references any more, re-renders the region in "
|
||||||
"still carrying the pre-4.0.0 undelimited block is converted to a marked region in the same "
|
"first-reference order, and reports every `[^id]` reference left with no definition.",
|
||||||
"pass - the marker carries the region's identity now, so re-rendering it under this "
|
"An undefined-reference report is not a failure: fix the reference, or run `cite add`, "
|
||||||
"instance's heading is a repair rather than a rename",
|
"and re-run.",
|
||||||
failures=(cli_contract.Failure(
|
"A page still carrying the pre-4.0.0 undelimited footnote block is converted to a "
|
||||||
label="",
|
"marked region in the same pass.",
|
||||||
cause="Neither or both of `--page`/`--all` given, or page not found",
|
"Each page's re-render is idempotent; safe to retry freely. `--dry-run` reports "
|
||||||
reaction="Safe to retry freely. An undefined-reference report is not a failure - fix the "
|
"without writing.",
|
||||||
"reference (or run `cite add`) and re-run",
|
),
|
||||||
),),
|
failures=(
|
||||||
|
cli_contract.Failure(
|
||||||
|
cause="Neither or both of `--page`/`--all` given, or the page is not found",
|
||||||
|
reaction="Fix the arguments and retry once",
|
||||||
|
),
|
||||||
|
cli_contract.Failure(
|
||||||
|
cause="A page write failed partway",
|
||||||
|
reaction="Resolve the write failure and re-run - each page's re-render is idempotent",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
examples=(
|
||||||
|
'tools/wikitool cite sync --page "Docker"',
|
||||||
|
"tools/wikitool cite sync --all --dry-run",
|
||||||
|
),
|
||||||
|
see_also=(
|
||||||
|
"`wikitool cite add` - writes a definition",
|
||||||
|
),
|
||||||
))
|
))
|
||||||
def cite_sync(
|
def cite_sync(
|
||||||
page_title: Optional[str] = typer.Option(None, "--page", help="Sync just this page"),
|
page_title: Optional[str] = typer.Option(None, "--page", help="Sync just this page"),
|
||||||
|
|||||||
@@ -70,17 +70,27 @@ def inbound(pages: dict[str, Page], title: str) -> list[dict]:
|
|||||||
atomic="Read-only",
|
atomic="Read-only",
|
||||||
budget=cli_contract.Budget.EXEMPT,
|
budget=cli_contract.Budget.EXEMPT,
|
||||||
),
|
),
|
||||||
notes="The declared graph around one page in both directions: the edges it asserts (from "
|
notes=(
|
||||||
"its own `related:`, with labels) and the edges other pages assert about it (computed "
|
"Shows the declared graph around one page in both directions: the edges it asserts "
|
||||||
"across the corpus). The inbound half is derived rather than stored - that is what makes it "
|
"(from its own `related:`, with labels) and the edges other pages assert about it.",
|
||||||
"complete, and it is the answer authored directional edges would otherwise have nowhere to "
|
"The inbound half is computed across the corpus on every call, never stored, so it is "
|
||||||
"come from. Read-only, exempt from the Iteration Budget Gate",
|
"complete.",
|
||||||
|
"`--json` prints both halves as JSON.",
|
||||||
|
"Read-only; exempt from the Iteration Budget Gate.",
|
||||||
|
),
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
|
||||||
cause="Page not found",
|
cause="Page not found",
|
||||||
reaction="Check the exact title with `search`; a wikilink target is not always the page's "
|
reaction="Check the exact title with `search`; a wikilink target is not always the "
|
||||||
"stem",
|
"page's stem",
|
||||||
),),
|
),),
|
||||||
|
examples=(
|
||||||
|
'tools/wikitool links show --page "Act Runner"',
|
||||||
|
'tools/wikitool links show --page "Act Runner" --json',
|
||||||
|
),
|
||||||
|
see_also=(
|
||||||
|
"`wikitool xref add` / `wikitool xref remove` - change the outbound edges",
|
||||||
|
"`wikitool search` - finds the exact title",
|
||||||
|
),
|
||||||
))
|
))
|
||||||
def links_show(
|
def links_show(
|
||||||
page: str = typer.Option(..., "--page", help="Exact page title"),
|
page: str = typer.Option(..., "--page", help="Exact page title"),
|
||||||
|
|||||||
+108
-41
@@ -157,22 +157,52 @@ def _check_authorised(source: Page, target: Page, label: str) -> None:
|
|||||||
"before either write",
|
"before either write",
|
||||||
budget=cli_contract.Budget.COUNTED,
|
budget=cli_contract.Budget.COUNTED,
|
||||||
),
|
),
|
||||||
notes="Declare **one** edge: `A <label> B`, written into A's `related:` as `- <label>: B` "
|
notes=(
|
||||||
"and rendered into A's generated links region. B is not touched and does not point back - "
|
"Declares **one** edge, `A <label> B`: written into A's `related:` as `- <label>: B` "
|
||||||
"its inbound view is rendered from the graph. Idempotent, and re-running with a different "
|
"and rendered into A's generated links region.",
|
||||||
"label *relabels* rather than appending, since one page asserts one thing about another. "
|
"B is not touched and does not point back - its inbound view is rendered from the "
|
||||||
"Refuses before writing when the type does not declare `related:` (a source page declares "
|
"graph.",
|
||||||
"`entities:`/`concepts:` - the refusal names them and points at `link-source`), and when "
|
"Idempotent; re-running with a different label *relabels* rather than appending, since "
|
||||||
"`<label>` is not authorised by the source collection's `outbound:` block for the target's "
|
"one page asserts one thing about another.",
|
||||||
"collection; that refusal lists the authorised set and points at "
|
"Refuses before writing when A's type does not declare `related:` - a source page "
|
||||||
"`instructions/link-taxonomy.md`",
|
"declares `entities:`/`concepts:` instead, and the refusal names them and points at "
|
||||||
failures=(cli_contract.Failure(
|
"`xref link-source`.",
|
||||||
label="",
|
"Refuses before writing when `<label>` is not authorised by the source collection's "
|
||||||
cause="Page A or B not found, or a page's type declares no `related:` field",
|
"`outbound:` block for the target's collection; the refusal lists the authorised set "
|
||||||
reaction="Safe to retry once as-is; re-running never duplicates a link. Never create the "
|
"and points at `instructions/link-taxonomy.md`.",
|
||||||
"missing page just to force the link through, and never hand-write a reference field "
|
"`--dry-run` reports the edge without writing.",
|
||||||
"the type does not declare",
|
),
|
||||||
),),
|
failures=(
|
||||||
|
cli_contract.Failure(
|
||||||
|
cause="Page A or B not found",
|
||||||
|
reaction="Fix the title and retry once - re-running never duplicates a link",
|
||||||
|
),
|
||||||
|
cli_contract.Failure(
|
||||||
|
cause="A's type declares no `related:` field",
|
||||||
|
reaction="For a source page use `xref link-source`; otherwise there is nothing to "
|
||||||
|
"link from this page",
|
||||||
|
),
|
||||||
|
cli_contract.Failure(
|
||||||
|
cause="`<label>` is not authorised for this collection pair, or the source "
|
||||||
|
"collection authorises no labels into the target's collection at all",
|
||||||
|
reaction="Pick a label from the authorised set the refusal lists, then retry once",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
examples=(
|
||||||
|
'tools/wikitool xref add --a "Gitea Actions" --b "Act Runner" --rel depends-on',
|
||||||
|
'tools/wikitool xref add --a "Gitea Actions" --b "Act Runner" --rel depends-on --dry-run',
|
||||||
|
),
|
||||||
|
never=(
|
||||||
|
"Never create the missing page just to force the link through.",
|
||||||
|
"Never hand-write a reference field the type does not declare.",
|
||||||
|
"Never edit a collection's `outbound:` block just to get past a label refusal - "
|
||||||
|
"authorising a further label is a deliberate edit of its own.",
|
||||||
|
),
|
||||||
|
see_also=(
|
||||||
|
"`wikitool xref remove` - removes a reference",
|
||||||
|
"`wikitool links show` - the edges out of and into a page",
|
||||||
|
"`instructions/link-taxonomy.md` - the labels and what each asserts",
|
||||||
|
),
|
||||||
))
|
))
|
||||||
def xref_add(
|
def xref_add(
|
||||||
a: str = typer.Option(..., "--a", help="Exact title of the page that asserts the edge"),
|
a: str = typer.Option(..., "--a", help="Exact title of the page that asserts the edge"),
|
||||||
@@ -252,20 +282,32 @@ def remove_link_bullets(body: str, other_title: str) -> str:
|
|||||||
atomic="No - writes A then B, both idempotent",
|
atomic="No - writes A then B, both idempotent",
|
||||||
budget=cli_contract.Budget.COUNTED,
|
budget=cli_contract.Budget.COUNTED,
|
||||||
),
|
),
|
||||||
notes="Clears the reference in **both** directions - it is the cleanup command for a "
|
notes=(
|
||||||
"deleted or hand-renamed page rather than the strict inverse of a one-directional `add`. "
|
"Clears the reference in **both** directions: the cleanup command for a deleted or "
|
||||||
"Clears `<B>` from every page-ref frontmatter field `<A>`'s type declares (`related:`, "
|
"hand-renamed page rather than the strict inverse of a one-directional `xref add`.",
|
||||||
"`sources:`, `entities:`, `concepts:`) plus the matching bullets. It also sweeps a field the "
|
"Clears `<B>` from every page-ref frontmatter field `<A>`'s type declares (`related:`, "
|
||||||
"type does *not* declare but some other type does, and drops that key outright once empty - "
|
"`sources:`, `entities:`, `concepts:`) plus the matching bullets.",
|
||||||
"a leftover written before the check above existed has to stay repairable, or the page is a "
|
"Also sweeps a page-ref field the type does *not* declare but some other type does, "
|
||||||
"dead end. `--b` need not still exist as a page, so this is how a reference left by a "
|
"and drops that key once empty.",
|
||||||
"hand-deleted or hand-renamed page gets cleared without hand-editing frontmatter. "
|
"`--b` need not still exist as a page, so this clears a reference left by a "
|
||||||
"Idempotent.",
|
"hand-deleted or hand-renamed page without hand-editing frontmatter.",
|
||||||
|
"Idempotent: removing an absent link is a no-op. `--dry-run` reports without writing.",
|
||||||
|
),
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
|
||||||
cause="Page A not found (B is allowed not to exist)",
|
cause="Page A not found (B is allowed not to exist)",
|
||||||
reaction="Safe to retry freely; removing an absent link is a no-op",
|
reaction="Fix the title; safe to retry freely",
|
||||||
),),
|
),),
|
||||||
|
examples=(
|
||||||
|
'tools/wikitool xref remove --a "Gitea Actions" --b "act_runner"',
|
||||||
|
'tools/wikitool xref remove --a "Gitea Actions" --b "Act Runner" --dry-run',
|
||||||
|
),
|
||||||
|
never=(
|
||||||
|
"Never hand-edit a page-ref array to remove a reference.",
|
||||||
|
),
|
||||||
|
see_also=(
|
||||||
|
"`wikitool xref add` - declares an edge",
|
||||||
|
"`wikitool rm` - deletes a page and de-links it",
|
||||||
|
),
|
||||||
))
|
))
|
||||||
def xref_remove(
|
def xref_remove(
|
||||||
a: str = typer.Option(..., "--a", help="Exact title of page A (must exist)"),
|
a: str = typer.Option(..., "--a", help="Exact title of page A (must exist)"),
|
||||||
@@ -337,20 +379,45 @@ def xref_remove(
|
|||||||
atomic="No - one write per entity plus one for the source page, idempotent per page",
|
atomic="No - one write per entity plus one for the source page, idempotent per page",
|
||||||
budget=cli_contract.Budget.COUNTED,
|
budget=cli_contract.Budget.COUNTED,
|
||||||
),
|
),
|
||||||
notes="Each target gets `sources:`, and the source page records each target in its own "
|
notes=(
|
||||||
"`entities:`/`concepts:`. No body bullet is written on either side - `sources:` *is* the "
|
"Each target gets the source page in its `sources:`, and the source page records each "
|
||||||
"record, and the See Also bullet this used to add was the reciprocal half of a model that "
|
"target in its own `entities:` or `concepts:`. No body bullet is written on either side "
|
||||||
"no longer exists. Which of the two is chosen follows the target's collection "
|
"- `sources:` *is* the record.",
|
||||||
"(`kb/entities/` -> `entities:`), so a new collection needs no code change here. A target "
|
"Which field a target lands in follows its collection (`kb/entities/` -> "
|
||||||
"whose collection matches no reference field the source type declares is linked one-way "
|
"`entities:`), so a new collection needs no code change.",
|
||||||
"and named in the output. Idempotent in both directions",
|
"A target whose collection matches no reference field the source type declares is "
|
||||||
failures=(cli_contract.Failure(
|
"linked one way and named in the output.",
|
||||||
label="",
|
"A target that does not exist is skipped and named; the others are still linked, and "
|
||||||
cause="Source page not found, an entity in `--entities` doesn't exist, or the source "
|
"the run then exits 1.",
|
||||||
"page itself could not be written after its targets were",
|
"Idempotent in both directions. `--dry-run` reports what would be linked.",
|
||||||
reaction="Use `--dry-run` first; safe to retry. `sources trace --page \"<Title>\"` shows "
|
),
|
||||||
"who was already linked",
|
failures=(
|
||||||
),),
|
cli_contract.Failure(
|
||||||
|
cause="Source page not found",
|
||||||
|
reaction="Fix the title and retry once",
|
||||||
|
),
|
||||||
|
cli_contract.Failure(
|
||||||
|
cause="A page in `--entities` does not exist, or a target could not be written; the "
|
||||||
|
"others were linked",
|
||||||
|
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="The source page itself could not be written after its targets were",
|
||||||
|
reaction="Fix the write failure and re-run (idempotent)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
examples=(
|
||||||
|
'tools/wikitool xref link-source --source "Source - Docker Cheatsheet" '
|
||||||
|
"--entities Docker,Podman --dry-run",
|
||||||
|
'tools/wikitool xref link-source --source "Source - Docker Cheatsheet" '
|
||||||
|
"--entities Docker,Podman",
|
||||||
|
),
|
||||||
|
see_also=(
|
||||||
|
"`wikitool sources trace` - who a source is already linked to",
|
||||||
|
"`wikitool xref add` - one labelled edge between two pages",
|
||||||
|
"`wiki-ingest` skill - where a source is linked after it is written",
|
||||||
|
),
|
||||||
))
|
))
|
||||||
def xref_link_source(
|
def xref_link_source(
|
||||||
source: str = typer.Option(..., "--source", help="Exact source page title, e.g. 'Source - Docker Cheatsheet'"),
|
source: str = typer.Option(..., "--source", help="Exact source page title, e.g. 'Source - Docker Cheatsheet'"),
|
||||||
|
|||||||
Reference in new issue
Block a user