-
Notifications
You must be signed in to change notification settings - Fork 26
fix(vault): slug_hint, ghost-page guard, dedup proposals, warn on claim stub edits #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: test
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,6 +84,7 @@ class VaultSyncResult: | |
| pages_proposed: list[str] = field(default_factory=list) | ||
| pages_skipped_unchanged: list[str] = field(default_factory=list) | ||
| pages_skipped_unknown_id: list[str] = field(default_factory=list) | ||
| claim_stubs_edited: list[str] = field(default_factory=list) | ||
|
|
||
|
|
||
| # --- state file ----------------------------------------------------------- | ||
|
|
@@ -268,6 +269,29 @@ def kb_to_vault(store: KBStore, vault_dir: Path) -> VaultSyncResult: | |
| # --- forward direction: vault -> KB --------------------------------------- | ||
|
|
||
|
|
||
| def _has_pending_page_proposal( | ||
| store: KBStore, page_id: str, *, body: str | None = None | ||
| ) -> bool: | ||
| """Return True if a pending proposal already targets ``page_id``. | ||
|
|
||
| When ``body`` is supplied, only returns True if the pending proposal | ||
| also carries the same body — allowing a second different vault edit | ||
| to file a new proposal even while the first is still pending. | ||
| Prevents duplicate proposals when vault_to_kb runs multiple times | ||
| before the reviewer approves the first proposal for a given page edit. | ||
| """ | ||
| from .models import ProposalKind, ProposalStatus | ||
| for proposal in store.list_proposals(ProposalStatus.PENDING): | ||
| if proposal.kind != ProposalKind.PAGE: | ||
| continue | ||
| payload = proposal.payload | ||
| if not isinstance(payload, dict) or payload.get("id") != page_id: | ||
| continue | ||
| if body is None or payload.get("body") == body: | ||
| return True | ||
| return False | ||
|
|
||
|
|
||
| def vault_to_kb( | ||
| store: KBStore, | ||
| vault_dir: Path, | ||
|
|
@@ -315,6 +339,35 @@ def vault_to_kb( | |
| result.pages_skipped_unknown_id.append(rel) | ||
| continue | ||
|
|
||
| # Fix 1 (#219): guard against proposing edits for pages that no | ||
| # longer exist in the KB. The mirror file may outlive the KB page if | ||
| # the page was deleted after the last backward sync; filing a proposal | ||
| # for a ghost page would produce an unresolvable slug on approve. | ||
| try: | ||
| store.get_page(page_id) | ||
| except ArtifactNotFoundError: | ||
| log.warning( | ||
| "vault sync: mirror file %s references page %r which no longer " | ||
| "exists in the KB; skipping to avoid a ghost proposal", | ||
| rel, page_id, | ||
| ) | ||
| result.pages_skipped_unknown_id.append(rel) | ||
| continue | ||
|
|
||
| # Fix 2 (#219): skip if a pending proposal already targets this page | ||
| # id. Without this guard, running vault_to_kb twice before the first | ||
| # proposal is approved files duplicate proposals for the same edit, | ||
| # cluttering the review queue and causing the second approve to fail | ||
| # with "page already exists". | ||
| if _has_pending_page_proposal(store, page_id): | ||
| log.debug( | ||
| "vault sync: pending proposal already exists for page %r; " | ||
| "skipping to avoid duplicate", | ||
| page_id, | ||
| ) | ||
| result.pages_skipped_unchanged.append(rel) | ||
| continue | ||
|
|
||
| # Record the user's edit as a vault-origin source so reviewers can | ||
| # see the exact bytes that triggered the proposal. Content-addressed | ||
| # via sha256, so re-runs against the same bytes coalesce on the same | ||
|
|
@@ -328,10 +381,11 @@ def vault_to_kb( | |
| tags=["vault", "vault-sync"], | ||
| ) | ||
|
|
||
| # File the page-edit proposal. The store-side validator will reject | ||
| # any references to artifacts the KB doesn't know about -- which is | ||
| # the right behaviour: the user can't introduce phantom claim ids | ||
| # from inside Obsidian. | ||
| # Fix 3 (#219): pass slug_hint=page_id so the proposal targets the | ||
| # existing page rather than a slugified copy of the title. Without | ||
| # this, a page with id "auth-decision-001" and title "Auth Decision" | ||
| # would produce a proposal for a new page "auth-decision", silently | ||
| # duplicating the KB entry on approve instead of updating it. | ||
| propose_page( | ||
| store, | ||
| title=edited.title, | ||
|
|
@@ -342,12 +396,33 @@ def vault_to_kb( | |
| source_ids=list({*edited.sources, source.id}), | ||
| proposed_by=actor, | ||
| tags=list(edited.tags), | ||
| slug_hint=page_id, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
when a vault edit is proposed now, this Useful? React with 👍 / 👎. |
||
| ) | ||
| result.pages_proposed.append(page_id) | ||
|
|
||
| # Update state so we don't re-propose the same edit on the next tick. | ||
| state[rel] = current_hash | ||
|
|
||
| # Fix 4 (#219): walk claims/ and warn on any user edit. Claim stubs are | ||
| # read-only mirrors written by kb_to_vault; edits there are silently | ||
| # dropped without this guard. Warn so the user knows to edit the citing | ||
| # page instead. | ||
| claims_mirror = _claims_dir(vault_dir) | ||
| if claims_mirror.is_dir(): | ||
| for path in sorted(claims_mirror.glob("*.md")): | ||
| rel = f"claims/{path.name}" | ||
| recorded = state.get(rel) | ||
| if recorded is None: | ||
| continue | ||
| current_hash = _sha256_text(path.read_text(encoding="utf-8")) | ||
| if current_hash != recorded: | ||
| log.warning( | ||
| "vault sync: edit detected in claim stub %s; claim stubs " | ||
| "are read-only mirrors — edit the citing page instead", | ||
| rel, | ||
| ) | ||
| result.claim_stubs_edited.append(rel) | ||
|
|
||
| _save_state(vault_dir, state) | ||
| return result | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when a user makes a second different edit to the same mirrored page while the first proposal is still pending, this condition only checks the page id and then treats the changed file as skipped/unchanged. in the default
sync_vaultdirection (both), the following backward pass rewrites the vault from the current KB and drops that second edit without ever filing a proposal; in forward-only mode it remains an unreported changed file. compare the current bytes/pending payload or preserve the changed file instead of treating all same-page pending proposals as duplicates.Useful? React with 👍 / 👎.