mirror of
https://github.com/Ar9av/obsidian-wiki.git
synced 2026-09-14 20:36:34 +08:00
58aba1808c
* docs(contract): define the portable source key contract A vault can be checked out on more than one machine and synced over git, so a stored source key must be machine-independent. Replace the "keys MUST be absolute paths with ~ expanded" rule with three canonical forms: - vault-relative for sources inside the vault (`Raw/x.pdf`), - home-relative for sources under `$HOME` (`~/.claude/...`), - a pseudo-key (any `scheme:` / `://` identifier) for anything that is not a file. Never store a bare absolute path; normalize before comparing; identity must survive a path change. The pseudo-key namespaces are recommended names, not a closed list — what matters is the shape, so a custom scheme still works. Project identity follows the same rule: `source_repo` (`host/owner/name`), with an optional `~`-relative `source_cwd_hint` for a machine-local checkout. Legacy manifests full of absolute keys stay readable, and `scripts/manifest.py migrate` converts them (merging collisions, keeping the newest `ingested_at`); after a cross-machine move the old vault root is supplied with `--from-root`. This is the single canonical definition — the other skills reference it. * feat(cache): resolve portable keys and classify cross-machine sources Read and write manifest source keys in the portable forms instead of treating the raw string as the identity. Write side: `stored_key` maps a source to vault-relative (`Raw/x.pdf`) or home-relative (`~/.claude/...`), and `update_source` records that form. A source with no portable representation keeps its path only as a fallback and now warns on stderr; an explicit `key=` from the caller is authoritative and re-keys an entry the manifest previously tracked by path. Read side: `resolve_key` normalizes every stored form to an absolute path (or None for a pseudo-key), and `_same_source` / `_missing_on_disk` compare through it, so an absolute query matches a vault-relative or `~`-relative key and legacy absolute keys keep working unchanged. Classification: `check_sources` gains an `unavailable` bucket. A key that is machine-specific and points outside this vault may simply not exist here, so it is reported as `unavailable` rather than `missing` — `missing` narrows to vault-local sources genuinely gone, which is what `wiki-ingest` reads as "optionally clean up". A relative key counts as vault-local only when its first segment names a real top-level vault entry; a bare vault-root filename is vault-local by construction, while a legacy ingest-root key, an out-of-vault namespace, and a key written for another OS (drive-letter or backslash form) are not. When the vault cannot be listed the lexical answer stands, so an I/O hiccup never hides a real loss. CLI: `cache-update --key` records an explicit pseudo-key (`repo:` / `url:` / `agent:`) for a source outside the vault and `$HOME`, and the JSON receipt reports the normalized key. * feat(manifest): mirror the key helper, resolve delta keys, add migrate `scripts/manifest.py` is a standalone stdlib helper that cannot import the package, so it carries its own copies of `resolve_key` / `stored_key` (kept honest by the parity test in `tests/test_manifest_portable_keys.py`). `delta` now resolves every stored key to an absolute path before matching, so a vault-relative or home-relative key is directly hit by a scanned absolute path instead of showing up as new. `migrate` (with `normalize` kept as an alias for existing instructions) rewrites legacy absolute keys to the portable form: in-vault keys become vault-relative, `$HOME` keys become `~`-relative, and pseudo-keys and legacy ingest-root-relative keys are preserved untouched. A path with no portable form is kept as-is with a warning rather than dropped, so provenance is never lost, and collisions merge by keeping the newest `ingested_at` while unioning `pages_created` / `pages_updated` / `pages_produced`. After a vault moves between machines its old keys are rooted at the *old* vault path, which matches neither the new root nor `$HOME`; `--from-root <old-vault-root>` (repeatable) strips it explicitly. Without it the summary says `nothing portable to write — N key(s) kept non-portable` rather than claiming success. The old root is never guessed — a wrong guess would write plausible-looking but incorrect relative keys — so the hint names the flag to pass instead. * feat(lint): report machine paths in page sources frontmatter A page `sources:` entry holding a machine absolute path cannot resolve on another machine, so a synced vault silently loses the trail. Add a read-only finding, `machine_path_sources`, that names the page and the offending entries. It is reported at warn level and rewrites nothing — cleaning existing pages is a one-time, owner-reviewed pass, not something a lint run should do. All three frontmatter shapes are handled (flow list, scalar value, block list), and both POSIX and Windows-drive absolute paths are recognized. `~`-relative and vault-relative entries are fine and are not reported. * docs: converge skills on the key contract and document the new surface Point the dependent skills at the contract instead of restating (and diverging from) it: - `wiki-status`: describe portable keys, document the `Unavailable` classification, and identify a project by `source_repo` + `source_cwd_hint` in the manifest example. - `wiki-ingest`: document the `unavailable` bucket and state that the manifest key and page `sources:` must be portable; note `cache-update --key` for a source with no portable path form. - `wiki-update`: record project identity as `source_repo` and an optional `~`-relative `source_cwd_hint`. - `wiki-query`: resolve a project's source from `source_repo`, falling back through `source_cwd_hint` -> legacy `source_cwd` -> page `source_path`, and report the repository rather than a machine path. - `claude-history-ingest`: replace the "keys are absolute paths" instruction. - `wiki-rebuild`: use an `$OBSIDIAN_VAULT_PATH` placeholder instead of a user path. User docs get the same treatment: `docs/cli.md` documents `cache-update --key`, `manifest.py migrate` / `--from-root`, the `unavailable` bucket and the lint finding, and `docs/architecture.md` states the key rule. Structural doc tests pin the contract section, the documented CLI surface, the portable manifest example, and that all four skills agree on the project identity fields. * fix(manifest): read the ingest timestamp under either field name cache.py writes `last_ingested`; `_newest` and `cmd_delta` read only `ingested_at`. On a real manifest both comparisons saw "", so a migrate collision kept the first-seen entry rather than the newest, and every already-ingested file came back MOD. --------- Co-authored-by: ar9av <ar9avg@gmail.com>