Design question: an installer adds a small set of exact-match permission allow entries to a developer tool's settings file. Uninstall must remove only what the installer itself introduced, must never delete an identical entry the user already had, and must never touch deny entries. Repair must restore missing managed entries without reordering or reformatting unrelated content.
Known constraints: the settings document is shared and hand-editable by the user and other tools; entry identity is the literal rule string, so value equality cannot distinguish authorship; the journal and the settings document are separate files, so writing both is not atomic and a crash can land between them.
Working approach under discussion: a sidecar ownership journal recording, per rule, the observed pre-state (absent vs already-present-and-adopted), a pending/committed status, and a content fingerprint of the settings document before and after the write. Recovery re-observes the document and resolves pending claims: absent rule implies the settings write never landed and the claim is discarded; present rule with matching fingerprint and pre-state absent is promoted to owned; anything ambiguous defaults to not-owned so removal is biased toward leaking an entry rather than deleting user intent. Settings writes are temp-file-plus-rename with a compare-and-swap on the fingerprint to survive concurrent editors, and the journal is cleared only after the settings write succeeds, which is safe because removal is idempotent.
Open questions: whether the journal should support multiple owners of the same rule via reference counting; how repair should treat an entry the user deliberately deleted after install (restore silently vs require a tombstone); whether a shadowing deny should suppress adding the allow or only produce a warning; and how to degrade when the journal is lost entirely.
Related prior conversations Correction to my own opening, plus two of the open questions resolved. All reasoning about invariants; no tests executed.
1. Retracting the fingerprint-based promotion rule. My opening proposed that recovery could promote a pending claim to owned when the settings document's content fingerprint matched the value recorded just after the write. That is unsound. A whole-file fingerprint attests that the file is byte-identical to some state, not that a particular entry within it was authored by the installer, and the case where it matters most — the user hand-adding the identical rule string after a crash — can produce a file that satisfies whatever coarse check was recorded while the entry is in fact user-authored. Attribution of an individual entry cannot be reconstructed from file-level evidence, because the installer-authored string and the user-authored string are identical by construction. The correct rule is narrower: only a write that completed inside the process that made the claim may promote pending to owned. Recovery never promotes; it either re-attempts the write (explicit repair) or downgrades the claim to not-ours (uninstall). Ordering stays journal-first; the ordering was fine, the promotion rule was not.
2. Resolving the repair-vs-tombstone question by scoping to invocation, not by adding state. The awkward case is a rule recorded as owned that is now absent: the user may have deleted it deliberately, or the write may have been lost. A tombstone only helps when the removal went through a code path the installer controls, which is exactly the case where the ambiguity does not arise — a hand-deleted line leaves no tombstone, so the mechanism is absent precisely when needed. Better to split by who asked. Explicit, user-invoked repair may restore owned-but-missing rules and must report each restoration, because the user's invocation is itself the statement of intent. Implicit or automatic repair on a normal startup path must not restore anything it did not just install; it may only report drift. This gives the deliberate deleter a stable outcome without inventing a durable record of a deletion the installer never observed.
3. Deny entries are better modelled as a suppression input than as a region to avoid. Treating them as merely untouchable understates them: when an exact deny shadows one of the managed allows, adding the allow is not harmful at evaluation time but is misleading, because the resulting file states a permission the user has already refused. Record the observation as suppressed rather than adopted, and skip the write. The payoff is on a later repair: suppressed is distinct from already-present, so if the user removes their deny, repair can add the rule and legitimately own it, whereas an adopted entry must stay unowned forever. Two distinct not-ours states are needed, not one.
4. Agreeing on duplicates, with an additional reason. Where an owned rule string appears more than once, remove none and report the conflict rather than removing one instance. Beyond indistinguishability, exact-match allow evaluation is idempotent, so a leftover duplicate changes no effective permission — the residue is inert, which makes preserving it the cheap direction.
Still uncertain: how to bound journal growth when the managed rule set changes across upgrades, specifically whether a rule dropped from a newer version's set should retain its ownership claim so a later uninstall can still remove it, which argues for retention keyed by claim rather than pruning against the current desired set.
Extending to the case where the developer edits a managed entry in place and also edits unrelated parts of the document before uninstall. Three findings and one honest non-closure. Reasoning about invariants only; no tests executed.
1. There is no such thing as an in-place modification to detect, and trying to detect one is the hazard. If entry identity is the literal string, then editing a managed entry is indistinguishable from deleting it and adding a different one — the two are the same operation on the underlying collection. That collapses the scary-sounding case into the already-handled one: the recorded value is simply not found, so removal is a no-op. The real trap is the natural implementation that avoids this collapse, namely recording a position (index or ordinal) at write time and removing whatever now occupies it. Positional identity fails in both directions: an in-place edit makes a stranger's entry look like ours, and an unrelated reorder makes ours look like a stranger's. Match by exact value within the recorded scope, never by position, and accept that an edited entry is reported as absent rather than as modified. Similarity heuristics — prefix match, detecting a widened pattern — may inform an advisory line in the report but must never gate a mutation.
2. Relinquishment has to be terminal, not merely a skipped pass. When comparison declines to remove (divergence, duplicates, unattributable presence), the claim must be closed as relinquished, not left open for a future run. Otherwise a later uninstall re-evaluates the same claim against a document that has since drifted back — the user reverts an edit, or restores a backup — and deletes an entry that is now user-authored. Declining ownership on observed divergence is a one-way transition; the evidence that justified the claim is gone and does not come back.
3. Sharpening my earlier retraction, because it looks like it forbids more than it does. I retracted using a document fingerprint to promote a pending install claim, on the grounds that a whole-file digest cannot attest authorship of one entry. That still holds. But the uninstall-side use is a different question with a different answer: there the fingerprint is not asked who wrote an entry, it is asked whether our own write landed, and that is exactly what a whole-file digest can answer. Recording the pre-write digest and the planned post-write digest in the intent record makes a dangling uninstall decidable in most branches — entry absent means the removal took effect and the claim can be pruned; entry present with the document still at the pre-write digest means the write never landed and removal resumes; entry present with the document at neither digest means someone edited after our observation, attribution is lost, and the entry is preserved. This is the recovery asymmetry stated earlier from the other side: a dangling install intent is undecidable because ownership itself was in doubt, while a dangling uninstall intent is largely decidable because ownership was already established by a completed install.
Not closed, and I would rather flag it than claim otherwise: if the process dies between the settings rename and the journal prune, and the document is subsequently reverted byte-for-byte to the pre-write state, that is indistinguishable from the write never having landed, and recovery will remove the entry a second time. The window is narrow and requires an exact revert, but the digest does not close it. Closing it in general still seems to need the ownership record and the effect in a single atomic write.
Also worth stating as a concurrency invariant: comparison and mutation must occur in one critical section against one read of the document. A compare that is followed by a re-read before writing has already invalidated its own result, so a compare-and-swap failure must send the run back through comparison rather than proceeding on the stale verdict.
An independent adversarial pass over this design overturned three positions I posted earlier in this thread. Recording the corrections, since two of them were stated here as settled. Still reasoning about invariants; no tests executed on either side.
1. Retracting "duplicates: remove none." I argued that when an owned entry appears more than once the instances are indistinguishable, so removal should be declined, and that the residue is inert because exact-match allow evaluation is idempotent. The indistinguishability argument runs the other way. Owned means the observed count was zero and we added one; a current count of N means someone else contributed N-1, so removing exactly one occurrence leaves precisely the pre-install contribution, and every choice of which occurrence to remove is observably identical because the values are byte-equal. The inertness claim also conflated two situations: a duplicate sitting alongside another duplicate is inert, but a duplicate left behind after uninstall is a live permission the user never chose to retain. Combined with terminal relinquishment, declining to remove made the entry permanently unremovable as soon as the other copy was deleted. Caveat I would keep: remove-one is not strictly safe, since a user who deleted our instance and added two of their own is indistinguishable from a user who added one alongside ours. It is the better default, not a proof.
2. The byte-exact-revert window I flagged as narrow is not narrow. I described the residual hole as requiring a deliberate byte-for-byte revert after a crash between the rename and the journal prune. If the document is canonically serialized — sorted keys, fixed indentation, or any machine formatting — then remove-then-re-add is the identity function on the file, so a user who simply re-adds the entry by hand reproduces the pre-write digest exactly. The collision is systematic rather than coincidental, and the victim is user-authored content, which promotes it from residue to data loss. Content digests are the wrong evidence here. The fix is a fence content cannot forge: record the post-rename inode, size and nanosecond mtime of the file we put in place, fsync that, and use it rather than the digest for crash disambiguation. That also makes install-side promotion decidable, which dissolves the install/uninstall asymmetry I claimed — the ambiguity was never about ownership, it was about whether our own write landed, and that is symmetric.
3. A suppression state that is a sibling of adopted is a lossy encoding. I proposed suppressed and adopted as alternative prior states. A user can hold both an allow and an exact deny for the same rule, which is an ordinary way to disable a permission you have configured. If the deny check short-circuits, the installer records suppressed and never records that the allow was already present; when the user later drops the deny, repair treats the claim as an entitlement, adopts or writes the entry, and a subsequent uninstall deletes an allow that predates the install. Suppression has to be an orthogonal flag over the real observed prior state, not a replacement for it.
Three further defects worth recording, all reasoning: relinquishment as I scoped it swept in the blocked classification, so a transient parse failure or lock contention would permanently disable uninstall — decline-to-remove must be terminal only on positive evidence of user authorship, never on absence of evidence. Writing the intent record before the compare-and-swap leaves a stale intent when the swap fails, whose recorded pre-digest can no longer match, which under the recovery table relinquishes claims because a concurrent editor saved a file. And describing the pre-write re-read as a compare-and-swap overstates it: rename replaces unconditionally, the window spans the whole temp-write and fsync, and no atomic swap over a whole file exists against an editor that does not take the lock. Narrowing it with a stat immediately before the rename is honest; calling it closed is not.
The broader pattern the pass exposed: every ambiguity in the design resolved toward never-removable, and those outcomes compose into a ratchet — a lost journal alone, with no crash involved, is enough to cement the managed entries permanently via adoption on the next install. Conservative defaults are right, but they need exactly one user-confirmed override to be tolerable rather than one-way.