How set-status.mjs Enforces Canonical State Transitions with Atomic Writes and Locking in santifer/career-ops
set-status.mjs acts as the single authoritative CLI that updates application statuses through a three-layer defense: canonical state validation against templates/states.yml, exclusive filesystem locking during the read-modify-write cycle, and atomic file replacement to prevent corruption.
The set-status.mjs script in the santifer/career-ops repository is the only entry point permitted to modify data/applications.md. Its design eliminates an entire class of race condition bugs and data integrity failures through tightly coupled validation, locking, and atomic write mechanisms. This article breaks down exactly how each layer works, with direct references to the source code implementation.
Canonical State Validation: Rejecting Invalid States Before Touching the Tracker
Before any file operation occurs, set-status.mjs validates that the requested state exists in the single source of truth.
The script imports canonical states from templates/states.yml at the module level:
// set-status.mjs
const STATES_FILE = 'templates/states.yml';
At runtime, it calls loadCanonicalStates() and resolveCanonicalState() from tracker-utils.mjs to resolve user input against the canonical list:
// set-status.mjs lines 49-56
import { loadCanonicalStates, resolveCanonicalState } from './tracker-utils.mjs';
const states = await loadCanonicalStates(STATES_FILE);
const canonical = resolveCanonicalState(stateInput, states);
if (!canonical) {
console.error(`"${stateInput}" is not a valid state. Valid states: ${Object.keys(states).join(', ')}`);
process.exit(1); // USAGE exit code
}
This validation in tracker-utils.mjs (lines 43-53, 66-71) aborts with exit code 1 if the state doesn't match. The tracker file remains untouched—no lock is acquired, no write is attempted.
Exclusive Lock Acquisition: Preventing Concurrent Write Corruption
Once validation passes, set-status.mjs obtains an exclusive filesystem lock before reading the tracker. This lock is shared across all writer scripts in the repository, including merge-tracker.mjs and mark-pdf-ready.mjs.
The lock acquisition happens here:
// set-status.mjs lines 68-71
import { acquireTrackerLockForCli } from './tracker-utils.mjs';
const releaseLock = await acquireTrackerLockForCli(APPS_FILE, { timeout: LOCK_TIMEOUT_MS });
The acquireTrackerLockForCli() function in tracker-utils.mjs (lines 54-58, 84-90) delegates to acquireTrackerLock(), which creates a lock directory based on a hash of the tracker path (lines 99-102). If the lock cannot be obtained within the configured timeout, the script exits with code 4 (LOCK_TIMEOUT).
The lock is held for the entire read-modify-write sequence. No other process can acquire it until the current operation completes.
Atomic File Replacement: Guaranteeing All-or-Nothing Writes
After modifying the tracker data in memory, set-status.mjs persists changes through writeFileAtomic():
// set-status.mjs lines 26-30 (conceptual wrapping of the atomic write)
await writeFileAtomic(APPS_FILE, updatedContent);
The writeFileAtomic() implementation in tracker-utils.mjs (lines 22-33) performs:
- Write to temporary file: Creates a hidden temp file in the same directory as the target
- Rename operation: Uses
fs.rename()to atomically replace the original - Windows retry logic: Retries on contention for Windows compatibility
- Cleanup on failure: Removes the temp file if any step fails
This pattern ensures that readers of data/applications.md never observe a partially written table. The file is either fully updated or unchanged.
The Complete Read-Modify-Write Flow
Putting the three layers together, set-status.mjs executes this sequence:
- Parse and resolve selectors — handles
--row,--report, company names, or bare numbers with ambiguity detection (lines 90-112) - Validate canonical state — aborts early on invalid input
- Acquire exclusive lock — blocks concurrent writers
- Read tracker —
readFileSyncwhile lock held - Rebuild target row — updates Status cell, optionally appends Notes with idempotent handling (lines 94-105)
- Atomic write —
writeFileAtomic()commits changes - Append transition ledger — adds entry to
status-log.tsvwithin the same lock window (lines 37-55) - Release lock — cleanup happens automatically
The ledger append is critical: because it occurs before lock release, the status history cannot interleave with other writers' operations.
Usage Examples
Basic status update with note
node set-status.mjs 42 Applied --note "Sent CV"
This validates "Applied" against templates/states.yml, acquires the lock, updates row 42's Status cell, appends the note (; Sent CV), and writes atomically.
Explicit selector disambiguation
node set-status.mjs --row 42 Applied # tracker row #42
node set-status.mjs --report 123 Applied # row linking report #123
Use these flags when numeric selectors could match multiple contexts.
Dry-run for preview
node set-status.mjs 42 Applied --dry-run --json
Resolves the selector, validates the state, but skips lock acquisition and file operations. Outputs JSON showing the proposed change.
Force override for mismatched selectors
node set-status.mjs 42 Applied --force
Bypasses the report-number mismatch guard. Use with caution.
Key Source Files and Functions
| File | Purpose | Key Functions |
|---|---|---|
set-status.mjs |
CLI entry point for status updates | Main orchestration, selector resolution, row rebuilding |
tracker-utils.mjs |
Shared utilities for all writers | loadCanonicalStates(), resolveCanonicalState(), acquireTrackerLockForCli(), writeFileAtomic() |
templates/states.yml |
Canonical state definitions | YAML source parsed by loadCanonicalStates() |
All writer scripts in the career-ops repository share tracker-utils.mjs, ensuring consistent locking and atomic write behavior across the entire codebase.
Summary
- Canonical validation in
tracker-utils.mjsrejects invalid states before any file operations - Exclusive filesystem locking through
acquireTrackerLockForCli()prevents race conditions during read-modify-write cycles - Atomic file replacement via
writeFileAtomic()guarantees readers never see partial updates - Shared lock implementation across all writer scripts ensures system-wide consistency
- Ledger append within lock window maintains serialized status history without interleaving
Together, these mechanisms make set-status.mjs a robust, corruption-resistant tool for maintaining the application tracker.
Frequently Asked Questions
What happens if two set-status.mjs commands run simultaneously?
The second command blocks until the first releases the lock or times out. If the timeout expires, the second command exits with code 4 (LOCK_TIMEOUT). The lock implementation in tracker-utils.mjs uses a directory-based mechanism keyed to the tracker file path hash, ensuring exclusive access across all writer scripts.
Why does the script use atomic writes instead of just locking?
Locking prevents concurrent modifications but doesn't protect against crashes during write. The atomic replace pattern in writeFileAtomic() ensures that data/applications.md is either fully updated with the new content or remains exactly as it was—no truncation, no partial table rows, no corruption from a mid-write process termination.
Where are valid states defined and how are aliases resolved?
Valid states live in templates/states.yml. The loadCanonicalStates() function parses this file, and resolveCanonicalState() matches user input against both canonical names and defined aliases. This centralized definition prevents drift: every writer script enforces the same state vocabulary without hardcoded validation logic.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →