diff --git a/docs/architecture/C1-repair-job-authority.md b/docs/architecture/C1-repair-job-authority.md index db504d1..523f8a3 100644 --- a/docs/architecture/C1-repair-job-authority.md +++ b/docs/architecture/C1-repair-job-authority.md @@ -121,15 +121,18 @@ offending names in declaration order. Two structural invariants are enforced as configuration validity rather than as a runtime check that could be forgotten: -- `repairBranch` and `protectedParentRef` denote **different branches**. A job - whose repair branch *is* the protected parent is not a quarantined repair; it - is a direct write to protected history wearing a repair job's name. +- `repairBranch` and `protectedParentRef` are **different branch refs** under + C1's canonical comparison rule. A job whose repair branch *is* the protected + parent is not a quarantined repair; it is a direct write to protected history + wearing a repair job's name. This compares canonical ref *names*; establishing + that two accepted names resolve to distinct targets in a repository is the + later trusted execution boundary's obligation, described below. - `independentValidatorId !== repairAgentId`. A repair agent that is its own validator defeats the quarantine the whole pipeline exists to enforce. ### Branch refs have exactly one accepted spelling -"Different branches", not "different strings". Git resolves `main`, +"Different branch refs", not "different strings". Git resolves `main`, `heads/main`, and `refs/heads/main` to one and the same ref, so a boundary that compares ref strings has three names for one authority target. Configuring `protectedParentRef: 'refs/heads/main'` beside `repairBranch: 'main'` would @@ -149,14 +152,18 @@ and refuses every other spelling of the same branch as malformed: - no segment beginning or ending with `.`, no `..` anywhere, and no segment ending in `.lock` in any ASCII case -The property that buys: **two accepted refs denote the same branch if and only if -they are equal strings.** That is what makes the distinctness invariant mean -something. The conservative ASCII character set is part of the guarantee, not a -convenience — it removes Unicode normalisation, under which an NFC and an NFD -spelling of one name are unequal strings a filesystem-backed loose ref can -resolve to a single ref, and it removes `~`, `^`, `:`, `?`, `*`, `[`, `\`, `@{`, -and whitespace in one rule. Nothing is normalised, prefixed, or case-folded on -the way in: a value is accepted exactly as supplied or refused. +The property that buys is a property of ref *names*, not of repository state: +**two accepted refs are the same canonical ref name if and only if they are equal +strings.** That is what closes caller-controlled textual aliasing and what makes +the distinctness invariant mean something at this layer. It is not a claim that +two unequal canonical names denote two distinct targets in a repository; see +*What canonical ref names do and do not prove* below. The conservative ASCII +character set is part of the guarantee, not a convenience — it removes Unicode +normalisation, under which an NFC and an NFD spelling of one name are unequal +strings a filesystem-backed loose ref can resolve to a single ref, and it removes +`~`, `^`, `:`, `?`, `*`, `[`, `\`, `@{`, and whitespace in one rule. Nothing is +normalised, prefixed, or case-folded on the way in: a value is accepted exactly +as supplied or refused. The same reader is applied to **every** security-relevant ref position — the two job fields, and the `ref`, `sourceRef`, and `targetRef` request operands — so @@ -165,14 +172,6 @@ uncanonical request operand. A supplied operand that is not canonical is refused `REF_MALFORMED` before any comparison, rather than compared as though it were a different branch. -**What this does not prove**, and must not be claimed to: that two unequal -accepted refs are two distinct refs on every filesystem. Git stores loose refs as -files, so on a case-insensitive filesystem `refs/heads/Main` and -`refs/heads/main` can be one ref while comparing unequal. C1 observes no -filesystem, so it refuses the ambiguous case instead of pretending it away: the -job's two configured refs are additionally compared with ASCII case folded, and a -pair that differs only by case is rejected as malformed configuration. - One relationship is enforced at authorization time, because it is about freshness rather than shape: `findingHeadSha` must equal `parentHeadSha`, or every operation is denied `FINDING_SHA_STALE`. A repair derived from a finding @@ -181,6 +180,169 @@ PR 004 remains the owner of `CURRENT` versus `STALE` for evidence; this is the narrower structural check that the job's own two SHAs agree, which C1 can decide without importing that kernel or producing a second answer to PR 004's question. +### What canonical ref names do and do not prove + +Stated precisely, because overclaiming here would be worse than not checking. + +**Proved.** An accepted value is a string in the one canonical `refs/heads/` +shape, and two accepted values that remain unequal under C1's documented +comparison rule are two different canonical ref names. Caller-supplied textual +aliasing is closed within that structural authority: the originally proven bypass +— configuring or requesting `main`, `heads/main`, and `refs/heads/main` against +one another so the protected parent could be presented as a different branch — is +refused as `REF_MALFORMED` before any comparison, and a configured pair that +collides under the comparison rule invalidates the job rather than authorizing it. + +**Not proved, and not claimed.** That two different canonical ref names are two +distinct branch targets in a repository. C1 does not establish repository-resolved +ref identity, does not detect whether an accepted ref is symbolic, does not +resolve a symbolic ref's target, does not determine whether two distinct canonical +names ultimately dereference to the same repository target, and observes no live +repository state. Two independent reasons stand: + +- **Symbolic refs.** A repository may hold a canonical-looking ref — say + `refs/heads/repair` — that is itself a symbolic ref to `refs/heads/main`. + Whether such a ref exists, and what it points at, is repository state at the + moment the name is used. C1 runs no git, spawns no subprocess, opens no file, + and observes no repository, so no string comparison it performs can decide it. +- **Filesystem identity.** Git stores loose refs as files, so on a + case-insensitive filesystem `refs/heads/Main` and `refs/heads/main` can be one + ref while comparing unequal. C1 observes no filesystem, so it refuses the + ambiguous case instead of pretending it away: the job's two configured refs are + additionally compared with ASCII case folded, and a pair that differs only by + case is rejected as malformed configuration. + +The case fold is a conservative refusal, not a resolution. It narrows one +filesystem-dependent collision that is characterisable from the strings alone; it +establishes nothing about symbolic refs, which are not decidable from a string at +all. + +**A future trusted repository/Git execution boundary must close the rest.** C1 +establishes structural canonical ref-name authority; it cannot establish live +repository identity, cannot bind the target a mutation will actually reach, and +cannot enforce anything across a concurrent change. Before acting on any authority +an `ExecutionPermit` records — not only operations that write a ref — that +boundary must satisfy the requirements below, and must **fail closed** — refuse +the operation — wherever a required identity cannot be safely established, +wherever resolution cycles or is otherwise indeterminate, or wherever an effective +identity is or dereferences to a ref the operand's role is not authorized to +denote. + +*The protected-parent rule is role-bound.* Protected-parent identity is forbidden +only where it is unauthorized for the operand's role — which is every role but +one. The effective mutation target of a `repair.commit` or a `repair.push`, and a +`repair.change_request` `sourceRef`, must each be the authorized repair ref, so +for all three an effective identity that is or dereferences to the protected +parent is a refusal. A `repair.change_request` `targetRef` is the single operand +whose *required* effective identity **is** the protected parent ref — the same +operand C1's string layer already singles out as the only one that may name it — +so for that role, and only that role, reaching the protected parent is the +authorized outcome and reaching anything else is the refusal. Stated role-blind +instead, the rule would forbid the one direction the quarantine depends on. No +role widens past this: an operand authorized to denote the protected parent as a +change-request *target* acquires no authority to denote it anywhere else, and the +exemption never reaches an operand that would mutate the parent. + +*Which identity is compared.* The isolation question is about the **effective +ref-name referent** — the terminal ref reached by resolving a symbolic-ref chain — +not about commit-object identity. A freshly created repair branch may legitimately +point at the **same commit object** as the protected parent until its first repair +commit, so distinct commit OIDs are neither necessary nor sufficient: two +different branch refs may share one commit OID, and commit-object equality does not +make two refs the same authority target. The boundary must therefore compare +effective ref-name referents, and must not rest the check on whether two refs +currently resolve to the same commit. A terminal ref-name *spelling* is not by +itself repository ref identity: where a repository applies its own ref-identity +semantics — for instance a case-insensitive ref store under which +`refs/heads/Main` and `refs/heads/main` are one ref — two terminal names that are +not equal strings may still be the same repository ref, so the boundary must +decide whether two effective referents are the same or distinct under that +repository's actual ref-identity semantics rather than by terminal-name string +(in)equality alone, and must fail closed wherever the required distinctness cannot +be safely proven under those semantics. What that comparison must *yield* is fixed +by the operand's role: for an operand whose required identity is the authorized +repair ref, a symbolic or effective ref-name identity that aliases the protected +parent must be detected and rejected; for the one operand whose required identity +is the protected parent ref — the `repair.change_request` `targetRef` — the alias +to detect and reject is the converse one, an effective identity that is not the +protected parent ref. + +*Binding the effective mutation target.* A resolved ref *name* is not the target a +mutation will advance, and the boundary must bind the two before it acts: + +- **`repair.commit`.** A commit advances the branch reached through the authorized + worktree's effective `HEAD` referent, not whatever ref name the request carried. + The boundary must bind that effective `HEAD` referent to the authorized repair + ref and refuse to commit if the worktree is detached, attached to the protected + parent, attached to any other ref, or its safe binding cannot be established. +- **`repair.push`.** A push carries both a source and a destination ref, and the + authorized repair ref governs **both**. The boundary must bind the push's + **effective source ref** and its **effective destination ref** — each by its + effective ref-name referent, not by commit-object identity — to the authorized + repair ref, and must not let a caller-selected source or destination refspec + redirect either half. The source must be **present**: an absent source, the + deletion refspec `:refs/heads/…`, is not a `repair.push` at all but a + `branch.delete`, which is denied, so a destination that still names the repair + ref does not make it authorized. No alternate branch, tag, or commit-ish may + stand in for the authorized repair ref on either half. The receiving/mutation + side must fail closed if either effective half is the protected parent, is not + provably the authorized repair ref, or ceases to be between the check and the + push — the authorized source-to-destination relationship must hold through to + that consuming boundary, not only at an earlier pre-check. An ordinary + `refs/heads/repair:refs/heads/repair` push, whose effective source and + destination are both the authorized repair ref, remains authorized. + +*Operands that set direction without mutating a ref.* The obligation is not +limited to ref-mutating operations. `repair.change_request` mutates no ref, but +its `sourceRef` and `targetRef` fix the effective direction of the stacked +validation request, which the quarantine requires to run **from** the repair +branch **to** the protected parent. The boundary must establish that the effective +source identity is the authorized repair ref and the effective target identity is +the protected parent ref, reject a symbolic or effective alias that changes that +direction, and fail closed if either effective identity cannot be safely +established. Because this operation performs no ref update to guard, the boundary +that consumes these identities is the change-request/provider creation — or +update — request itself, and the established source and target identities must be +**bound through to that provider request**: the provider must create the request +from exactly the authorized effective source and target. Provider-side resolution +of the supplied ref names is not itself forbidden — a create/update API may have +to resolve the source and target names against its own authoritative repository +state — but it must yield exactly those authorized effective identities: it must +not let re-resolution, ambient repository state, or any substitution cause the +request to be created from, or to consume, a **materially different** effective +source or target than the one authorized. Resolution that preserves the exact +authorized source-to-target relationship conforms; resolution that would consume +a materially different effective identity does not. If that authorized +source-to-target relationship cannot be maintained through to the provider +request — because an effective identity has changed, cannot be safely +re-established, or cannot be shown equivalent to the authorized one at that +boundary — the boundary must fail closed and create no change request. + +*Concurrency is not closed by a pre-check.* A resolve-then-check-then-act +sequence is **not** an atomic security guarantee: an effective ref or referent can +change between the comparison and the moment the identity is consumed, so a name +observed as an ordinary repair ref can become symbolic to the protected parent — +or a target can cease to denote it — after the check and before the act. The +invariant must be enforced **at the actual trusted execution boundary that +consumes each identity, not only where a ref is mutated**, by a mechanism whose +semantics prevent an unchecked identity change between the comparison and that +consumption — not by an earlier client-side observation the boundary later trusts. +That consuming boundary differs by operation and the obligation is identical at +each: for `repair.commit` it is the commit mutation boundary, for `repair.push` +the push receiving/mutation boundary, and for `repair.change_request` — which +mutates no ref — the change-request/provider creation boundary at which the source +and target identities are actually consumed. An operation whose authorized +effective-identity relationship cannot be held through to its consuming boundary +must fail closed. + +This document states the required invariant, not an implementation: it names no +git command, lock, or transaction mechanism, and it claims no more atomicity than +the eventual executor's own primitives can actually provide. + +Writing that obligation down adds no runtime git authority to C1 and grants no new +authority anywhere: C1 gains no git invocation, no filesystem access, no +subprocess, and no network, and remains pure TypeScript. + ## Operations are structured, not named A generic action name is not sufficient for Cockpit write authority. There is no @@ -196,9 +358,14 @@ authority cannot be checked against an exact operand has no place in the model. | `repair.push` | ref, non-force | ref is exactly the repair branch and the push is not forced | | `repair.change_request` | source ref, target ref | repair branch → protected parent ref | -Every ref operand is read through the same canonical branch-ref reader the job -envelope uses, so "exactly the repair branch" is a claim about a branch and not -about a spelling. +Every ref operand — the `repair.commit` and `repair.push` ref, and the +`repair.change_request` source and target refs alike — is read through the same +canonical branch-ref reader the job envelope uses, so "exactly the repair branch" +is a claim about a canonical ref name and not about a caller's chosen spelling. It +is not a claim about the effective ref-name referent that name reaches in a +repository, about which ref a commit or push would actually advance, or about the +effective direction of a change request — all of which only the later trusted +execution boundary can establish. `repair.change_request` is the **only** operation that may name the protected parent ref, and only as a change-request *target*. Opening a change request @@ -404,6 +571,20 @@ actually holds: Forgery therefore buys nothing, and a permit widens no authority — it records authority already derived from trusted configuration. +A permit is also **not a repository-safety finding**. That a ref operand — a +`repair.commit` or `repair.push` ref, or a `repair.change_request` `sourceRef` or +`targetRef` — passed C1's canonical syntax validation says nothing about the +effective ref-name referent it reaches in the repository the operation would +touch, about which ref a commit or push would actually advance, or about the +effective direction of a change request, so a permit must never be read as proof +that repository-level ref identity, the effective mutation target, or the +change-request direction is safe. The trusted execution boundary that acts on a +permit binds the effective identity, resolves it, and enforces it at the boundary +that actually consumes that identity — the mutation/receiving boundary for a +`repair.commit` or `repair.push`, and the change-request/provider creation boundary +for a `repair.change_request` — and fails closed; see *What canonical ref names do +and do not prove* above. + ### Single use Single use is stated structurally. `singleUse` is typed as the literal `true` and @@ -525,8 +706,14 @@ C1 implements none of that workflow. It encodes only the minimal authority invariants that stop a later layer from bypassing the quarantine by accident: - The protected parent ref is never a write target of any operation, under any - spelling: refs are canonical everywhere, so an alias of the parent cannot be - presented as a different branch. + *spelling*: refs are canonical everywhere, so a caller cannot present a textual + alias of the parent as a different branch. Repository-dependent aliasing — a + canonical repair ref that is symbolic to the parent, a worktree `HEAD` or push + destination whose effective target is the parent, a change-request source or + target whose effective direction is reversed, or an effective ref that changes + after a pre-check — is not visible to a pure string boundary, and is the later + trusted execution boundary's to bind, resolve, or reject before it acts on any + authority a permit records. - Filesystem-shaped operations are bound to the repair worktree, so an edit cannot land in the parent's checkout. - The stacked change request must run from the repair branch to the protected diff --git a/src/domain/repair-job.ts b/src/domain/repair-job.ts index a0c03fe..0705e34 100644 --- a/src/domain/repair-job.ts +++ b/src/domain/repair-job.ts @@ -462,17 +462,18 @@ function endsWithDotLockSuffix(value: string, start: number, end: number): boole * * So C1 does not resolve; it **narrows**. Exactly one spelling is accepted, and * every other spelling of the same branch is refused as malformed rather than - * silently treated as a different ref. The property that buys is precise: + * silently treated as a different ref. The property that buys is precise, and it + * is a property of ref *names* rather than of repository state: * - * > Two accepted refs denote the same branch if and only if they are equal - * > strings — up to the ASCII-case caveat below. + * > Two accepted refs are the same canonical ref name if and only if they are + * > equal strings — up to the ASCII-case caveat below. * * That is what makes `repairBranch !== protectedParentRef` mean "two different - * branches" instead of "two different strings". Before this, a job configured - * with `protectedParentRef: 'refs/heads/main'` and `repairBranch: 'main'` was - * accepted as a quarantined repair, and a `repair.push` naming `main` passed - * every check and produced an `ExecutionPermit` whose ref denotes the protected - * branch. + * canonical ref names" instead of "two different strings", which is what closes + * caller-controlled textual aliasing. Before this, a job configured with + * `protectedParentRef: 'refs/heads/main'` and `repairBranch: 'main'` was accepted + * as a quarantined repair, and a `repair.push` naming `main` passed every check + * and produced an `ExecutionPermit` whose ref denotes the protected branch. * * Accepted, and nothing else: * @@ -498,13 +499,65 @@ function endsWithDotLockSuffix(value: string, start: number, end: number): boole * rewritten. * * **What this does not prove**, and must not be claimed to: that two unequal - * accepted refs are two distinct refs *on every filesystem*. Git stores loose - * refs as files, so on a case-insensitive filesystem `refs/heads/Main` and - * `refs/heads/main` can be one ref while comparing unequal here. That residue is - * handled where it matters — {@link mayDenoteSameBranchRef} compares the job's - * two configured refs case-insensitively, so such a pair is refused as - * configuration — rather than pretended away here. C1 observes no filesystem and - * cannot do better than refuse the ambiguous case. + * accepted refs are two distinct branch targets in a repository. What is proved + * is a structural canonical ref-name representation and this module's own + * documented string-comparison rules; repository-resolved ref identity is not + * established here. Two residues stand: + * + * - **Symbolic refs.** A repository may hold a canonical-looking ref — say + * `refs/heads/repair` — that is itself a symbolic ref to `refs/heads/main`. + * Whether such a ref exists, and what it dereferences to, is repository state + * at the moment the name is used. C1 does not detect that a ref is symbolic, + * does not resolve a symbolic ref's target, and cannot tell whether two + * distinct canonical names ultimately dereference to one repository target. + * - **Filesystem identity.** Git stores loose refs as files, so on a + * case-insensitive filesystem `refs/heads/Main` and `refs/heads/main` can be + * one ref while comparing unequal here. That residue is handled where it + * matters — {@link mayDenoteSameBranchRef} compares the job's two configured + * refs case-insensitively, so such a pair is refused as configuration — rather + * than pretended away here. C1 observes no filesystem and cannot do better + * than refuse the ambiguous case. + * + * The symbolic-ref residue is not narrowable from a string at all, and neither is + * the effective target a mutation would reach. A later trusted repository/Git + * execution boundary must, before acting on any authority an `ExecutionPermit` + * records, resolve the requested ref's effective ref-name referent against the + * actual repository — the terminal ref reached through a symbolic-ref chain, not + * commit-object identity, since a fresh repair branch may legitimately share the + * protected parent's commit OID — and bind each effective identity the operation + * acts on to the authorized repair ref. A terminal ref-name spelling is not by + * itself repository ref identity: where a repository applies its own ref-identity + * semantics — for instance a case-insensitive ref store treating `refs/heads/Main` + * and `refs/heads/main` as one ref — terminal names that are not equal strings may + * still be the same repository ref, so the boundary must judge sameness or + * distinctness under that repository's actual ref-identity semantics rather than by + * terminal-name string (in)equality alone, and fail closed where the required + * distinctness cannot be safely proven. A commit advances one mutation target: the + * worktree's effective `HEAD` referent, the ref it will actually advance. A push + * binds two effective identities in distinct roles — its destination ref, the + * receiving/mutation target the push advances, and its source ref, the input the + * push consumes to select what is sent — each of which must be the authorized + * repair ref, so an absent (deletion) or redirected source is refused. It must + * **fail closed** — refusing the + * operation — if any such effective identity is or dereferences to the + * protected parent, if it changes between comparison and update, or if it cannot + * be safely established; a resolve-then-act pre-check is not itself atomic, so the + * invariant is enforced at the boundary that actually consumes each identity — the + * mutation/receiving boundary for a commit or push, and the + * change-request/provider creation boundary for a change request. That refusal is + * bound to the operand's role rather than being a blanket ban on the protected + * parent's identity: a `repair.change_request` `targetRef` is *required* to reach + * the protected parent ref, and fails closed when it reaches anything else. Because + * that operation mutates no ref, the identity it consumes is bound at its provider + * create/update request: the effective source must remain the authorized repair ref + * and the effective target the protected parent ref through to that request. The + * provider may resolve those ref names at its own boundary, but must not let + * re-resolution or ambient repository state substitute a materially different + * effective identity for either end, and the boundary must fail closed if the + * authorized relationship cannot be maintained or shown equivalent there. + * Nothing here acquires git invocation, filesystem access, a subprocess, or + * network to decide it. See `docs/architecture/C1-repair-job-authority.md`, + * "What canonical ref names do and do not prove". * * The value is returned exactly as supplied, or not at all. No normalisation, * no prefixing, no case folding: a boundary that repaired the spelling would be @@ -557,7 +610,7 @@ export function readCanonicalBranchRef(value: unknown): string | null { } /** - * Could these two accepted branch refs denote the same branch? + * Could these two accepted canonical ref names collapse into one ref? * * Both arguments are already-validated canonical refs, so this is exact string * equality widened by one conservative allowance: ASCII case. Git stores loose @@ -567,9 +620,20 @@ export function readCanonicalBranchRef(value: unknown): string | null { * as possibly-identical and the job that configures one is refused. * * Conservative in the safe direction: it answers `true` — refuse — whenever it - * cannot establish that the two refs are distinct, including for a character it - * could not read at all. Only ASCII case is folded, because the canonical reader - * admits no other alphabet. + * cannot establish that the two *names* are distinct, including for a character + * it could not read at all. Only ASCII case is folded, because the canonical + * reader admits no other alphabet. + * + * A `false` result means only that the two names are distinct under this rule. + * It is **not** a finding that they denote distinct targets in a repository: this + * compares strings and resolves nothing, so a canonical name that is a symbolic + * ref to the other still answers `false` here, and — since two different branch + * refs may legitimately share one commit object — commit-object equality is not + * the question either. Repository-resolved identity — whether the effective + * referents reached by resolving symbolic-ref chains are the same or distinct + * under the repository's own ref-identity semantics, which a terminal ref-name + * spelling alone does not settle — is the later trusted repository/Git execution + * boundary's to establish; see {@link readCanonicalBranchRef}. */ function mayDenoteSameBranchRef(left: string, right: string): boolean { if (left === right) { @@ -711,16 +775,23 @@ export interface RepairJobAuthorization { * The isolated repair branch, in the canonical `refs/heads/` spelling * {@link readCanonicalBranchRef} defines. * - * Must denote a different branch from {@link protectedParentRef}. A job whose - * repair branch is the protected parent ref is not a quarantined repair; it is - * a direct write to protected history wearing a repair job's name, and it is - * rejected as malformed configuration rather than evaluated. + * Must be a different canonical ref name from {@link protectedParentRef}. A + * job whose repair branch is the protected parent ref is not a quarantined + * repair; it is a direct write to protected history wearing a repair job's + * name, and it is rejected as malformed configuration rather than evaluated. * - * "Different branch", not "different string": both refs are read through the - * canonical reader, so an alternate spelling of the protected parent — `main`, - * `heads/main` — cannot pass as an isolated repair branch, and a pair that - * differs only by ASCII case is refused too because a case-insensitive + * "Different branch ref", not "different string": both refs are read through + * the canonical reader, so an alternate spelling of the protected parent — + * `main`, `heads/main` — cannot pass as an isolated repair branch, and a pair + * that differs only by ASCII case is refused too because a case-insensitive * filesystem can store the two as one loose ref. + * + * That closes caller-controlled textual aliasing only. It does not establish + * that the two names resolve to distinct targets in a repository — a canonical + * repair ref that is symbolic to the parent is invisible to a pure string + * boundary — which the later trusted repository/Git execution boundary must + * resolve or reject before it acts on any authority a permit records. See + * {@link readCanonicalBranchRef}. */ readonly repairBranch: string; /** The isolated repair worktree. Filesystem-shaped operations are bound to it. */ @@ -886,11 +957,13 @@ export function readRepairJobAuthorization(job: RepairJobAuthorization): RepairJ if (findingHeadSha === null) { append(invalidFields, 'findingHeadSha'); } - // The repair branch must be a *different branch* from the protected parent + // The repair branch must be a *different branch ref* from the protected parent // ref, or the isolation the whole quarantine depends on does not exist. Both - // refs are canonical here, so unequal strings are different branches — except - // for the ASCII-case pair a case-insensitive filesystem can collapse into one - // loose ref, which `mayDenoteSameBranchRef` refuses as well. + // refs are canonical here, so unequal strings are different canonical ref names + // — except for the ASCII-case pair a case-insensitive filesystem can collapse + // into one loose ref, which `mayDenoteSameBranchRef` refuses as well. This is a + // name-level check: repository-resolved identity, including a canonical ref + // that is symbolic to the parent, is the later trusted execution boundary's. if ( repairBranch === null || (protectedParentRef !== null && mayDenoteSameBranchRef(repairBranch, protectedParentRef))