Skip to content
Little Lab Rats home
build-roadmap

The roadmap answers how long, how much, in what order

Those are the first three things a client asks, and a correct stack of work items still leaves all three open. The roadmap builder takes the reviewed package and answers them: epics and stories over tasks, effort summed from the individual estimates, and every task placed in a month against a real capacity figure.

Rolled up from
8 tasks, 4 stories, 2 epics
Total effort
18.0+ h, a floor
Schedule
3 months at 8 h capacity
Reported gaps
1 unsized task, 1 dropped edge
The problem

A backlog is a set. A plan is an order.

The requirements pass upstream of this produces something genuinely good: every requirement typed and traced to the line it came from, split into items small enough to hand to one person, each sized individually, each labelled must, should, could or won’t, and the whole package reviewed by two agents on different model families.

What that produces is a correct, reviewed, well-sized set. A plan is a sequence, and a sequence is what gets signed: months attached and a total at the bottom. Getting from one to the other takes more than formatting: it needs a rule for rolling effort up, a rule for ordering work that depends on other work, and an answer for the items nobody managed to size.

The last of those is where most estimating tools quietly fail. Summing a column is trivial. Deciding what to do about the row that has no number in it is the part that determines whether the total at the bottom is honest.

What it produces

Three tiers, and the same three tiers every time

One document, validated against delivery-roadmap.schema.json, with a spreadsheet rendered from the same object for the people who will only ever open a spreadsheet.

Epic13.0 h

3. Checkout

The client-facing heading. Its effort is the sum of everything beneath it rather than a figure anybody typed, and it carries no month at all, which is why an epic can never quietly disagree with its own contents.

Story7.5 h

3.1 Card payments

A coherent slice a client can accept or defer as one decision. It carries a scope label of its own, and the month range shown against it is the span its tasks occupy: 2026-09 ~ 2026-10.

Task4.0 h

Tokenise card details at the edge

One unit of work, small enough for one person or one agent. Tasks are the only tier with an estimate of their own and the only tier that gets scheduled. Everything above them is arithmetic.

The same input produces a byte-identical file

There is no model in this stage. Rollups, ordering and month assignment are arithmetic over the package, so the output is generated rather than composed, and the golden file it produces from the fixture is byte-compared in CI. That matters for a duller reason than it sounds: it makes the roadmap diffable. Re-run it after a scope conversation and the change set is the scope conversation. A tool that returns a freshly worded opinion each run cannot be reviewed, only re-read.

The live document

Real output from the real generator

What follows is the roadmap.json the builder emits from its bundled fixture package, rendered field by field. It is transcribed from the golden file rather than written to look plausible, so if the generator changes shape this table stops matching the product.

Generator
build-roadmap@v1
Source package
ctxpkg-fixture-enriched
Schema
v1
Upstream gate
pass
  • E1

    3. Checkout

    epic
    Scope
    no scope classification
    Month
    not scheduled directly
    Effort
    13.0 hours
  • E1-S1

    3.1 Card payments

    story
    Scope
    must
    Month
    2026-09 ~ 2026-10
    Effort
    7.5 hours
  • E1-S1-T1

    Tokenise card details at the edge

    task
    Scope
    must
    Month
    2026-09
    Effort
    4.0 hours
  • E1-S1-T2

    Retry tokenisation on provider timeout

    task
    Scope
    must
    Month
    2026-10
    Effort
    1.5 hours
  • E1-S1-T3

    Validate CVV before authorisation

    task
    Scope
    must
    Month
    2026-10
    Effort
    2.0 hours

    after E1-S1-T1

  • E1-S2

    3.3 Refunds

    story
    Scope
    should
    Month
    2026-11
    Effort
    5.5 hours
  • E1-S2-T1

    Issue partial refunds

    task
    Scope
    should
    Month
    2026-11
    Effort
    5.0 hours
  • E1-S2-T2

    Record an audit row per refund

    task
    Scope
    could
    Month
    2026-11
    Effort
    0.5 hours

    after E1-S2-T1

  • E2

    5. Infrastructure

    epic
    Scope
    no scope classification
    Month
    not scheduled directly
    Effort
    at least 5.0 hours, a partial total
  • E2-S1

    Harden the payment event queue

    story
    Scope
    must
    Month
    2026-09 ~ 2026-10
    Effort
    5.0 hours
  • E2-S1-T1

    Make event consumption idempotent

    task
    Scope
    must
    Month
    2026-09
    Effort
    3.0 hours
  • E2-S1-T2

    Alert when the queue backs up

    task
    Scope
    should
    Month
    2026-10
    Effort
    2.0 hours

    after E2-S1-T1

  • E2-S2

    5.2 Dead letters

    story
    Scope
    no scope classification
    Month
    2026-09
    Effort
    effort unknown, this work was never sized
  • E2-S2-T1

    Retain undeliverable events

    task
    Scope
    no scope classification
    Month
    2026-09
    Effort
    effort unknown, this work was never sized

