Skip to content

refactor: named seams — one download path, cli-core atomic writes and lock, program.ts split - #141

Merged
leemour merged 6 commits into
mainfrom
refactor/named-seams
Oct 17, 2026
Merged

leemour merged 6 commits into
mainfrom
refactor/named-seams

Conversation

@leemour

@leemour leemour commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Step 7 of the architecture re-review (2026-10-14), zoom's items. Each claim was checked against origin/main at 9062f17 before acting; the review is three days older than that.

1. Store opening — claim false as stated, one small duplicate fixed

  • Found: the store is opened in one place only (createStoreConnector in src/store-connection.ts). There were no eight copies. What was written twice was the step before it: program.ts and bin/main.ts (the MCP path) each checked the selected profile, built the { profile, externalId, intent } request and phrased the same error.
  • Changed: connectSelectedProfile in store-connection.ts does that once. StoreConnection and StoreConnectRequest move from program.ts to store-connection.ts (the contract they describe); index.ts still exports both. The MCP path still reads the profile once; program.ts still wraps non-CliError failures.

2. Hand-written atomic writes and lock — claim true

  • Found: six temp-file-then-rename/link writes (archive manifest, verified-size cache, schedule job file, systemd unit, sync checkpoint, export file) and one hand-written lock (the checkpoint's .lock/owner.json with a .lock.recovery directory).
  • Changed: five writes call cli-core 0.307.0 writeFileAtomic, each keeping mode 0600, its overwrite rule (overwrite: false for units, !!existing for schedule jobs) and its last-moment check in beforePublish (the schedule file's inode/size/mtime check, the checkpoint lease check, cancellation). signal is never passed, so cancellation still raises zoom's own timeout/cancelled errors.
  • Checkpoint lock → acquireLock at a new path <checkpoint>.locks, so an old .lock/owner.json is not read as an unknown holder. Contention still answers configuration_error with the same words; release twice still refuses.
  • Left on purpose: src/output-file.ts. It reports a failure when cleanup after publishing fails (its tests pin that); writeFileAtomic swallows that cleanup error.

Strictly safer:

  • the verified cache is now fsynced, and its folder too (it was a plain write + rename);
  • a checkpoint temp file is now removed when the lease check fails (it leaked);
  • the lock folder <checkpoint>.locks stays 0700 like the old .lock (created private before acquireLock, mode set again once held; tested);
  • the checkpoint folder sync refuses a non-ordinary folder. A symlinked archive folder still works: the save resolves the folder first (new test).

Not strictly the same — please review:

  • two processes asking for the checkpoint lock at the same instant can both be refused (never both granted); a retry succeeds. The archive writer lock already behaves this way. The test now asserts "at most one winner, then a retry wins".
  • an empty lock folder no longer blocks; an unreadable marker still does.
  • an older zm that still uses .lock does not exclude the new .locks (no users yet).

3. Download paths — claim true in substance, wrong place named

  • Found: ZoomRemote itself had no download; the host check and the manual-redirect loop were written twice, in api.ts (transcript) and recording-archive.ts (recording).
  • Changed: one ZoomRemote.download(url, { accessToken, signal, what, headers, unreachable }): checks the host on every hop, follows at most 5 redirects, cancels redirect bodies, returns the final response. Status and body checks stay with each caller. The transcript path keeps provider_error and no cancellation check on its private 30 s deadline; the recording path keeps network_error and its cancellation check — through the unreachable callback.

4. Large files — split

File main now
src/program.ts 977 476
src/recording-archive.ts 560 314
src/recording-manifest.ts (new) — 214
src/watch-service.ts (new) — 172
src/store-commands.ts (new) — 162
src/setup-commands.ts (new) — 158
src/recording-schedule-setup.ts (new) — 93
src/meeting-source.ts (new) — 67
src/answer-policy.ts 10 45
  • program.ts keeps the command tree, output and per-run state; per-run state a parse resets stays there or behind a reset() the parse calls. Command registration order is unchanged.
  • Layout: zoom's flat *-commands.ts files, not drive's src/commands/ — moving every existing group would be a much larger diff for no seam gained.
  • src/mcp-server.ts (476) stays: it is one job (the MCP transport and its tools) and under 500.
  • biome.json: per-file cap 1,000 → 500. The installed-package acceptance scripts (scripts/installed-*.mjs, the largest 710 lines) join the tests that are exempt.
  • The watch service and the lock/manifest code the review named: the watch cycle is watch-service.ts; the manifest code is recording-manifest.ts; the lock is now cli-core's.

Contract

  • commands --json: captured from main and from this branch with ~/.cache/step7/capture.mjs; diff without run_id/duration_ms is empty.
  • MCP tools/list of zm serve --mcp (plain, and with --unified-search --meeting-context --meeting-preparation) in a throwaway config: identical (8 / 14 tools). A --semantic run listed the same 8 as plain, so it did not exercise a semantic tool.

Tests changed

  • recording-sync-store.test.ts: lock tests plant cli-core markers (<pid>-<nonce>.json in .locks) instead of .lock/owner.json; the .lock.recovery case becomes "a live holder refuses"; the simultaneous-acquire test asserts at most one winner, then a retry wins; new test for a symlinked archive folder (it fails with ENOTDIR without the realpath).
  • recording-schedule-store-race.test.ts: the injected race now matches the helper's temp name (.tmp suffix) instead of .zoom-schedule-; it still asserts the race happened.

Not done: effects beside each command

command-effects.ts still decides effects centrally: some commands are mounted by cli-core, and the list wins over per-command annotations. It already disagrees with them (watch is FETCH in the list, LOCAL_WRITE in watch-commands.ts; same for recordings schedule run), so moving effects would change what commands --json reports. Left as debt for its own change.

Checks (local)

The PR runs only changelog / guard and standards; "Full validation" (ci.yml) is workflow_call and did not run here. Run locally:

typecheck, lint, docs:lint, docs:check, standards:check, contracts:check, contracts:upgrade, security:no-leak, generate:check, lint:commands, full tests with coverage, test:matrix:check.

🤖 Generated with Claude Code

leemour and others added 6 commits October 17, 2026 02:31
The transcript reader and the recording transfer each checked the Zoom
download host and followed manual redirects on their own. Both now go
through ZoomRemote.download, which checks every hop and returns the final
response; status and body checks stay with each caller. The error words,
codes and cancellation handling of each path are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Five hand-written temp-file-and-rename writes now call writeFileAtomic:
the archive manifest, the verified-size cache, the schedule job file,
the systemd timer units and the sync checkpoint. Each keeps its mode
(0600), its overwrite rule and its last-moment check (beforePublish).

The sync checkpoint's own lock (a .lock directory with owner.json and a
.recovery directory) is replaced by acquireLock at a new path,
<checkpoint>.locks, so a lock left by an older zm is not read as an
unknown holder. Contention still answers configuration_error with the
same words.

Strictly safer:
- the verified cache is now synced, and its folder too;
- a checkpoint temp file is now removed when the lease check fails;
- the checkpoint folder sync refuses a non-ordinary folder (the save
  resolves a symlinked archive folder first, as before it followed it).

Changed:
- two processes that ask for the checkpoint lock at the same moment can
  both be refused (never both granted); a retry succeeds. The archive
  writer lock already behaved this way.
- an empty lock folder no longer blocks; a marker that cannot be read
  still does.

output-file.ts stays hand-written: it reports a failure when cleanup
after publishing fails, which its tests pin and writeFileAtomic does not do.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The store itself was already opened in one place (createStoreConnector).
What was written twice was the step before it: the command path and the
MCP path each checked the selected profile, built the connect request
and phrased the same error. connectSelectedProfile does that once.

The connection types move from program.ts to store-connection.ts, the
module whose contract they are, which also removes its type import back
into program.ts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
recording-archive.ts (560 lines on main) held two jobs: transferring
recordings into an archive, and reading, writing and verifying the
archive's manifests and verified-size cache. The second moves to
recording-manifest.ts with inspectArchive and verifyArchive; the
transfer stays. 314 and 214 lines.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
program.ts was 977 lines on main: the command tree and per-run state,
plus five jobs wired inline. Each moves beside its commands, keeping
zoom's flat *-commands.ts layout:

- watch-service.ts: watch planning and one ingestion cycle;
- setup-commands.ts: config, doctor, session status and login;
- store-commands.ts: the commands over the shared store;
- meeting-source.ts: the API or folder source the config names;
- recording-schedule-setup.ts: saved jobs, systemd timers, credentials;
- answer-policy.ts gains failure rendering (reportFailure).

Per-run state that a parse resets stays in createProgram or behind a
reset() the parse calls. Registration order, and so `commands --json`,
is unchanged. program.ts is now 476 lines; the per-file cap drops from
1,000 to 500 lines, with the installed-package acceptance scripts
exempt like the other tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
acquireLock creates its marker folder with the default mode (0775 under
a usual umask). The old checkpoint lock folder was 0700; create
<checkpoint>.locks private before taking the lock and set the mode again
once held, so a folder another holder recreated is private too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@leemour
leemour force-pushed the refactor/named-seams branch from 6bbe60d to 94cd707 Compare October 17, 2026 00:33
@leemour
leemour merged commit 9f8f719 into main Oct 17, 2026
2 checks passed
@leemour
leemour deleted the refactor/named-seams branch October 17, 2026 00:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant