myxce/.wolf/bugs/bug-1748.md
Rongsong Shen d26ff8870d
feat(buglog): one Markdown record per bug, checked against a record standard
The buglog was one 27K-line JSON file whose fields covered only symptom,
root cause and fix, as free text: 2% of entries named the code version a
bug was found on, 12% the build profile, 36% a way to reproduce, and 58% of
the file was OpenWolf hook auto-detections ("Missing guard clause",
"Function not marked async" on .md files).

- .claude/skills/buglog/bug-record.md: the record standard. Eight
  sections (Summary as the title, Description, Environment, Reproduction;
  Root cause, Fix, Verification, References once fixed), each with the
  content it must contain -- e.g. Environment needs the OS, the libc the
  failing binary loads (from its ELF interpreter: glibc 2.42 in the nix
  shell vs 2.40 on the host), toolchain, build profile and commit.
- .wolf/bugs/<id>.md: 500 records migrated from buglog.json; every field
  verified to round-trip. They carry legacy: true and an explicit "not
  recorded" where the old log had no such field; component derived from
  file paths, severity from keywords (else unknown), fix_commits from
  hashes in status_note/fix that git resolves (91 of 463 fixed).
- .wolf/buglog-archive.json: the 710 hook auto-detections, verbatim.
- .wolf/buglog.json is now an index generated from the records, keeping
  the fields OpenWolf's hooks and `openwolf bug search` read.
- scripts/buglog.py rewritten: new (template + Environment collected from
  the machine and --binary), check, status (refuses a status the record
  doesn't support; fixed needs Root cause, Fix, Verification and
  --commit hashes git resolves), note, recur, set, show, list, triage,
  env, index. 29 unittest cases in scripts/tests/test_buglog.py.
- scripts/git-hooks/pre-commit runs `check --staged`: incomplete staged
  records, a legacy record hand-changed to fixed without the fix-time
  sections, a new record claiming legacy: true, or a stale index refuse
  the commit.
- scripts/gitea-sync.py reads the records: issue body = the record's
  sections plus linked fix commits (origin remote, GITEA_COMMIT_REPO to
  override), and a body hash so an issue's text is refreshed when the
  record changes.
- bug-1866 rewritten to the full standard as the worked example.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 03:40:11 +00:00

38 lines
5.1 KiB
Markdown

---
id: "bug-1748"
status: "fixed"
status_updated: "2026-09-24"
status_note: ""
component: "controller"
severity: "unknown"
timestamp: "2026-09-01T02:00:00Z"
last_seen: "2026-09-19T09:00:00Z"
occurrences: 1
files: ["src/controller/resync.c"]
tags: ["worker-rejoin-resync", "task-3.2", "resync-c", "requirement-4.4", "requirement-4.8", "resync_apply_running_entry", "record_job_reconcile_assign_fact", "guard-logic", "short-circuit", "review-caught", "test-baked-in-bug"]
related: ["bug-1747", "bug-1697", "bug-1631"]
legacy: true
---
# bug-1748: worker-rejoin-resync task 3.2 (resync_apply_running_entry, resync.c), round-2 independent review…
## Description
worker-rejoin-resync task 3.2 (resync_apply_running_entry, resync.c), round-2 independent review: the round-1 fix for the PEND/unassigned-job regression (bug-1747-adjacent, not itself in bug-1747) introduced a NEW Requirement 4.4 violation. The round-1 guard `if (!resync_job_is_terminal(job->job.state) && resync_job_assigned_to_host(job, w->host.name)) { no-op } else { record_job_reconcile_assign_fact(...) }` short-circuits the `&&` to false whenever job->job.state IS terminal, falling into the `else` branch and calling record_job_reconcile_assign_fact() on an ALREADY-TERMINAL (DONE/EXIT) job -- which has no backward-transition guard of its own and unconditionally sets job->job.state = JOB_STAT_RUN, clobbering a terminal job back to RUN. Confirmed live and, worse, CERTIFIED AS CORRECT by the round-1 implementation's own test: test_apply_report_all_five_outcomes_updates_job_table seeded j_extra at JOB_STAT_DONE with a comment claiming this 'satisfies Req 4.8's trigger,' then asserted j_extra->job.state == JOB_STAT_RUN afterward and the suite reported PASS -- the test baked the bug in as an expected outcome instead of catching it.
## Environment
_Not recorded (migrated from buglog.json)._
## Reproduction
_Not recorded (migrated from buglog.json)._
## Root cause
Requirement 4.8's actual trigger condition is 'no existing non-terminal record of that job assigned elsewhere' (requirements.md AC 4.8; record_job_reconcile_assign_fact's own doc comment and design.md's Service Interface Preconditions: 'job is non-terminal ... or has no existing non-terminal assignment'). Terminal-vs-non-terminal is NOT the axis that decides assign-eligibility on its own -- Requirement 4.4 ('shall not overwrite that state with a less final or older report') independently and unconditionally forbids ever assign-routing a terminal job, regardless of host. The round-1 guard conflated two orthogonal conditions (terminal-ness, and host-assignment) into a single boolean expression via `&&`, which is only correct when BOTH must jointly hold for the no-op case -- but Req 4.4's terminal exclusion needs to apply UNCONDITIONALLY, independent of the host check, not only when the host check also happens to be true. A two-way boolean cannot express three independent outcomes (terminal-always-noop; non-terminal-but-already-assigned-noop; non-terminal-and-unassigned-assign) without this exact class of short-circuit bug.
## Fix
FIXED (2026-09-01, round-2 review, task 3.2). resync_apply_running_entry() rewritten as a genuine three-way `if`/`else if`/`else` split, not a boolean guard: (a) resync_job_is_terminal(job->job.state) -> ALWAYS a no-op, checked FIRST and independently, regardless of host (closes the Req 4.4 violation). (b) non-terminal AND job->job.allocs != NULL (already has SOME live non-terminal host assignment -- to the reporting worker's own host, or to a different one, it does not matter which) -> a no-op; also tightened from round 1's own host-specific check (resync_job_assigned_to_host(job, w->host.name)) to a host-agnostic allocs!=NULL check, since round-1's version would have ALSO wrongly routed a job assigned to a DIFFERENT host into the assign branch, resetting its start_time and re-appending a spurious WAL_JOB_DISPATCH fact for a job that was never actually lost. (c) non-terminal AND allocs == NULL (genuinely unassigned) -> record_job_reconcile_assign_fact(), Req 4.8's real trigger. Verified: (1) test_apply_report_all_five_outcomes_updates_job_table's j_extra is now seeded TERMINAL and asserted COMPLETELY UNTOUCHED (state stays DONE, no WAL append) -- the exact inversion of the round-1 assertion that had certified the bug. (2) test_apply_report_unqueried_extra_already_tracked_is_noop (branch (b), assigned to a DIFFERENT host) strengthened with a start_time-unchanged assertion (record_job_reconcile_assign_fact unconditionally stamps time(NULL), bug-1697 -- invisible to a bare state/allocs check since the job was already RUN either way) and an LSN-unchanged assertion (no spurious WAL_JOB_DISPATCH append) -- round 1's version of this test could not have caught either defect. (3) test_apply_report_unqueried_extra_unassigned_pend_job_gets_assigned (branch (c), genuinely unassigned/PEND) already covers the real assign-success case with full restart-replay rigor, added in the same round-1 fix pass. Full controller test suite (meson test -C src/xce.build 'controller test'): all green.
## Verification
_Not recorded (migrated from buglog.json)._
## References
- Related: bug-1747, bug-1697, bug-1631