Repository navigation
Commit 86ae47e
ci(fleet-patrols): pin each schedule cron to its job if: twin in the watchdog gate (#26653)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9DQWWBhDUtPvKnwBReoUmbC)_
Fixes #26526
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:`check-platform-checklist-watchdog` 新增第 7 条:`fleet-patrols.yml`
里每个 `on.schedule` cron 都必须有某个 job 的 `if:` 与之逐字相同,反之亦然;只改脚本,workflow 只读。
- 为什么改:同一个 cron 写在两处,只改一处时巡查会静默停跑或空跑,CI 毫无红灯;现在任一侧单边修改都会在 lint 的既有步骤里变红。
- 风险与回滚:只是一条新增的静态断言,今天 main 上四对 cron 全部相同,读数绿;回滚即 revert 本 PR。
- 先看:`scripts/check-platform-checklist-watchdog.mjs` 的
`judgeScheduleTwins`,以及 self-test 里 clause 7 那组用例。
- 两分钟验证:`node scripts/check-platform-checklist-watchdog.mjs --self-test
&& node scripts/check-platform-checklist-watchdog.mjs`,预期两行 OK(53 条断言、9
组)。
- 需要你做的:无,席位落地。
## What changes
`scripts/check-platform-checklist-watchdog.mjs` gains **clause 7**,
`judgeScheduleTwins(doc)`, called from `judgeWorkflow`:
- every `on.schedule[].cron` must be named by some **job-level** `if:`
as `github.event.schedule == 'CRON'` (byte-for-byte, because
`github.event.schedule` carries the string exactly as the schedule
spells it);
- every cron a job-level `if:` names that way must be an `on.schedule`
entry;
- a job-level `if:` that reads `github.event.schedule` in any other
spelling (`!=`, the reversed operand order, …) is **refused as
unreadable**, never skipped: a comparison the clause cannot read is one
it cannot check.
The docblock's clause list (six → seven) gains the clause and a `##
Clause 7` section; `--list` and the OK line name it.
No edit to `.github/workflows/**`, no new CI job, required context or
package script: the watchdog already runs as the two lines of
`lint.yml`'s `Platform-checklist watchdog workflow pin` step
(`--self-test`, then the gate), and the clause rides both.
**Forced test edits** (the self-test is in the same file):
- the `GOOD` fixture's `watchdog` job gains a job-level `if:` in the
real file's folded shape — without it the positive control's cron has no
twin and clause 7 fires on it;
- `SELF_TEST_BATTERY_FLOOR` 8 → 9, so deleting the new battery from the
roster reds.
## Tests
New battery `clause 7 — every schedule cron and every job if: cron has
its twin`, floor 9, 9 cases:
- two crons, each named by its own job ⇒ silent (positive control);
- ⭐ a cron added under `on.schedule` with no job `if:` twin ⇒ fires, and
is the only finding;
- ⭐ a job `if:` cron edited with no `on.schedule` twin ⇒ names the job,
and the cron it left behind fires from the other side;
- a cron edited under `on.schedule` alone ⇒ exactly two findings, one
each way;
- a job `if:` cron with no `schedule:` at all ⇒ fires;
- `!=` spelling and the reversed spelling ⇒ refused as unreadable;
- a twin in a STEP-level `if:` is not a job twin ⇒ fires.
Self-test: 44 → 53 assertions across 8 → 9 floored batteries.
## Acceptance notes
- **Mechanism hypothesis (measured, head `9f54e952`):** all FOUR jobs
route by the same spelling, `(github.event_name != 'schedule' ||
github.event.schedule == 'CRON')` — `required-set-patrol` :271,
`platform-checklist-watchdog` :475, `release-coverage-patrol` :857,
`board-snapshot` :1182. No job uses another spelling, so the pin reads
exactly that one and refuses any other loudly rather than widening.
- **Stale workflow prose (governed, not edited):** `fleet-patrols.yml`'s
header (lines 22–25) still says of the two one-sided shapes "neither
shape is red anywhere". After this PR the watchdog gate reds both. The
file is Tier H and outside this card's surface; the sentence wants a
governed follow-up naming clause 7.
- **A third spelling, not pinned:** `PATROL_SCHEDULE` restates two of
the crons in job `env:` (:515 for the watchdog, :983 for
release-coverage), already flagged in the workflow as "Two readers, one
fact". This card's scope is `on.schedule` against job `if:`; the env
twin stays unpinned.
- **Not pinned on purpose:** two jobs naming the same cron (both would
run on it). The card asked for orphans on either side, not multiplicity.
- The card's acceptance names "its existing package.json check": there
is no `check:platform-checklist-watchdog` package script; the existing
check is the `lint.yml` step above, which is what runs the clause.
## Verification
Gate list derived by `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at head `9f54e952` (1 path, +101 /
−4).
<details><summary>Watchdog gate and self-test at 9f54e95</summary>
```text
OK check-platform-checklist-watchdog self-test: 53 assertions across 9 floored batteries, every clause driven by a fixture that makes it fire.
OK .github/workflows/fleet-patrols.yml: schedule + workflow_dispatch present, pull_request_target / merge_group absent, any pull_request trigger paths-filtered to this file and the sibling patrol jobs' own inputs alone, no board write reachable from a pull_request run, every schedule cron twinned with a job `if:`, invoked through `pnpm check:platform-checklist`.
```
</details>
<details><summary>One-sided edits of the REAL fleet-patrols.yml (scratch
probe, not committed)</summary>
```text
real file at HEAD: 0 finding(s)
schedule side only: '19 4 * * *' -> '19 5 * * *': 2 finding(s)
- job `release-coverage-patrol`'s `if:` compares `github.event.schedule` to `19 4 * * *`, which no `on.schedule` entry declares …
- `on.schedule` declares the cron `19 5 * * *` but no job's `if:` compares `github.event.schedule` to it …
job if: side only: board-snapshot '7 2,8,14,20' -> '7 3,9,15,21': 2 finding(s)
- job `board-snapshot`'s `if:` compares `github.event.schedule` to `7 3,9,15,21 * * *`, which no `on.schedule` entry declares …
- `on.schedule` declares the cron `7 2,8,14,20 * * *` but no job's `if:` compares `github.event.schedule` to it …
```
</details>
<details><summary>Derived local gate commands at 9f54e95</summary>
```text
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test :: exit 0
node scripts/check-platform-checklist-watchdog.mjs :: exit 0
node scripts/check-platform-checklist-watchdog.mjs --self-test :: exit 0
node scripts/check-self-test-wired.mjs :: exit 0
node scripts/check-self-test-wired.mjs --self-test :: exit 0
node scripts/check-self-test-workflow-commands.mjs :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:entry-guard :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:pnpm-flag-spellings :: exit 0
pnpm check:ratchet-remedy-authority :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:scripts-symbol-anchors :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:pm-dispatch-gates :: exit 0
dispatch-gates --ran: Run reconciliation — 32 derived, 32 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 32 recorded an exit code and none of them is 3).
Artifact-roster families whose roster lives under scripts/ (outside the derived 32), read too:
node scripts/check-published-list-mirrors.mjs :: exit 0
node scripts/check-published-list-mirrors.mjs --self-test :: exit 0
node scripts/check-dts-references.mjs --self-test :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:i18n-stale-fill :: exit 0
pnpm check:dts-closure :: exit 3 (PREREQUISITE NOT MET: no package build in this worktree; NOT MEASURED, CI runs it)
pnpm check:published-readme-exports :: exit 3 (PREREQUISITE NOT MET: no package build; NOT MEASURED, CI runs it)
```
</details>
ESLint on the file: `pnpm exec eslint --no-inline-config
scripts/check-platform-checklist-watchdog.mjs` exit 0. No package
touched, so no build closure (①) and no package test/typecheck (②);
`skip-changeset`: the diff is a root `scripts/` file of the private root
package and publishes nothing.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmRDtDiaiKtmnnobjEt)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 079deea commit 86ae47e
1 file changed
Lines changed: 101 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
23 | | - | |
| 23 | + | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | | - | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
34 | 36 | | |
35 | 37 | | |
36 | 38 | | |
| |||
92 | 94 | | |
93 | 95 | | |
94 | 96 | | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
95 | 115 | | |
96 | 116 | | |
97 | 117 | | |
| |||
172 | 192 | | |
173 | 193 | | |
174 | 194 | | |
| 195 | + | |
175 | 196 | | |
176 | 197 | | |
177 | 198 | | |
178 | 199 | | |
179 | | - | |
| 200 | + | |
180 | 201 | | |
181 | 202 | | |
182 | 203 | | |
| |||
328 | 349 | | |
329 | 350 | | |
330 | 351 | | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
331 | 392 | | |
332 | 393 | | |
333 | 394 | | |
| |||
378 | 439 | | |
379 | 440 | | |
380 | 441 | | |
| 442 | + | |
381 | 443 | | |
382 | 444 | | |
383 | 445 | | |
| |||
426 | 488 | | |
427 | 489 | | |
428 | 490 | | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
429 | 494 | | |
430 | 495 | | |
431 | 496 | | |
| |||
587 | 652 | | |
588 | 653 | | |
589 | 654 | | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
590 | 686 | | |
591 | 687 | | |
592 | 688 | | |
| |||
646 | 742 | | |
647 | 743 | | |
648 | 744 | | |
| 745 | + | |
649 | 746 | | |
650 | 747 | | |
651 | 748 | | |
| |||
655 | 752 | | |
656 | 753 | | |
657 | 754 | | |
658 | | - | |
| 755 | + | |
659 | 756 | | |
660 | 757 | | |
661 | 758 | | |
| |||
0 commit comments