src/tick.ts
194 lines and 24 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| runTick | 21 to 205 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:28> | 28 to 28 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:29> | 29 to 35 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:44> | 44 to 44 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:44> | 44 to 44 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:45> | 45 to 49 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:60> | 60 to 60 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:61> | 61 to 65 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:76> | 76 to 76 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:77> | 77 to 81 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:91> | 91 to 91 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:92> | 92 to 102 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:115> | 115 to 115 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:116> | 116 to 120 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:132> | 132 to 132 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:133> | 133 to 139 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:149> | 149 to 149 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:150> | 150 to 154 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:166> | 166 to 166 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:167> | 167 to 172 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:183> | 183 to 183 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:184> | 184 to 188 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:197> | 197 to 197 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:198> | 198 to 202 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
Imports
It imports src/auto-merge-tick.ts, src/digest.ts, src/doc-index.ts, src/env.ts, src/log.ts, src/maintenance.ts, src/merge-resume.ts, src/merge-safety.ts, src/merge-units.ts, src/outcome-prs.ts, src/public-data.ts, src/push.ts, src/task-runs.ts, src/watcher-gather.ts, src/watcher.ts. Imported by src/index.ts.
Source
import type { Env } from "./env";
import { autoMergeTick } from "./auto-merge-tick";
import { mergeResumeTick } from "./merge-resume";
import { sweepIfDue } from "./outcome-prs";
import { runTask, type TaskResult } from "./task-runs";
import { watcherTick } from "./watcher";
import { gatherFindings } from "./watcher-gather";
import { maintenanceTick } from "./maintenance";
import { publicBuildTick } from "./public-data";
import { docIndexTick } from "./doc-index";
import { pushTick } from "./push";
import { digestTick } from "./digest";
import { unitsTick } from "./merge-units";
import { mergeSafetyTick } from "./merge-safety";
import { logEvent } from "./log";
// The five-minute tick's steps, in order. Each is its own task in the run ledger
// (src/task-runs.ts): runTask records a run, and records and logs a throw without
// rethrowing it, so one step's throw does not stop the rest of the tick. A step that was
// not due returns null from its judge and records nothing.
export async function runTick(env: Env, now: Date): Promise<void> {
// Auto-merge. The policy document decides whether it does anything; it ships
// disabled, and a disabled policy is not a run. A policy that cannot be read or is
// refused is.
await runTask(
env,
"auto-merge",
() => autoMergeTick(env, now),
(merged): TaskResult => {
if (merged.ran) {
logEvent("log", "AUTO_MERGE", { message: `AUTO_MERGE ${merged.note}` });
return { outcome: "ok", reason: merged.note };
}
return merged.policy_version === null ? { outcome: "refused", reason: merged.note } : null;
},
{ tag: "AUTO_MERGE_THREW:", rethrow: false }
);
// The watcher, on its own half-hourly stamp. It only posts jobs, so the worst a
// broken pass can do is add a row to the queue.
await runTask(
env,
"watcher",
() => watcherTick(env, now, () => gatherFindings(env, now)),
(watched): TaskResult => {
if (!watched.ran) return null;
logEvent("log", "WATCHER", { message: `WATCHER ${watched.note}` });
return { outcome: "ok", reason: watched.note };
},
{ tag: "WATCHER_THREW:", rethrow: false }
);
// Merge units (src/merge-units.ts): after the watcher, which records rollbacks in
// deploy_outcomes. It proposes promotions and demotions and suspends a unit whose
// unattended merge was rolled back; the suspension only narrows, and the auto-merge
// step reads it on the next tick. Recorded only when something changed.
await runTask(
env,
"merge-units",
() => unitsTick(env, now),
(report): TaskResult => {
if (!report.ran) return null;
logEvent(report.newly_suspended.length ? "error" : "log", "MERGE_UNITS", { message: `MERGE_UNITS ${report.note}` });
return { outcome: "ok", reason: report.note };
},
{ tag: "MERGE_UNITS_THREW:", rethrow: false }
);
// The auto-merge safety meter and shutoff (src/merge-safety.ts): after merge-units. Its
// revert pass runs at most half-hourly; the counts and the shutoff read D1 only, every
// tick. A namespace it pauses only narrows: the auto-merge step reads the pause on the
// next tick. Recorded only when something changed.
await runTask(
env,
"merge-safety",
() => mergeSafetyTick(env, now),
(report): TaskResult => {
if (!report.ran) return null;
logEvent(report.newly_paused.length ? "error" : "log", "MERGE_SAFETY", { message: `MERGE_SAFETY ${report.note}` });
return { outcome: "ok", reason: report.note };
},
{ tag: "MERGE_SAFETY_THREW:", rethrow: false }
);
// Stale jobs: a blocked job whose named pull requests have all merged goes back to its
// holder with a note. It resumes and never completes; see src/merge-resume.ts. Recorded
// only when it resumed something, so the ledger is not one empty pass every five minutes.
await runTask(
env,
"merge-resume",
() => mergeResumeTick(env, now),
(report): TaskResult => {
// A stale:prs cache that could not be written is recorded even on a pass that
// resumed nothing, so the stale view's missing rows have a cause on the ledger.
if (report.cache_error) {
logEvent("error", "MERGE_RESUME_CACHE", { message: `MERGE_RESUME_CACHE ${report.note}` });
return { outcome: "refused", reason: report.note };
}
if (report.resumed.length === 0) return null;
logEvent("log", "MERGE_RESUME", { message: `MERGE_RESUME ${report.note}` });
return { outcome: "ok", reason: report.note };
},
{ tag: "MERGE_RESUME_THREW:", rethrow: false }
);
// The daily maintenance pass (src/maintenance.ts): once a UTC day after 11:00, it lists
// what has gone stale for the seat's morning read. Its writes are pruning merged
// branches, off until the seat turns it on, and tagging a shared package's merged version
// bump (src/maintenance-releases.ts), on until the seat turns it off. Each delete and each
// tag is audit-logged before the next, so a broken pass costs a missing list, not an
// unrecorded write. Not due is not a run.
await runTask(
env,
"maintenance",
() => maintenanceTick(env, now),
(report): TaskResult => {
if (!report.ran) return null;
logEvent("log", "MAINTENANCE", { message: `MAINTENANCE ${report.note}` });
return { outcome: "ok", reason: report.note };
},
{ tag: "MAINTENANCE_THREW:", rethrow: false }
);
// The public system model (src/public-data.ts): once a UTC day, repo visibility read
// from GitHub, the model filtered by toPublic and stored for /public/model/v1.json,
// and the public repos' code maps for /public/code/v1.json, rebuilt hourly. A
// failed read throws before anything is written, so the last good document stays
// served and the ledger shows the failure. Not due is not a run.
await runTask(
env,
"public-build",
() => publicBuildTick(env, now),
(report): TaskResult => {
if (!report.ran) return null;
// A code map that would not parse is a refusal the ledger shows; the model was still built.
const failed = "codeFailed" in report;
logEvent(failed ? "error" : "log", "PUBLIC_BUILD", { message: `PUBLIC_BUILD ${report.note}` });
return { outcome: failed ? "refused" : "ok", reason: report.note };
},
{ tag: "PUBLIC_BUILD_THREW:", rethrow: false }
);
// The meaning index (src/doc-index.ts): documents changed since the last tick go to
// AI Search, and once a day stale items are removed and missing ones re-sent. A
// refused upload is recorded as refused; the next edit or the daily pass retries it.
await runTask(
env,
"doc-index",
() => docIndexTick(env, now),
(report): TaskResult => {
if (!report.ran) return null;
logEvent(report.failed > 0 ? "error" : "log", "DOC_INDEX", { message: `DOC_INDEX ${report.note}` });
return { outcome: report.failed > 0 ? "refused" : "ok", reason: report.note };
},
{ tag: "DOC_INDEX_THREW:", rethrow: false }
);
// The daily merge-state sweep. Outcome rows record a pull request as unmerged when
// written, because the driver blocks and the seat merges afterwards. The merge path
// corrects the rows it can see; this catches the rest (a merge done with gh rather
// than capsid_close_pr action merge, and rows written before the join table existed). Bounded per
// sweep, so the cost is fixed however far behind it is.
await runTask(
env,
"outcome-sweep",
() => sweepIfDue(env, now),
(swept): TaskResult => {
if (!swept) return null;
const line = `checked ${swept.checked}, changed ${swept.changed}, seeded ${swept.seeded}, backfilled ${swept.backfilled}`;
logEvent("log", "OUTCOME_SWEEP", { message: `OUTCOME_SWEEP ${line}` });
return { outcome: "ok", reason: line };
},
{ tag: "OUTCOME_SWEEP_THREW:", rethrow: false }
);
// Phone alerts (src/push.ts): after the auto-merge step and the watcher, so a typed
// pull request or a rollback they just recorded is seen this tick. It spends no model
// tokens. Recorded only when an alert was due, so the ledger is not one empty pass
// every five minutes; no key or no device is not a run.
await runTask(
env,
"push",
() => pushTick(env, now),
(report): TaskResult => {
if (!report.ran) return null;
logEvent("log", "PUSH", { message: `PUSH ${report.note}` });
return { outcome: report.sent.length > 0 ? "ok" : "refused", reason: report.note };
},
{ tag: "PUSH_THREW:", rethrow: false }
);
// The daily email digest (src/digest.ts): once a UTC day, and only once Email Routing is
// on. Off or not due is not a run; a send Cloudflare refused is recorded as refused.
await runTask(
env,
"digest",
() => digestTick(env, now),
(report): TaskResult => {
if (!report.ran) return null;
logEvent(report.sent ? "log" : "error", "DIGEST", { message: `DIGEST ${report.note}` });
return { outcome: report.sent ? "ok" : "refused", reason: report.note };
},
{ tag: "DIGEST_THREW:", rethrow: false }
);
}