feat: wikitool move - eine kb-Seite folgt ihrem Subtype ins berechnete Verzeichnis (#56)
Files changed: - CHANGES.md - VERSION - instructions/page-lifecycle.md - instructions/publish-cycle.md - tools/CONTRACT.md - tools/chemenu/cli.py - tools/chemenu/commands/log_append.py - tools/chemenu/commands/migrate_cmd.py - tools/chemenu/commands/new_page.py - tools/chemenu/commands/page_ops.py - tools/chemenu/corpus_diff.py - tools/chemenu/lint_core.py - tools/chemenu/tests/test_corpus_diff.py - tools/chemenu/tests/test_lint.py - tools/chemenu/tests/test_migrate_cmd.py - tools/chemenu/tests/test_page_ops.py - tools/chemenu/tests/test_type_resolver.py - tools/chemenu/type_resolver.py
This commit is contained in:
+4
-2
@@ -40,6 +40,7 @@ tools/wikitool <command> --help
|
||||
| `touch --page "<Title>" [--summary "..."] [--provenance <v>] [--confidence-base <n>] [--date YYYY-MM-DD] [--set field=value ...] [--add field=value ...] [--remove field=value ...] [--no-date] [--dry-run]` | Update a page's own frontmatter: bump `modified:` and optionally rewrite any field its type declares. `--summary`/`--provenance`/`--confidence-base` are shorthands; `--set` reaches every other field and **replaces** its value, while `--add`/`--remove` change single elements of an array field (removing an absent element succeeds and says so). Repeating `--set` for one array field appends *within the call*, and `\,` is a literal comma - same rules as `new --set`. Refused with the command that owns them instead: `type:` (page-lifecycle), `confidence:` (derived - set `--confidence-base`), and the page-ref arrays `related:`/`sources:`/`entities:`/`concepts:` (`xref`). Everything else the schema declares is settable, and an unknown field lists what the page actually has. Schema-validates the fields it writes, and `raw_files:` entries must exist on disk. A source declares `date:` instead of `modified:`, and that is the *publication* date of the raw material - it is never bumped to today, and changes only when `--date` names a value explicitly. |
|
||||
| `rename --from "<Old>" --to "<New>" [--dry-run]` | Rename a page and repoint every reference to it: body `[[wikilinks]]` (aliases and anchors preserved), a `[^cite-id]` whose id was derived from the old title (refreshed to match the new one, both in its Footnotes definition and every reference to it), the page's own H1, and every page-ref frontmatter array declared by the type's `page_ref_fields:`. If `--from` is *not* a page but is referenced, it instead repoints those references onto the existing `--to` page and moves nothing - the fix for a reference spelled `act_runner` when the page is `Act Runner` |
|
||||
| `rm --page "<Title>" [--yes] [--dry-run]` | Delete a page and mechanically de-link it. Refuses without `--yes` while other pages still reference it. Strips ref-array entries and bare `- [[Title]]` / `- **label:** [[Title]]` bullets; leaves prose and inline citations in place and reports them |
|
||||
| `move --page "<Title>"` \| `move --reconcile` `[--dry-run]` | Move a page to the directory its type-spec computes for its current frontmatter (`base_dir` + `layout` - the same rule `new` places a page by, via `TypeResolver.compute_target_dir`), never a hand-chosen destination - there is no `--to <dir>`. `--reconcile` applies it corpus-wide: every misplaced page moves in one call, and a second run reports nothing left to do (`lint`'s `Misplaced Pages` finding is the advisory that this fixes - see `lint`). Neither mode touches a body or a frontmatter field, and the page's title (its only identity in the wiki) never changes - only the file moves. A destination already occupied (a pre-existing duplicate-stem collision) is refused rather than silently skipped |
|
||||
| `xref add --a "<A>" --b "<B>" --rel <label>` | 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` |
|
||||
| `xref remove --a "<A>" --b "<B>" [--dry-run]` | 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. |
|
||||
| `xref link-source --source "Source - X" --entities A,B,C` | Batch-link a source page to every entity/concept it mentions: 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 |
|
||||
@@ -48,7 +49,7 @@ tools/wikitool <command> --help
|
||||
| `cite add --page "<Title>" --source "Source - X" [--file <qualifier>] [--dry-run]` | 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 |
|
||||
| `cite sync [--page "<Title>" \| --all] [--dry-run]` | Reconcile each page's footnotes region against its actual `[^id]` references: 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 |
|
||||
| `index rebuild [--dry-run]` | Regenerate the catalog from every page's frontmatter: `kb/index.md` becomes a map (statistics, one row per collection and per area, links to the shards) and the page tables are written to a generated `INDEX.md` in each collection. An area past 50 rows gets its own shard. Stale shards from removed collections/areas are deleted in the same pass |
|
||||
| `log append --op ingest\|query\|lint\|create\|update\|delete\|rename --title "..." [--body "..."\|--body-file path]` | Append a formatted entry to `kb/log.md` |
|
||||
| `log append --op ingest\|query\|lint\|create\|update\|delete\|rename\|move --title "..." [--body "..."\|--body-file path]` | Append a formatted entry to `kb/log.md` |
|
||||
| `log status` | Read-only: count `ingest` entries logged since the last `lint` entry - the deterministic trigger behind the Maintenance Schedule's "every 10 sources" full-lint cadence |
|
||||
| `lint [--json] [--markdown out.md] [--full] [--fail-on-error]` | Structural + provenance checks: broken wikilinks, dangling frontmatter references, orphan pages, index drift, schema gaps, duplicate titles, title mismatches, uncovered raw files, broken `raw_files:` refs, raw files claimed by more than one source page, unmarked provenance, citation/frontmatter drift, unbalanced generated-region markers, edges whose label is missing or not authorised by the source collection's `outbound:` (both hard once `kb_version` has reached the release that introduced labelled edges - advisory below it, so a corpus mid-migration is not refused by the check measuring it), `see-also` edges whose reverse direction already carries a specific label (advisory only - redundant rather than wrong, and never migration-gated, since no version turns the redundancy into an error), quote-limit overages (>2 blockquoted lines/page, advisory only). Prints only the sections that found something and always writes the full report to `reports/Lint Report <date>.md` (or `--markdown`), naming the path - `--full` prints everything, `--json` prints the findings and writes nothing |
|
||||
| `search ["<text>"] [--field <predicate> ...] [--kind/--subtype/--collection/--tag <v>] [--regex] [--limit N] [--sort [-]<field>] [--backend <name>] [--matches] [--json]` | Find pages in `kb/` without reading the index. Text search runs through a pluggable backend (`rg` today); `--field` predicates are evaluated on frontmatter - `f=v`, `f~substring`, `'f>=v'`, `'f:*'` (present), `'!f'` (absent), repeatable and ANDed. With no text this is a pure structured query. Results carry kind/summary/confidence so a hit can be judged without opening the page. A page whose frontmatter does not parse can match no positive predicate, so it is **named** rather than dropped: `--json` always carries an `unreadable` list of `{path, reason}` (usually empty), and the table form writes the same lines to stderr. `--regex` is applied by `rg` alone, whose engine is linear; the ranking boosts for title and summary are literal-containment only, so a non-literal pattern is ranked by match count. `rg` is killed after 30 s and reported as a failure. Read-only, and **exempt from the Iteration Budget Gate** |
|
||||
@@ -80,7 +81,7 @@ tools/wikitool <command> --help
|
||||
| `version release [--title "<...>"] [--dry-run]` | Fix the running candidate: strip `VERSION`'s `-beta.N` suffix and close its `CHANGES.md` entry, ending the pre-release phase `version bump` started. Without `--title` the heading keeps whichever bump last set it; with it, the heading's title is replaced - the normal case for a candidate that collected several bump titles, since the entry wants a summarising heading rather than the most recent one. Leaves the entry's machine-managed bump-title list untouched, as the record of what happened. Commits nothing and pushes nothing (invariant 5) - the following `publish` moves `VERSION` onto `main`, which `release.yml` reacts to. Refuses when `VERSION` is already a release (no running candidate to fix), or when the changelog's newest entry does not match `VERSION` |
|
||||
| `migrate list [--json]` | List every migration document under `instructions/migrations/`, oldest target first, with its kind and obligation. Read-only and **exempt from the Iteration Budget Gate** |
|
||||
| `migrate status [--json]` | Show the migrations this instance still owes, in the order they must run: every **required** document whose `migrates_to` lies in `(kb_version, VERSION]`. `offered` documents are listed separately above the chain and never block, never count as owed, and are bounded by the applied ledger rather than by `kb_version` - taking one deliberately does not move the version, so the version cannot say whether it was taken. When a release stamp is present, also reports which shipped files this instance has since edited (from the per-file sha256 in `.wikitool-release.json`), which is what says whether an offer may be copied over or has to be reconciled by hand; without a stamp that question is reported as unanswerable rather than answered. Exits 1 only when `.wikitool-kb.json` is missing - the content's shape is a question the tool refuses to answer by guessing. Read-only and exempt from the budget gate |
|
||||
| `migrate verify --from <rev> [--path P ...] [--expect-body-change] [--json] [--fail-on-error]` | Compare `kb/` against a git revision on the invariants a content migration must not change: wikilink and citation **counts** (not sets), footnote definitions, H1, structural frontmatter, and the **count of generated-region marker pairs** - a page that went from one links region to two has the same set of region names and a different count, and a lost marker turns a generated region into prose the next write appends a second one beside. Reports added/removed pages without failing on them. `--expect-body-change` additionally flags a page whose body did not change at all. Not migration-specific - worth running after any bulk rewrite, and the one question `lint` cannot answer, since it reads a single revision and so cannot see that something went missing. Read-only and exempt from the budget gate |
|
||||
| `migrate verify --from <rev> [--path P ...] [--expect-body-change] [--json] [--fail-on-error]` | Compare `kb/` against a git revision on the invariants a content migration must not change: wikilink and citation **counts** (not sets), footnote definitions, H1, structural frontmatter, and the **count of generated-region marker pairs** - a page that went from one links region to two has the same set of region names and a different count, and a lost marker turns a generated region into prose the next write appends a second one beside. Pages are matched by **title**, not path, so a page `wikitool move` (or `move --reconcile`) relocated compares as itself - reported separately as `moved` - rather than as a removed-and-added pair. Reports added/removed pages without failing on them. `--expect-body-change` additionally flags a page whose body did not change at all. Not migration-specific - worth running after any bulk rewrite, and the one question `lint` cannot answer, since it reads a single revision and so cannot see that something went missing. Read-only and exempt from the budget gate |
|
||||
| `migrate done <version> [--pages N] [--dry-run]` | Record one migration as applied, advancing `kb_version` in `.wikitool-kb.json` to its target. **Refuses any version that is not the next link in the chain** - skipping one leaves the corpus in a shape no version describes, and an interrupted multi-step upgrade has to be resumable rather than guessable. An `offered` migration is recorded in the applied ledger *without* moving `kb_version` and with no ordering rule applied: it is not a link in the chain, so there is nothing to skip, and requiring the chain first would make an unrelated file upgrade wait on it. Re-recording one already in the ledger is a no-op, not an error |
|
||||
| `migrate baseline <version> [--force]` | Declare `kb_version` once, for an instance predating `.wikitool-kb.json`. Refuses to overwrite an existing declaration without `--force`: advancing after a migration is `done`, which checks the chain, and this command must not become the quiet way around it |
|
||||
| `upstream merge [--remote upstream] [--branch main] [--no-fetch]` | Take a stack update into a private instance's branch, machinery only - the code procedure behind `instructions/private-instance.md` § "Taking a stack update". Refuses on a dirty working tree, a merge already in progress, or a remote that does not resolve; WARNs (does not block) when `.wikitool-remotes.json` is absent, pointing at the setup step that arms it. Fetches `<remote>/<branch>` (unless `--no-fetch`) and reports "already up to date" if nothing new exists. Otherwise opens `git merge --no-commit --no-ff <remote>/<branch>` - and stops, untouched, if git refused to open a merge at all (unrelated histories), since without a `MERGE_HEAD` every stack path would read as "the upstream deleted it". Then forces every content stage (`kb/`, `raw/`, `work/`, `reports/`) back to the local side by removing **only the paths tracked in either tree** and checking `HEAD`'s back out - never the stage directory wholesale, because `reports/` is gitignored apart from its contract and holds local, non-recomputable data (telemetry traces `eval score` reads, saved eval and lint reports) that no merge has business deleting. Then restores from the upstream side exactly the paths `chemenu.ownership.is_stack_owned` recognises as machinery (`<stage>/CONTRACT.md`, and anything ending `.template` under a content stage) - including a deletion, if the upstream removed one. A real conflict left in `tools/`, `types/` or `instructions/` after that leaves the merge open, uncommitted, and exits 1 rather than guessing. Commits with `git commit --no-edit`, then re-checks the resulting range with the same logic as `upstream verify`; a finding there is a loud, uncommitted-nothing-rolled-back error, because the merge commit already exists and needs a human's eyes, not an automatic repair. Never pushes. Not idempotent - see the tool error contract below |
|
||||
@@ -173,6 +174,7 @@ is atomic, and whether a retry is safe.
|
||||
| `touch` | Page not found; an invalid value for a field it writes; a field owned by another command (`type:`, `confidence:`, a page-ref array) or absent from the type's schema; `--add`/`--remove` on a non-array field; a `raw_files:` path that doesn't exist | Yes - single file write, and every refusal happens before it | Fix the argument and retry once. Safe to re-run as-is: `--set` and `--add` are idempotent, and `--remove` of an already-absent element succeeds while reporting it |
|
||||
| `rename` | Neither `--from` nor `--to` is a page, target title already taken, or `--from` equals `--to` | No - one write per referencing page, then the file move | Safe to retry once as-is; each page's rewrite is idempotent. Use `--dry-run` first to see the blast radius. Never fix up references by hand instead |
|
||||
| `rm` | Page not found, **or** other pages still reference it and `--yes` was not passed | No - one write per referencing page, then the delete | For "still referenced": show the user the inbound list, get approval, then re-run with `--yes`. Prose references it reports afterwards are an editorial fix, not a retry |
|
||||
| `move` | Neither or both of `--page`/`--reconcile` given, the named page not found, it has no `type:` to compute a placement from, or the destination already exists | `--page`: yes, a single file move. `--reconcile`: no - one file move per page, each idempotent | Safe to retry once as-is; a page already at its computed location is reported and left alone, and `--reconcile` only re-moves what is still misplaced. Use `--dry-run` first to see the blast radius. Never choose a directory by hand instead |
|
||||
| `xref add` | Page A or B not found, or a page's type declares no `related:` field | No - writes A then B, but both edits are idempotent, and both refusals happen before either write | 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 |
|
||||
| `xref remove` | Page A not found (B is allowed not to exist) | No - writes A then B, both idempotent | Safe to retry freely; removing an absent link is a no-op |
|
||||
| `xref link-source` | Source page not found, an entity in `--entities` doesn't exist, or the source page itself could not be written after its targets were | No - one write per entity plus one for the source page, idempotent per page | Use `--dry-run` first; safe to retry. `sources trace --page "<Title>"` shows who was already linked |
|
||||
|
||||
Reference in New Issue
Block a user