# How the `archify preview` Command Watches and Reloads Verified JSON Files During Desktop Authoring

> Learn how the archify preview command watches and reloads verified JSON files during desktop authoring. Understand its live-reload loop for efficient development.

- Repository: [tt-a1i/archify](https://github.com/tt-a1i/archify)
- Tags: internals
- Published: 2026-08-05

---

**`archify preview` runs a self-contained live-reload loop that watches a JSON source file for changes, rebuilds a verified HTML artifact when the source stabilizes, and pushes updates to the browser via Server-Sent Events (SSE).**

The `archify preview` command in the `tt-a1i/archify` repository provides desktop authors with instant visual feedback while editing JSON diagram files. Rather than running manual builds after every change, the tool monitors the source file, coalesces rapid edits, validates each build, and automatically refreshes the browser—only when a verified artifact is ready. This article breaks down the complete watch-and-reload pipeline as implemented in the source code.

---

## Live Preview Architecture Overview

The preview system combines **file system watching**, **digest-based change detection**, **debounced builds**, **bounded child processes**, and **SSE-driven live reload**. These components work together to ensure the browser always displays the latest valid diagram without flashing intermediate errors or stale versions.

Key modules in `archify/bin/preview.mjs`:

- `runPreview` – entry point that initializes the server, resolves paths, and starts the watch loop.
- `observeSource` – polls and hashes the source file to detect changes.
- `queueStableBuild` – debounces build requests and triggers `beginBuild` on stable sources.
- `commitCandidate` – verifies build receipts and promotes artifacts to the live view.
- `broadcast` – pushes state updates to connected browsers via SSE.

---

## File Watching Strategy: Native Watches Plus Polling

`archify preview` uses a hybrid watching strategy to avoid missing rapid editor events.

### Native `fs.watch` with Directory Monitoring

The tool attaches a native watcher to the source file's directory:

```js
// From archify/bin/preview.mjs, lines 89-95
watcher = fs.watch(path.dirname(inputPath), (eventType, filename) => {
  if (filename === path.basename(inputPath)) {
    observeSource();
  }
});

```

Native `fs.watch` is efficient but unreliable during rapid rename bursts common in editors like VS Code or Vim.

### Periodic Poll Backup

To close the reliability gap, a polling timer runs every 800 milliseconds by default:

```js
// From archify/bin/preview.mjs, lines 98-100
pollTimer = setInterval(() => observeSource(), pollMs);

```

This dual approach ensures changes are detected without excessive CPU usage or missed events.

---

## Digest-Based Change Detection and Debouncing

Every detected change triggers `observeSource`, which computes a SHA-256 hash of the file contents:

```js
// From archify/bin/preview.mjs, lines 24-28
async function sourceDigest(inputPath) {
  const bytes = await fs.promises.readFile(inputPath);
  return sha256(bytes); // stable canonical representation
}

```

The hash—**not timestamps**—determines whether the source has meaningfully changed.

### Debounced Build Queue

When a new hash appears, `queueStableBuild` schedules a build with a 400ms debounce:

```js
// From archify/bin/preview.mjs, lines 55-58
function queueStableBuild(newHash) {
  queuedHash = newHash;
  clearTimeout(buildTimer);
  buildTimer = setTimeout(() => beginBuild(newHash), debounceMs);
}

```

If the source stabilizes (same hash after debounce), the build launches. If it changes again, the timer resets with the newer hash.

---

## Bounded Child Process Builds

The `beginBuild` function spawns `archify deliver` as a detached child process:

```js
// From archify/bin/preview.mjs, lines 71-82
const child = spawn(process.argv[0], [
  binPath, 'deliver', type, inputPath,
  '--output', candidatePath,
  '--json-receipt'
], { detached: true, stdio: ['ignore', 'pipe', 'pipe'] });

```

### Process Lifecycle Management

The preview enforces strict boundaries on build processes:

- **Graceful shutdown**: `stopGraceTimer` allows the child to complete naturally.
- **Forced termination**: `stopKillTimer` sends `SIGKILL` if the child lingers.
- **Pending build tracking**: If new changes arrive during a build, `pendingBuild = true` schedules a follow-up build immediately after the current child exits.

This prevents zombie processes and ensures the queue never stalls.

---

## Verification, Commit, and State Promotion

Build output goes through cryptographic verification before reaching the browser.

### Receipt Parsing and Hash Validation

The child writes a JSON receipt to stdout. `parseReceipt` extracts the expected artifact hash, and `commitCandidate` validates:

```js
// From archify/bin/preview.mjs, lines 42-50
const digest = await artifactDigest(candidatePath);
if (digest !== receipt?.artifact?.sha256) {
  throw new VerificationError('Artifact hash mismatch');
}

```

### Atomic Promotion to Live View

Verified artifacts are moved from the staging directory (`.archify-preview-…`) to the output path, stored in `artifactBuffer`, and marked as **verified**:

```js
// From archify/bin/preview.mjs, lines 61-68
if (digest !== lastArtifactDigest) {
  await fs.promises.rename(candidatePath, outputPath);
  artifactBuffer = await fs.promises.readFile(outputPath);
  lastArtifactDigest = digest;
  revision++;
  state = { status: 'verified', revision, updatedAt: Date.now() };
  broadcast();
}

```

If the source changed during the build (`supersededBy`), the newer hash is re-queued for the next cycle.

---

## Live Reload via Server-Sent Events

The preview server exposes three endpoints that enable real-time browser updates:

| Endpoint | Purpose |
|----------|---------|
| `/` | Serves `previewPage` with an `<iframe>` for the artifact. |
| [`/artifact.html`](https://github.com/tt-a1i/archify/blob/main//artifact.html) | Returns the current `artifactBuffer` (latest verified HTML). |
| `/events` | SSE stream broadcasting state changes. |

### Client-Side Reload Logic

The browser connects to `/events` and listens for state transitions:

```js
// From archify/bin/preview.mjs, lines 28-33 (server) and client-side equivalent
const es = new EventSource('http://127.0.0.1:12345/events');
es.addEventListener('state', ev => {
  const s = JSON.parse(ev.data);
  if (s.status === 'verified') {
    document.getElementById('preview-frame').src =
      `/artifact.html?revision=${s.revision}`;
  }
});

```

The `revision` query parameter busts caches and forces the iframe to load fresh content.

### Three UI States

The preview page renders distinct feedback for each state:

- **checking** – Build in progress; shows spinner with elapsed time.
- **verified** – Valid artifact displayed; badge shows revision number.
- **needs-fix** – Build failed; diagnostic panel shows redacted error details.

---

## Startup and Shutdown Sequences

### Initialization

`runPreview` orchestrates startup:

```bash

# CLI usage

npx archify preview architecture my-diagram.json

```

```js
// Programmatic usage
import { runPreview } from 'archify/bin/preview.mjs';

await runPreview({
  type: 'architecture',
  input: 'my-diagram.json',
  // output: 'my-diagram.html',
  // debounceMs: 300,
  // pollMs: 500
});

```

Steps include: creating the staging directory, resolving the output path via `resolveOutputPath`, starting the HTTP server with `startPreview`, and launching the watch loop.

### Graceful Termination

On `SIGINT` or `SIGTERM`:

1. First signal triggers graceful stop (`stopGraceTimer`).
2. Second signal forces `SIGKILL` on the child process.
3. Server closes, staging directory cleans up, and `preview.closed` resolves.

```js
// From archify/bin/preview.mjs, lines 84-104
process.on('SIGINT', () => terminate('SIGINT'));
process.on('SIGTERM', () => terminate('SIGTERM'));

```

---

## Key Files and Module Responsibilities

| File | Role |
|------|------|
| `archify/bin/preview.mjs` | Core live-preview engine: watch, debounce, build queue, SSE server, state machine. |
| `archify/bin/open-artifact.mjs` | Browser launcher (`openLoopbackUrl`) for the preview URL. |
| `archify/renderers/shared/output-path.mjs` | Output path resolution and `meta.output` field handling. |
| `archify/bin/archify.mjs` | CLI entry point that parses arguments and delegates to `runPreview`. |

All modules live under `archify/bin/` and `archify/renderers/` in the repository.

---

## Summary

- **`archify preview`** combines native `fs.watch`, 800ms polling, and SHA-256 digests to detect stable source changes.
- **400ms debouncing** ensures rapid editor bursts coalesce into single builds.
- **Detached child processes** run `archify deliver` with grace-period and kill-timer safeguards.
- **Cryptographic verification** of build receipts guarantees only valid artifacts reach the browser.
- **Server-Sent Events** push state transitions; the iframe reloads only on verified updates with cache-busting revision parameters.
- **Three UI states** (checking, verified, needs-fix) give authors clear feedback without distraction.

---

## Frequently Asked Questions

### How does `archify preview` handle rapid successive saves in my editor?

The tool uses **debounced digest-based change detection**. Rapid saves produce the same or successive file hashes; the 400ms debounce timer resets with each change, and only sources that remain stable for the full debounce period trigger builds. The 800ms polling backup catches any events missed by native `fs.watch` during editor rename bursts.

### What happens if I edit the JSON while a build is running?

The running build continues to completion (or times out). If the source hash changed during the build, `supersededBy` is set to the newer hash. When the child exits, `pendingBuild` triggers an immediate new build cycle with the latest hash—no stale artifacts are ever promoted.

### Why does the browser only reload on "verified" status, not on every file change?

This design prevents **flashing invalid or intermediate states**. The preview only pushes updates after `commitCandidate` validates the artifact hash against the build receipt. Failed builds enter the `needs-fix` state with diagnostics, keeping the last good diagram visible.

### Can I adjust the watch sensitivity or debounce timing?

Yes. The `runPreview` function accepts `debounceMs` (default 400) and `pollMs` (default 800) options. Pass these programmatically or modify defaults in your wrapper. The CLI currently uses fixed defaults; for custom timing, use the programmatic API.