Lists what is wrong with a plan - double bookings on a lane, dependencies that do not fit, work in blocked time - from the same data the schedule draws. Reach for it to list, count or filter the problems beside the picture.
import { findings, overlaps, violatedDependencies, inBlockedTime } from "@umriss-ui/schedule";
findings(subtasks, dependencies, blocked) returns all three kinds; overlaps, violatedDependencies and inBlockedTime return one each. They are pure functions: run them wherever the data changes, with or without a schedule on screen.
Lead-in and lead-out count as occupied time; touching at a shared edge is no overlap.
findings returns what the schedule marks - overlaps, violated dependencies, work in blocked time - as data for a list beside it.
import { Stack, Text } from "@umriss-ui/core";
import { findings, useSchedule } from "@umriss-ui/schedule";
import type { BlockedTime, Dependency, Subtask, Task } from "@umriss-ui/schedule";
const day = (d: number, hours = 9) => new Date(2026, 2, d, hours).getTime();
const PROJECTS: readonly Task[] = [
{ id: "shop", name: "Online shop relaunch", color: "light-dark(#2563eb, #6b9bff)" },
{ id: "portal", name: "Member portal", color: "light-dark(#7c3aed, #a98bfa)" },
];
const WORK: readonly Subtask[] = [
{ id: "design", task: "shop", lane: "noah", from: day(16), to: day(17, 17), name: "Search results layout" },
{ id: "build", task: "shop", lane: "chloe", from: day(17), to: day(19, 17), name: "Search results page" },
{ id: "address", task: "portal", lane: "chloe", from: day(18), to: day(19, 17), name: "Change of address form" },
{ id: "profile", task: "portal", lane: "arjun", from: day(17), to: day(19, 17), name: "Profile page" },
];
/* The build may start only a morning after the design is done. */
const HANDOVERS: readonly Dependency[] = [{ id: "design-to-build", from: "design", to: "build", lag: 4 * 60 * 60_000 }];
const LEAVE: readonly BlockedTime[] = [{ id: "arjun-holiday", lane: "arjun", from: day(18, 0), to: day(21, 0), label: "Holiday" }];
const when = (instant: number) => new Date(instant).toLocaleString("en-GB", { weekday: "short", hour: "2-digit", minute: "2-digit" });
const hours = (ms: number) => Math.round(ms / 3_600_000);
export default function ListTheFindings() {
const { Schedule, Lane, Subtasks, Dependencies, BlockedTimes } = useSchedule({ initialView: { domain: [day(16, 0), day(21, 0)] } });
const found = findings(WORK, HANDOVERS, LEAVE);
return (
<Stack gap={3}>
<Schedule ariaLabel="Three people's week with its findings" height={184}>
<Lane id="noah" label="Noah Fischer" />
<Lane id="chloe" label="Chloe Durand" />
<Lane id="arjun" label="Arjun Mehta" />
<BlockedTimes data={LEAVE} />
<Dependencies data={HANDOVERS} />
<Subtasks data={WORK} tasks={PROJECTS} />
</Schedule>
<ul style={{ margin: 0, paddingLeft: 18 }}>
{found.overlaps.map((o) => (
<Text as="li" size="sm" key={`${o.first}/${o.second}`}>
Double-booked: {o.lane} has {o.first} and {o.second} from {when(o.from)} to {when(o.to)}
</Text>
))}
{found.violatedDependencies.map((v) => (
<Text as="li" size="sm" key={v.dependency}>
Too early: {v.dependency} leaves {when(v.departure)} for a start at {when(v.arrival)}, {hours(v.shortBy)} hours short
</Text>
))}
{found.inBlockedTime.map((b) => (
<Text as="li" size="sm" key={`${b.subtask}/${b.blocked}`}>
In leave: {b.subtask} covers {b.blocked} from {when(b.from)} to {when(b.to)}
</Text>
))}
</ul>
</Stack>
);
}
When all three lists findings returns are empty, say so plainly instead of showing an empty list.
import { EmptyState, Stack } from "@umriss-ui/core";
import { findings, useSchedule } from "@umriss-ui/schedule";
import type { Subtask, Task } from "@umriss-ui/schedule";
const day = (d: number, hours = 9) => new Date(2026, 2, d, hours).getTime();
const PROJECTS: readonly Task[] = [{ id: "booking", name: "Booking app", color: "light-dark(#0d9488, #3cc7b8)" }];
const WORK: readonly Subtask[] = [
{ id: "w-111", task: "booking", lane: "hana", from: day(16), to: day(18, 17), name: "Push notifications" },
{ id: "w-113", task: "booking", lane: "kofi", from: day(16), to: day(18, 17), name: "Calendar sync" },
];
export default function NothingToFix() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [day(16, 0), day(21, 0)] } });
const found = findings(WORK, []);
const count = found.overlaps.length + found.violatedDependencies.length + found.inBlockedTime.length;
return (
<Stack gap={3}>
<Schedule ariaLabel="Two developers' week, without findings" height={140}>
<Lane id="hana" label="Hana Sato" />
<Lane id="kofi" label="Kofi Mensah" />
<Subtasks data={WORK} tasks={PROJECTS} />
</Schedule>
{count === 0 && <EmptyState title="Nothing to fix" description="Nobody is double-booked, every hand-over fits, and no work falls into leave." />}
</Stack>
);
}
overlaps and violatedDependencies are the halves of findings on their own - here as counts above the day's legs.
import { Badge, Stack } from "@umriss-ui/core";
import { overlaps, useSchedule, violatedDependencies } from "@umriss-ui/schedule";
import type { Dependency, Subtask, Task } from "@umriss-ui/schedule";
const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const min = (n: number) => n * 60_000;
const VEHICLES = [
{ id: "van-402", label: "Van FP 402 R" },
{ id: "truck-520", label: "Truck FP 520 E" },
{ id: "van-455", label: "E-van FP 455 R" },
];
const CONSIGNMENTS: readonly Task[] = [
{ id: "c-2041", name: "C-2041 Holloway Garden Supplies", color: "light-dark(#2563eb, #6b9bff)" },
{ id: "c-2043", name: "C-2043 Oakridge Pharmacy", color: "light-dark(#c2410c, #f08a52)" },
];
const LEGS: readonly Subtask[] = [
{ id: "c-2041-2", task: "c-2041", lane: "van-402", from: at(8), to: at(10, 30), leadIn: min(30), leadOut: min(15) },
{ id: "c-2043-1", task: "c-2043", lane: "truck-520", from: at(6, 30), to: at(8), leadIn: min(30), leadOut: min(15) },
{ id: "c-2043-2", task: "c-2043", lane: "van-402", from: at(10), to: at(11, 30), leadIn: min(15) },
{ id: "c-2043-3", task: "c-2043", lane: "van-455", from: at(12), to: at(14), leadIn: min(20), leadOut: min(20) },
];
const TRANSFERS: readonly Dependency[] = [
{ id: "t-2043-1", from: "c-2043-1", to: "c-2043-2", lag: min(45) },
{ id: "t-2043-2", from: "c-2043-2", to: "c-2043-3", lag: min(25) },
];
export default function CountPerKind() {
const { Schedule, Lane, Subtasks, Dependencies } = useSchedule({ initialView: { domain: [at(5, 30), at(18)] } });
const doubleBooked = overlaps(LEGS).length;
const late = violatedDependencies(LEGS, TRANSFERS).length;
return (
<Stack gap={3}>
<Stack direction="row" gap={2}>
<Badge tone={doubleBooked > 0 ? "warning" : "success"}>{doubleBooked} double-booked</Badge>
<Badge tone={late > 0 ? "warning" : "success"}>{late} transfers too short</Badge>
</Stack>
<Schedule ariaLabel="The day's legs with their findings counted" height={196}>
{VEHICLES.map((vehicle) => (
<Lane key={vehicle.id} id={vehicle.id} label={vehicle.label} />
))}
<Dependencies data={TRANSFERS} />
<Subtasks data={LEGS} tasks={CONSIGNMENTS} />
</Schedule>
</Stack>
);
}
The keys of First schedule apply here.
Overlap| Name | Type | Default | Description |
|---|---|---|---|
lanerequired | string | — | The lane both sit on. Shown in: List the findings, Refuse a move that double-books someone (Move and lane) |
firstrequired | string | — | The subtask that starts earlier (lead-in included); on a tie, the one that
stands first in the data. Shown in: List the findings, Refuse a move that double-books someone (Move and lane) |
secondrequired | string | — | The other one. Shown in: List the findings, Refuse a move that double-books someone (Move and lane) |
fromrequired | number | — | Where the covering begins. Shown in: List the findings |
torequired | number | — | Where it ends. Shown in: List the findings |
ViolatedDependency| Name | Type | Default | Description |
|---|---|---|---|
dependencyrequired | string | — | The id of the dependency. Shown in: List the findings |
departurerequired | number | — | When it leaves, by its anchor. Shown in: List the findings |
arrivalrequired | number | — | When it has to have arrived, by its anchor. Shown in: List the findings |
shortByrequired | number | — | How much time is missing: departure plus lag, minus arrival. Shown in: List the findings |
InBlockedTime| Name | Type | Default | Description |
|---|---|---|---|
subtaskrequired | string | — | The id of the subtask. Shown in: List the findings, Block out leave and maintenance (Blocked time), Check a sprint against the leave calendar (Blocked time) and 2 more |
blockedrequired | string | — | The id of the blocked time it covers. Shown in: List the findings, Block out leave and maintenance (Blocked time), Plan the sprint around leave (Scenarios) |
lanerequired | string | — | The lane both are on. Shown in: Hand over the on-call week (Scenarios), Plan the sprint around leave (Scenarios) |
fromrequired | number | — | Where the covering begins. Shown in: List the findings, Block out leave and maintenance (Blocked time), Hand over the on-call week (Scenarios) |
torequired | number | — | Where it ends. Shown in: List the findings, Block out leave and maintenance (Blocked time), Hand over the on-call week (Scenarios) |
What umriss deliberately does not build, and why: ADR-0032.