feat(verify): stored readiness is the single fact, the host owns the clock (#123)
Samva, DomainKit's design-partner host, ended up with three writers of
one fact and two copies of
it. Its server graph calls `Verify.observe` and mirrors the verdicts
onto its own row; its browser
flow's `Verify.useController` observes again on mount and polls; its
table reads one projection
while its rail reads the mirrored copy. Nothing tells the host when
DomainKit wrote readiness.
This makes DomainKit's stored readiness the single fact, and gives the
host the clock and the
projection.
## What changes
- **`Verify.Observer`** — a reference-shaped host seam with one method,
`readinessChanged({ domain, readiness, cause })`, fired once per stored
readiness after the
`Storage` write returns. An observer failure or defect is logged and
swallowed: the observation is
already durable, and losing it because a projection broke would make the
stored fact depend on the
host's side effects.
- **`Verify.latest` on the wire** — `GET /domains/:domain/readiness` and
`Transport.VerificationGroup.latest(domain)`, so a surface can read the
stored observation instead
of making one.
- **`Records.standingOf` replaces `Records.statusOf`** — it returns `{
planned, observed }` rather
than a union that let a pending plan's operation hide the observation.
The registry's records table
renders a plan column only while a plan is pending and takes its status
column from the
observation.
- **`Requirement.key` and `Verify.summary`** — `key` is the record's
type, name, and data, the same
string `Records.identity` produces, so a host pairs rows by content
rather than by position.
`Verify.summary(readiness)` is a pure count of the requirement statuses.
- **Host-supplied readiness in the flow** — `FlowOptions.verification`
and the same option on
`Verify.useController`. When supplied, the hook creates no observation
on mount and sets no timer,
and its `observe` and `retry` call the host's. Drift replanning still
runs off the supplied value.
- **Docs and gallery** — the reference inventories, a guide section on
host-owned observation, and
typechecked snippets for the Effect host, the drop-in host that changes
nothing, and a fetch host
reading `latest`.
## Usage
Every block below is sliced from a file in `examples/`, which CI
typechecks, so none of it can drift
from the shipped signatures.
### An Effect host providing the observer, and a job on its own clock
`examples/core/host-observation.ts`, regions `observer` and `job`:
```ts
/**
* `Verify.Observer` fires once per stored readiness, after the write. It is the one place a host
* learns that DomainKit wrote readiness, so a projection lives here instead of beside every call
* that might have caused one.
*/
const observer = Layer.succeed(Verify.Observer, {
readinessChanged: ({ cause, domain, readiness }) =>
Effect.gen(function* () {
if (readiness.overall === "ready") return yield* markReady(domain);
// A pending domain carries its own schedule, so the host sleeps on it rather than polling.
if (readiness.nextCheckAt !== null && cause === "observe") {
yield* wake({ domain, at: new Date(DateTime.toEpochMillis(readiness.nextCheckAt)) });
}
}),
});
export const live = DomainKit.layer({ providers }).pipe(Layer.provideMerge(observer));
```
```ts
/**
* The clock is the host's. A job observes the domains that are due, and the observer above is what
* tells the rest of the application that something changed.
*/
export const sweep = Effect.gen(function* () {
const due = yield* domainsDue;
yield* Effect.forEach(due, (domain) => Verify.observe({ domain }), { concurrency: 8 });
}).pipe(Effect.provideService(Principal.Service, principal));
```
### An API handler answering from the stored fact
`examples/core/host-observation.ts`, region `snapshot`:
```ts
/**
* A page or an API answers from the stored fact. `Verify.latest` reads it without observing, and
* `Verify.summary` counts it, so nothing about the domain is decided twice.
*/
export const snapshot = (domain: string) =>
Effect.map(Verify.latest(domain), (readiness) => ({
readiness,
...Verify.summary(readiness),
}));
```
### A React page the host feeds, with both row facts in their own
columns
`examples/react/host-readiness.tsx`, region `host-readiness`:
```tsx
/**
* The host owns the clock: it reads the stored readiness on its own schedule, and the flow renders
* what it was given. Nothing observes on mount, no timer runs, and `observe` and `retry` on the
* surface ask the host for a fresh reading.
*/
function HostObservedSetup({ domain }: { readonly domain: string }) {
const [readiness, setReadiness] = useState<Verify.Readiness | null>(null);
const refresh = useCallback(() => {
void api.verification?.latest(domain).then(setReadiness);
}, [domain]);
useEffect(refresh, [refresh]);
const flow = Domain.useFlow({
domain,
requirements,
verification: { readiness, observe: refresh },
});
const counts = Verify.summary(flow.readiness);
return (
<section>
<p>{counts.observed ? `${counts.satisfied} of ${counts.total} found` : "Not checked yet"}</p>
<table>
<tbody>
{flow.requirements.map((record) => {
// Two facts, two columns: what a pending plan will do, and what was read back.
const standing = Records.standingOf(record, {
plan: flow.plan,
readiness: flow.readiness,
});
return (
<tr key={Records.identity(record)}>
<td>{record.name}</td>
<td>{standing.planned === null ? null : standing.planned._tag}</td>
<td>{standing.observed?.status ?? "not checked"}</td>
</tr>
);
})}
</tbody>
</table>
<button onClick={() => flow.verification.observe()} type="button">
Check again
</button>
</section>
);
}
export function HostObserved({ domain }: { readonly domain: string }) {
return (
<DomainKit.Root transport={transport}>
<HostObservedSetup domain={domain} />
</DomainKit.Root>
);
}
```
### The drop-in host that changes nothing, and a fetch transport reading
`latest`
`examples/react/host-readiness.tsx`, region `drop-in`, and
`examples/client/transport.ts`, region
`latest`:
```tsx
/** No `verification`: the flow observes on mount and follows `nextCheckAt` on its own. */
function SelfObservingSetup({ domain }: { readonly domain: string }) {
const flow = Domain.useFlow({ domain, requirements });
return <p>{flow.readiness?.overall ?? "not checked"}</p>;
}
/** The stored readiness over the same transport, for a host that wants it without observing. */
export const storedReadiness = (domain: string) => api.verification?.latest(domain) ?? null;
export function SelfObserving({ domain }: { readonly domain: string }) {
return (
<DomainKit.Root transport={transport}>
<SelfObservingSetup domain={domain} />
</DomainKit.Root>
);
}
```
```ts
/**
* `latest` reads the stored readiness without observing, for a surface whose host owns the clock.
* It is `null` until the domain has been observed once.
*/
export const stored = Effect.gen(function* () {
const verification = transport.verification;
if (verification === undefined) return null;
return yield* verification.latest("app.example.com");
});
```
## Final API
```ts
// domainkit — Verify
export type Cause = "observe" | "evidence";
export interface ReadinessChanged {
readonly domain: string;
readonly readiness: Readiness;
readonly cause: Cause;
}
export interface ObserverShape {
readonly readinessChanged: (event: ReadinessChanged) => Effect.Effect<void, unknown>;
}
export class Observer extends Context.Reference<ObserverShape>("@domainkit/Verify/Observer", {
defaultValue: (): ObserverShape => ({ readinessChanged: () => Effect.void }),
}) {}
export const requirementKey: (record: DnsRecord.Model) => string;
export interface Summary {
readonly observed: boolean;
readonly total: number;
readonly satisfied: number;
readonly missing: number;
readonly mismatch: number;
readonly unknown: number;
}
export interface Summarisable {
readonly requirements: ReadonlyArray<{ readonly status: Storage.RequirementStatus }>;
}
export const summary: (readiness: Summarisable | null) => Summary;
// Changed: every requirement now carries its key, on the wire too.
export interface Requirement {
readonly key: string;
readonly operationId: Plan.OperationId | null;
readonly record: DnsRecord.Model;
readonly status: Storage.RequirementStatus;
readonly evidence: ReadonlyArray<Evidence>;
}
```
```ts
// domainkit/client — Transport.VerificationGroup
export interface VerificationGroup {
readonly observe: (
domain: string,
options?: { readonly requirements?: ReadonlyArray<DnsRecord.Model> },
) => Fx<Readiness>;
readonly latest: (domain: string) => Fx<Readiness | null>;
}
// domainkit/server — one new route
// GET /domains/:domain/readiness -> Readiness | null (endpoint name: "readiness")
```
```ts
// @domainkit/react — Records
export interface Standing {
readonly planned: Plan.Operation | null;
readonly observed: Readiness["requirements"][number] | null;
}
export const standingOf: (record: DnsRecord.Model, sources: Sources) => Standing;
// Removed: `Records.statusOf` and the fused `Records.Standing` union.
// @domainkit/react — Verify
export interface Supplied {
readonly readiness: Readiness | null;
readonly observe?: (() => void) | undefined;
}
export interface Options {
readonly domain: string;
readonly requirements?: ReadonlyArray<DnsRecord.Model>;
readonly polling?: boolean;
readonly supplied?: Supplied | undefined;
}
export const summary: typeof CoreVerify.summary;
// @domainkit/react — Domain.FlowOptions
export interface FlowOptions {
// ...unchanged fields
readonly verification?: Verify.Supplied;
}
// @domainkit/react — Messages.Catalog gains `headingPlan`.
```
## Public contract
Breaking, in 0.x: `Records.statusOf` and the fused `Records.Standing`
union are gone. Every other
change is additive, and a host that supplies no `verification` and no
`Observer` behaves exactly as
it does today.
## Verification
- `bun run release:check` (root) — pass
- `bun run typecheck:examples` — pass
- `apps/docs`: `reference:check`, `typecheck`, `test` (snippets +
browser spec), `build`,
`blume validate --strict`, `audit --strict` — pass
- `git diff --check` — clean
- `apps/docs` `registry:check` — fails, and fails identically on `main`
at 62f1e7c: the scratch
shadcn project's `tsc` cannot resolve `@types/react`, with every error
inside third-party
`node_modules` (`@floating-ui/react-dom`, `lucide-react`) and none in a
DomainKit file. S
Saatvik Arya committed
d46e457a0c13416e139c73100cf21ca7516bdf6d
Parent: 62f1e7c
Committed by GitHub <noreply@github.com>
on 9/18/2026, 9:43:17 PM