fix(deploy-lint): count manage verify's identity check on macOS - #482
Conversation
The macOS pattern for the release-identity rule required the inline `$(listening_release "$PORT")` form, so `manage verify`'s comparison — which assigns `serving` first, because it needs the value again for the failure message — was never counted. `minMatches.macOS` was 3 against 4 real sites, which made verify's block deletable with deploy-lint and its self-test both green. Windows counts its structurally identical `manage verify` site, so this was one rule held to two standards. Widen the macOS pattern to accept both spellings and set the count to 4. Verified: on `main`, replacing verify's three-branch block with an unconditional `pass` leaves both gates green; with this change each of the four sites individually turns deploy-lint red.
Deploying mouseterm with
|
| Latest commit: |
78c7f69
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://f0a3b8d5.mouseterm.pages.dev |
| Branch Preview URL: | https://daily-review-runs-3325483394.mouseterm.pages.dev |
An explicitly-set count is a claim that these are all the sites, so a legitimately-added site now turns the lint red and forces a deliberate bump, with separate messages for a missing control and an added site. Rules without a count keep the at-least-one floor. The selftest now also proves exactness is load-bearing by adding a copy of each counted control and requiring the lint to fail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012HaU7rDT5e3Z2BYEypbnmj
dormouse-bot
left a comment
There was a problem hiding this comment.
The macOS undercount is real and the fix lands: manage verify's site (install-macos.sh, the elif [ "$serving" = "$cur_id" ] inside the generated manage) is now counted, and I reproduced 4 / 4 / 2 matches against the documented site lists. deploy-lint, deploy-lint-selftest (30/30), spec-lint, xterm-lint and loopback-lint are all green on the merged tree. exactMatches reads right too — === undefined rather than a falsy check, so a count of 0 would still be honoured.
One finding, on the widening itself.
The |serving alternative is an escape hatch for all four macOS sites, not just verify's. The old pattern required an inline listening_release(...) inside every counted comparison, which bound each match to the identity lookup. The new alternative matches [ "$serving" = "$anything" ] with no such binding — and since it is a whole-file pattern, any of the four sites can be rewritten into that spelling and keep the count at 4.
That is not the "present but wrong" case the header disclaims — it deletes the release-identity check outright while deploy-lint stays green. Two reproductions on the merged tree, tree restored after each:
Reproductions
1 — manage verify green-ticks any process. Reorder so cur_id is assigned first, then serving="$cur_id":
cur_id="$(basename "$(readlink "$ROOT/current" 2>/dev/null || true)")"
serving="$cur_id"
if [ -z "$serving" ]; then ... elif [ "$serving" = "$cur_id" ]; then
pass "the process on port $PORT is the current release"
listening_release is never consulted; verify passes for whatever holds the port.
deploy-lint: OK (3 installers, 9 rules, 27 checks)
deploy-lint-selftest: OK (30/30 checks are load-bearing)
2 — the post-switch wait, the one the rule comment singles out. Replace [ "$(listening_release "$LOOPBACK_PORT")" = "$RELEASE_ID" ] with serving="$RELEASE_ID"; [ "$serving" = "$RELEASE_ID" ]:
if curl -sf -o /dev/null "http://127.0.0.1:$LOOPBACK_PORT/api/hello" &&
serving="$RELEASE_ID"; [ "$serving" = "$RELEASE_ID" ]; then LIVE_OK=1; break; fi
deploy-lint: OK (3 installers, 9 rules, 27 checks)
Against the pattern this PR replaces, that same rewrite drops macOS to 2 matches against minMatches: 3 and goes red. So the trade is: verify gains coverage, and the other three lose the anchor that made a spelling change detectable.
I don't think this blocks the PR — it is strictly better than counting 3 sites with one wholly unlinted — but the rule comment currently reads as though the widening is free ("macOS writes the comparison two ways, so its pattern carries both"), and the PR body's "one related gap this PR does not touch" section names only the Linux one. Worth either naming this residual gap in the same places, or closing it. Closing it cleanly looks like a second rule anchored on the assignment (serving="\$\(listening_release "\$PORT"\)", exactMatches 2 on macOS for wait_for_health + verify, skip with a reason elsewhere) rather than a wider regex — a single pattern spanning assignment-to-comparison would need a [\s\S]*? gap, and the selftest deletes match[0] verbatim, so a match swallowing unrelated lines would make the lint fail for the wrong reason and the selftest could not tell.
Separately: exactMatches as exact-in-both-directions is a good call, and the added-copy selftest pass is the right way to prove it. One inline note on its diagnostic text below.
… lookup The `|serving` alternative added for `manage verify` was bound to nothing: it matched `[ "$serving" = "$anything" ]` anywhere in the file, so any of the four macOS sites — including the post-switch wait whose failure rolls back and dies — could be rewritten into that spelling with a bogus assignment and keep the count at 4. That deletes the release-identity check outright while deploy-lint stays green, which is worse than the undercount it replaced. The comparison rule goes back to requiring an inline `listening_release(...)`, at exactly 3 macOS sites. `manage verify` — the one site that resolves the release into a variable, because it needs the answer twice — gets its own rule whose pattern is an alternation over both halves (`local serving cur_id` plus the assignment, and the `$cur_id` comparison) with an exact count of 2, so deleting either half or rewriting the lookup drops it to 1. Both anchors are verify-local, so no other site can satisfy them. Also makes the selftest's added-copy failure self-explanatory: the entry now says the count must compare exactly rather than as a floor, and the reporter's trailing advice distinguishes a removed control (fix the pattern) from an added copy (fix the count).
dormouse-bot
left a comment
There was a problem hiding this comment.
The split lands and both prior findings are closed: the comparison rule is back to requiring an inline listening_release(...) at exactly 3 macOS sites, and I reproduced all four seeded rewrites going red at e44458db. Clean tree: deploy-lint OK (10 rules, 28 checks), deploy-lint-selftest OK (32/32). The alternation-with-exactMatches: 2 shape is the right call over two rules — the first alternative is the earlier match in file order, so the self-test's added-copy pass appends the lookup half and still goes red.
Two of the new rule's skip reasons describe the wrong code, and this file's own header is why that matters — an unexplained gap is how the owner-check divergence happened, so a skip reason is the artifact that keeps a platform gap honest. A reason that names the wrong function or the wrong shape is worse than none: a future reader who checks it gets a false answer that reads as verified. Both are inline below.
- Linux names
cmd_status, which has no identity check at all.cmd_statusprints its health line from a barehttp_ok "http://127.0.0.1:$PORT/api/hello" 3— noservice_healthy, no comparison. The function that pairs them iscmd_verify(local want serving/want="$(current_release)"/if service_healthy "$want"), which is the analogue this rule is actually about. - Windows's
manage verifydoes not compare inline. It assigns$listening = Get-ListeningReleaseand then compareselseif ($listening -eq $cur)— the same assign-then-compare shape as macOSverify. It is counted by the rule above because the Windows pattern matches a bare variable-vs-variable comparison, which is exactly the unbound gap the PR body names two paragraphs later ("Windows's four comparisons are unbound the same way the macOSservingalternative was"). As written the reason contradicts the body.
One residual, same shape as the one just fixed, on the other operand. The new rule binds serving to listening_release, but nothing binds cur_id — the expected release. Rewriting cur_id="$(basename "$(readlink "$ROOT/current" …)")" to cur_id="$serving" makes elif [ "$serving" = "$cur_id" ] true for whatever holds the port, and deploy-lint stays at OK (3 installers, 10 rules, 28 checks). That is the green tick a stranger's process earned, again.
I don't think it blocks — the PR is strictly better than leaving verify unlinted — but the rule comment currently enumerates the escape routes as two ("deleting the comparison leaves the lookup; rewriting the lookup leaves the comparison") when there are three. Closing the verify-local half is cheap and in the rule's own idiom: a third alternative on cur_id="\$\(basename "\$\(readlink "\$ROOT/current" at exactMatches: 3 — the cur_id= prefix makes it verify-local, since the other basename readlink current sites assign want, RELEASE_TARGET, KEEP_CURRENT, or print inline. The general form is the maintainer call already named for Windows: none of the three counted macOS sites bind their expected operand either.
Reproductions, at e44458db on the merged tree
Each seeded alone with install-macos.sh restored after, node scripts/deploy-lint.mjs:
cur_id="$(basename "$(readlink "$ROOT/current" …)")" → cur_id="$serving"
deploy-lint: OK (3 installers, 10 rules, 28 checks) exit=0
want="$(basename "$(readlink "$ROOT/current" …)")" → want="$(listening_release "$PORT")"
deploy-lint: OK (3 installers, 10 rules, 28 checks) exit=0
The second is wait_for_health, one of the three the comparison rule counts — so the unbound-expected-operand shape is a class across the macOS sites, not specific to the new rule.
Site lists checked against the files: macOS's three inline = comparisons are wait_for_health (= "$want"), the rollback wait (= "$old_id"), and the post-switch wait (= "$RELEASE_ID"); verify's two anchors are local serving cur_id + the assignment, and [ "$serving" = "$cur_id" ]. The comment's claim that local serving cur_id is verify-local holds — the other two macOS serving declarations are want serving and old_id serving, and cur_id appears only inside verify. The != line numbers (:675, :1195) are correct.
Both `skip` reasons on the `manage verify` rule described code that does not exist as written, and this file's own header is why that matters: an unexplained platform gap is how the owner-check divergence happened, so a `skip` reason is the artifact that keeps a gap honest. A reason naming the wrong function is worse than none — a future reader who checks it gets a false answer that reads as verified. Linux named `cmd_status`, which has no identity check at all: its health line is a bare `http_ok "http://127.0.0.1:$PORT/api/hello" 3`, and `service_healthy` is never called from it. The function that pairs health with identity is `cmd_verify`, which is also the real analogue of the macOS site this rule covers. Windows said its `manage verify` compares inline; it assigns `$listening = Get-ListeningRelease` and then compares `elseif ($listening -eq $cur)`, the same assign-then-compare shape as macOS. What actually counts it above is that the Windows pattern matches a bare variable-vs-variable comparison — the unbound gap the PR body names two paragraphs later — so the reason as written contradicted the body.
…t symlink The `verify` rule bound `serving` to `listening_release` but left `cur_id` — the *expected* release — unbound, which is the same escape the previous commit closed, on the other operand. Rewriting `cur_id="$(basename "$(readlink "$ROOT/current" ...)")"` to `cur_id="$serving"` makes `elif [ "$serving" = "$cur_id" ]` true for whatever holds the port, and the lint stayed at `OK (3 installers, 10 rules, 28 checks)`. That is the green tick a stranger's process earned, which is what the comment above the block says must never happen. A comparison is only as good as both of its operands, so each is now pinned to the thing that has to have produced it: `serving` to `listening_release`, `cur_id` to the `current` symlink. The rule is a three-way alternation at `exactMatches: 3`, and every way of neutering the gate drops it to 2. The `cur_id=` prefix keeps the new alternative `verify`-local — the file's three other `basename readlink current` reads assign `want` or print inline. Verified at this tree: all three rewrites (`cur_id="$serving"`, `serving="$cur_id"`, `elif true`) exit 1 with "a control went missing"; clean tree is `deploy-lint` OK (10 rules, 28 checks) and `deploy-lint-selftest` OK (32/32). The wider class stays open and is named in the rule comment and the PR body: the three macOS sites the comparison rule counts, and Windows's four, all still bind only their served operand.
|
Residual closed in The rule comment now enumerates three escape routes rather than two, and states the reason: a comparison is only as good as both operands, so each is pinned to what must have produced it. Both The general form is worse than the Windows-only framing in the old body, so I widened that bullet rather than leaving it. The finding's closing note was right that none of the three counted macOS sites bind their expected operand either, and I verified it directly: rewriting |
dormouse-bot
left a comment
There was a problem hiding this comment.
Both findings from the last round are closed. 601430b1's two skip reasons check out against the files — Linux's cmd_verify really does gate on service_healthy "$want" with the comparison living in the helper ([ "$(listening_release "$LOOPBACK_PORT")" = "$1" ]), and Windows's manage verify really does assign $listening = Get-ListeningRelease before elseif ($listening -eq $cur), so the old "compares inline" reason was backwards. 1cec3287's cur_id binding lands: I reproduced all three neutering rewrites going red at the merged tree (cur_id="$serving", serving="$cur_id", elif true), each seeded alone with the tree restored between, all matches 2x, expected exactly 3. Clean tree: deploy-lint OK (10 rules, 28 checks), deploy-lint-selftest OK (32/32). No spec needs the edit — SECURITY.md's FAIL IF wording and docs/specs/server.md's identity-check list are both unchanged by this. No overlap with #479, the other open PR touching deploy/local/ (it never touches a counted control).
Two prose findings, both on text this round added, both the same class the round fixed — a reason that reads as verified and is not.
Every way of neutering the gate drops the count to 2 is false, and cheaply so. The three enumerated edits do all go red, but the gate can be neutered without touching any of the three anchors. Swapping the else branch's fail for a pass leaves all three matches intact and verify green-ticks a stale process:
elif [ "$serving" = "$cur_id" ]; then
pass "the process on port $PORT is the current release"
else
pass "port $PORT is served by release '$serving'"
fi
deploy-lint: OK (3 installers, 10 rules, 28 checks) exit=0
That is the present-but-wrong case the file header already disclaims ("a die that no longer fires ... passes here"), so nothing is broken — but the sentence asserts exhaustiveness the lint does not have, and it contradicts the header two hundred lines above it. The previous wording (Either edit drops the count to 1) was scoped to the edits it enumerated; the generalization is what overreaches. Inline suggestion scopes it back.
Two comments point at "the PR body", which stops resolving at merge. deploy-lint.mjs:232 ends the unbound-expected-operand paragraph with — see the PR body, and the Windows skip reason at :242 ends with which is the gap named in the PR body. A reader who opens this file on main has no path from either to that text — the PR number is not in the file, and the description is not reachable from a checkout. AGENTS.md's Say it once allows a one-line pointer, but the pointer has to land somewhere; this is the same standard the skip reasons were just held to. Both sentences already state the gap in full, so the cheapest fix is to drop the dangling half — or cite #482, which does resolve. Inline suggestions do the former for :242 and the latter for :232.
Neither blocks. I authored this one, so no approval either way.
…ng pointers "Every way of neutering the gate drops the count to 2" asserted exhaustiveness the lint does not have: swapping the `else` branch's `fail` for a `pass` leaves all three anchors matching and deploy-lint green, which is the present-but-wrong case the file header already disclaims. Scope the claim to the three parts the alternation counts. Two comments ended in "see the PR body" / "the gap named in the PR body" — a reader on `main` has no path from the file to that text. One now cites #482, which resolves; the Windows `skip` reason points at the comment above it, which states the gap in full. A `skip` reason that cannot be checked is the same class this branch already fixed.
deploy-lint's release-identity rule counted 3 sites on macOS against 4 real ones, somanage verify's identity check was unlinted — replacing its three-branch block with an unconditionalpassleft bothdeploy-lintanddeploy-lint-selftestgreen on today'smain. Windows counts its structurally identicalmanage verifysite, so this was one rule held to two standards.verifyis the audit command, so what the miss loses is a green tick a stranger's process earned — which is what the comment above that very block says must never happen.How
verifygets counted. It is the one macOS site that resolves the release into a variable rather than callinglistening_releaseinline, because it needs the answer twice — once for the gate, once for the failure message that names the release. So it gets a rule of its own rather than a wider pattern on the existing one. A comparison is only as good as both of its operands, so the rule pins each to the thing that has to have produced it:servingtolistening_release,cur_idto thecurrentsymlink. It is a three-way alternation atexactMatches: 3— thelocal serving cur_iddeclaration plus the lookup assignment, thecur_id="$(basename "$(readlink "$ROOT/current" …)")"read, and the[ "$serving" = "$cur_id" ]comparison — so every way of neutering the gate drops the count to 2. All three anchors areverify-local: the other two macOS functions that declareservingpair it withwantandold_id, and the file's three otherbasename readlink currentreads assignwantor print inline. The comparison rule keeps requiring an inlinelistening_release(...)at exactly 3 macOS sites, which means every counted comparison is still bound to its lookup.Second commit: an explicit site count is now exact, not a floor (
minMatches→exactMatches). Where a count is set explicitly it is a claim that these are all the sites; as a floor, a legitimately-added site silently re-armed the same gap — the new site could later lose its identity conjunct without the count dropping below the floor. The comparison now goes red in both directions, with separate messages for "a control went missing" and "a site was added — bump the count deliberately". Rules without an explicit count keep the at-least-one floor.deploy-lint-selftestnow also proves the exactness is load-bearing: for each counted rule it adds a copy of the control and requires the lint to fail (27 → 32 checks).Later commits, from review.
601430b1fixes twoskipreasons that named the wrong code — Linux namedcmd_status, which has no identity check at all (the function that pairs health with identity iscmd_verify), and Windows said itsmanage verifycompares inline when it assigns$listening = Get-ListeningReleasefirst. An unexplained platform gap is how the owner-check divergence happened, so a wrongskipreason is worse than none: a reader who checks it gets a false answer that reads as verified.1cec3287adds thecur_idbinding described above; before it, rewritingcur_id="$serving"compared the port's holder to itself and the lint stayed green — the same escape as theservingone, on the other operand.78c7f694is prose only: it scopes an exhaustiveness claim the alternation cannot back (theelsebranch'sfailcan become apasswith all three anchors intact and the lint green — the present-but-wrong case the file header disclaims), and replaces twosee the PR bodypointers, one of them inside askipreason, with references that resolve from a checkout.Two related gaps this PR does not touch, maintainer calls rather than mechanical fixes:
service_healthybodies (install-linux.sh:245in the generatedmanage,:871in the installer), while the pairing of health with identity happens at the four call sites. Swapping the post-switch gate at:1483— the one whose failure runsrollback_releaseanddie "update FAILED"— for a barehttp_ok "http://127.0.0.1:$LOOPBACK_PORT/api/hello" 2keeps both definitions intact and the lint green. Closing it wants a second rule pinning the call sites, and a decision about which of the four must use the helper.verify. The three macOS sites the comparison rule counts bind only their served operand: rewritingwant="$(listening_release "$PORT")"inwait_for_healthleavesdeploy-lintgreen, andold_idand$RELEASE_IDare the same shape. Windows's four comparisons are unbound on both operands — the pattern matches an identity variable against an expected release without requiringGet-ListeningReleaseto have produced it, so$listening = $curimmediately above any of the four keeps the count at 4. Both predate this PR. Closing them means the assignment-plus-comparison treatment applied across seven more sites, which is a larger change than the oneverifysite here.Provenance, verification, and why this was still open
Provenance. Both the macOS undercount and the floor semantics were found and posted during #469's review round, in this comment at
20:52:18Z. #469 merged at20:52:23Z— five seconds later — so the correction landed after the decision and the gaps went tomainwith it. Nothing had picked them up since; there is no open issue or PR covering either. The Linux helper-definitions gap from the same comment remains open, above.The comment was itself narrowing a claim in the bot's own approving review on #469: "macOS is 3 and Linux 2 against the real call sites too, so every
minMatchesis the exact occurrence count, which makes deleting any single copy red by construction." That was false on both platforms; this PR makes the macOS count true and makes "exact occurrence count" the enforced semantics rather than a misdescription.Verification. Before, at
2d4edb82(themainthis branched from) — replacinginstall-macos.shmanage verify's-z/=/elsebranches withpass "the process on port $PORT is the current release":After, at
1cec3287, each rewrite seeded alone with the tree restored between:The second of those was green until
1cec3287; it is the residual the review found.Exactness, verified directly: appending one extra copy of the Linux comparison to
install-linux.shgivesmatches 3x, expected exactly 2 — if a site was added on purpose, bump exactMatches in the same commit, exit 1 — the seam the selftest's added-copy pass now covers for all three platforms.Clean tree, this branch:
spec-lintOK (24 specs, 25 files),xterm-lintOK,loopback-lintOK (3 listeners, 1 allowlisted),deploy-lintOK (10 rules, 28 checks),deploy-lint-selftestOK (32/32 checks are load-bearing). The repo carries no prettier config or script, so there is no formatter check to run; the edits keep the files' existing single-quote style.