Total effort, a floor

18.0+ h

Floor, not an estimate. 1 of 8 tasks was never sized, so the real figure is this or higher. The unsized work is reported by identifier rather than counted as zero.

Epics
2
Stories
4
Tasks
8
Months
3

Hours by month

2026-09
7.0 h
2026-10
5.5 h
2026-11
5.5 h

Hours by scope

must
10.5 h
should
7.0 h
could
0.5 h
unclassified
? h

A bucket holding unsized work is reported as a floor, or as unknown where that is all it holds.

Read the last row

One task reads ? h, and that is the point

E2-S2-T1 was never sized. The work-breakdown agent had not run on this package, so there is no estimated_minutes and no bite_size to fall back on. The generator reports the effort unknown, names the task in the report, and marks every total above it a floor: the epic reads 5.0+ h and the grand total reads 18.0+ h.

Rendering that cell as 0.0 h would have been one line of code and it would have made the plan look complete. It would also have quietly promised the work for free, because a zero in an effort column is not read as “unknown”, it is read as “nothing”. Every downstream total would have inherited the lie, and the first person to notice would have been whoever had to build the thing.

This is the part worth buying. Any tool can total a column. A plan that tells you precisely where it does not know is worth more than a plan that looks finished, and it is the only kind you can safely sign.

Warnings

1
  • effort is unknown for 1 of 8 tasks: the work-breakdown agent had not run on this package, so totals are a floor rather than an estimate

Unestimated tasks

1
  • E2-S2-T1

Dropped dependencies

1
  • act-queue-001b -> act-queue-001 (target not in roadmap)

The dropped edge is worth reading too. That dependency pointed at a parent work item rather than at a task, and parents are not schedulable, so the edge could not be honoured. It was dropped and recorded. A tool that tells you what it discarded is easier to trust than one that silently does the same thing.

Where the hours come from

Three sources, checked in a fixed order

Effort is never invented at this stage. For every task the generator asks the same two questions in the same order, and if both come back empty it says so.

  1. 1execution_brief.estimated_minutes5 of 8 tasks here

    An explicit estimate written against the work item, converted to hours. This is the only figure anybody actually intended as an estimate, so it wins outright wherever it exists.

  2. 2bite_size2 of 8 tasks here

    The size band the work-breakdown agent assigned, mapped to a fixed number of hours: xs to 0.5, s to 2, m to 4. Coarse, but it is a deliberate statement about size rather than a by-product of something else.

  3. 3unknown1 of 8 tasks here

    Neither field present, so the effort is reported unknown and the task is named in the report. Nothing is guessed, interpolated from siblings, or averaged, because a guess here is indistinguishable from an estimate once it reaches a spreadsheet.

The field it refuses to read

effort_hint is ignored on purpose

There is a third field sitting right there, and using it would take the unknown count in this sample to zero. The roadmap builder will not touch it, because the assembler derives effort_hint from the character count of the source text. A requirement somebody wrote three paragraphs about reads as a large task. A one-line requirement with a fortnight of work behind it reads as a small one. It measures how much was written, not how much there is to do.

Reading it would improve exactly one number, the proportion of tasks with an estimate, and degrade the only one that matters. Leaving the effort unknown is less flattering and more useful.

Where the months come from

Sorted by dependency, then packed against capacity

Every depends_on edge between tasks becomes an edge in a graph, and the graph is topologically sorted, which orders every task after the work it depends on. Each one therefore lands in the same month as the work it needs, or a later one. In this sample, validating the CVV depends on tokenising the card, so tokenisation takes September and validation waits for October.

Note what a dependency does not do. Both refund tasks land in 2026-11 even though one depends on the other, because an edge forbids running earlier, it does not demand a fresh month. The pair costs 5.5 h together and fits inside one.

Tasks are then packed into months in that order against a capacity figure. A cycle cannot be sorted, so where one exists it is broken at a deterministic point and reported rather than resolved quietly. Same input, same break, same file.

This run, at 8 h per month

  • 2026-097.0 / 8.0 h
  • 2026-105.5 / 8.0 h
  • 2026-115.5 / 8.0 h

September stops an hour short of the cap, and that hour is not slack anybody missed. The cheapest unblocked task left costs 1.5 hours, and the one 0.5-hour task in the plan sits behind a 5-hour predecessor that does not fit either. Nothing legal fits, so October opens.

For whoever owns the plan

A document that stays current as inputs change

The reason to generate a roadmap rather than draw one is that a generated plan keeps its wiring. Every row can still answer where it came from, and re-running the pass produces a diff instead of a fresh opinion, which is the difference between a document you maintain and a document you replace.

  1. Source packagectxpkg-fixture-enriched
  2. Action itemact-card-001
  3. TaskE1-S1-T1
  4. Month2026-09

Every row answers “why is this here?”

Each task carries the identifiers of the action items it came from, and each of those carries a link to the source it was extracted from. Every row keeps that chain, so nobody has to defend a line of the plan from memory in a steering meeting.

A change produces a reviewable diff

The same inputs produce a byte-identical file, and the sample on this page is compared byte for byte in our own CI. So after the next meeting you get a delta you can read line by line, rather than a new plan whose differences you have to go looking for.

Dropped edges are recorded, not swallowed

Where a dependency could not be honoured the run says which one and why. This sample carries one such entry: a dependency pointing at a parent work item rather than a task, which is not schedulable.

act-queue-001b -> act-queue-001 (target not in roadmap)
The limits

Four things this document will not do for you

A generated plan is easy to oversell. These are the limits, stated here rather than discovered later.

Pricing is the proposal's job

The output is effort in hours and a month per task. Turning that into a figure on a contract involves rates, risk appetite, and margin, none of which live in this document. The roadmap is an input to a price rather than a price.

See how a proposal is built

Judgement about risk stays with a person

Capacity is a flat number of hours per month. It knows nothing about leave, onboarding, the integration that always goes wrong, or which of these tasks the team has done before. A delivery manager still owns the plan.

Sizing needs a broken-down backlog

There is no model in the loop inventing a number for unsized work. If the breakdown never reached a task, the roadmap reports it unknown and marks the totals a floor. That is the limit working correctly.

It sequences the dependencies you state

The order respects the depends_on graph it was handed. If nobody recorded that the migration has to precede the cutover, the plan will schedule them in whichever order capacity suggests. What was left unsaid stays unknown to it.

See it run

Everything above came out of the pipeline itself

The table on this page is a real roadmap.json, generated by build-roadmap@v1 from package ctxpkg-fixture-enriched. The demo runs the same path end to end: meeting notes in, requirements assembled and reviewed, then this roadmap derived from them, streaming each stage as it lands. You can download the workbook at the end.

Run the pipeline

Tell us what you’re building

Bring us a problem, a half-formed idea, or a system that has outlived its stack. We will tell you honestly whether we are the right team for it.