Lesson 0001 · The merge decision
One distinction decides the merge question, and your schema already contains the evidence. Ten minutes.
By the end you can state what a sprint is allowed to store in this schema, name the two columns to delete first, and give a one-sentence answer to "should we merge Sprints & Agile with Tasks?" that survives a code review.
A work item has an owner, a status, and a life. It moves through columns. It gets assigned, estimated, commented on, and closed.
A timebox has a window, a goal, and a membership question. It does not get assigned. It does not move through columns. You do not close a timebox — it ends, or you cancel it.
A sprint is a timebox. Everything interesting about it is a
query: "which work items fell inside this window?" That query is a
WHERE clause over tasks, not a row in a sibling
table.
Read your own schema with that lens and the shape stops being arbitrary:
pm_sprints.status is CHECK-constrained to
'Planned' | 'Active' | 'Completed' — three lifecycle values, versus
tasks.status_column which is free text with eight live spellings.
Different lifecycles. Different kinds of thing.pm_sprints has no owner_id. Nothing
in your docs explains this, and it is the schema telling you the truth.tasks.sprint_id is a nullable FK to pm_sprints with
ON DELETE SET NULL — the relationship is a task pointing at a
container, never the reverse.Merging a timebox into a work item is a category error: you would be adding
columns with a different lifecycle to a table with its own. The row would have
to answer "who owns this?" and "is this done?" and "is this finished?" — three
questions with three different vocabularies — and you would get exactly what
status_column already gave you.
So: do not merge the tables. pm_sprints is a
correctly-shaped table. The error is not its existence.
Here is the part that changes your product, not just your schema. The 2020 Scrum Guide is the normative source, and in it the Sprint's commitment is the Sprint Goal — a qualitative objective. There is no numeric commitment. There is no velocity. There is no story points.
Sutherland, on why the Guide's Commitment was removed in 2017 and reintroduced qualitatively in 2020:
"Unfortunately, some managers weaponized team velocity. They criticized teams for delivering a point less or sometimes even for delivering a point more… Systems have variability and if you try to control normal variability, the system will spin out of control."
Now look at what you have built:
capacity_points numeric(6,2) DEFAULT 40 committed_points numeric(6,2) DEFAULT 0 completed_points numeric(6,2) DEFAULT 0
Those three columns are a velocity weapon, pre-aimed, with a default capacity of 40 points. They are rendered on the screen (sprints page lines 100 and 107). They are never computed from anything. They are typed constants waiting for someone to start comparing people against them — which is precisely the outcome Sutherland is describing, shipped before a single sprint exists.
Goodhart's law: "When a measure becomes a target, it ceases to be a good measure." LVC WorkOS is sold to HR buyers. A points number rendered next to a person's name on a delivery board will be gamed, and you will have shipped the gaming surface. The SPACE framework (ACM Queue 2021) exists to argue that productivity needs five dimensions in tension, not one — and one of its cautionary tales is a team that set a PR turnaround SLA and destroyed its own coding time.
You do not need points to know whether a team is overloaded.
capacity.service.ts already computes it, in hours, from real HR data:
base capacity, minus holidays, minus approved leave, compared against
allocations plus task estimates plus logged timesheet hours. It emits
OVERALLOCATED / OPTIMAL / UNDERALLOCATED /
BENCH.
That is a measure of effectiveness, not a proxy. It cannot be
gamed by re-pointing a ticket. capacity_points is a proxy for
something you already measure directly — which is the definition of a redundant
metric.
| Question | Answer | Why |
|---|---|---|
Merge pm_sprints into tasks? |
No | Different lifecycles, no owner, different cardinality. A category error. |
Merge the /pm/sprints and /tasks screens? |
Yes | Two boards over one table with two status vocabularies is the actual bug. |
| Keep the three points columns? | No | Velocity as a commitment. Capacity is already measured in hours, properly. |
| Keep the sprint table at all? | Yes, if | Only if it holds a goal and a window. That is worth building. |
| Keep story points? | Yes, if | For planning inside the team. Not for reporting up, never per person, and drop the DEFAULT 3. |
Two connections are already in the schema and correct. Three are missing. One is wrong.
pm_sprints.project_id and pm_sprints.milestone_id are
right. A sprint terminates at a milestone; that is the only honest relationship
between a window and a point.capacity_points with a call to the capacity service scoped to the
sprint window. The sprint then answers "did this team have room?" — the only
question its capacity field was ever asked to answer.pm_sprints.organization_id is nullable while
project_id is NOT NULL, and RLS on this table is
USING (true) like the other 95 policies in the schema. A sprint
written without an org is unfilterable and therefore visible to every
tenant. Add NOT NULL plus a backfill before the first
writer lands.Retrieval, not recognition. Answer before you expand anything.
In the 2020 Scrum Guide, what is the Sprint's commitment?
The board filters with t.sprint_id === s.id || t.project_id === s.project_id. What does it show?
Should pm_sprints be merged into the tasks table?
What already answers "can this team take on more work?" in this codebase?
Without opening the wiring map: which four columns or tables in this
module have zero writers, and which one column in
pm_sprints would be the tenancy hazard?
Then check Reference 0002 §3 and §6.
The primary source, and it is short: the 2020 Scrum Guide. Read the Sprint section and Commitment: Sprint Goal. Note what is absent — no points, no velocity, no capacity. Then read Sutherland's answers in the InfoQ interview, especially the one on weaponised velocity, and confirm for yourself that it is a direct description of your schema.
Ask me anything. Specifically worth asking: how to model the
sprint goal so it is not another number; whether to delete the points columns
or just stop rendering them; whether milestone_id on
pm_sprints is redundant given tasks.milestone_id;
and how to sequence the migration so no tenant sees a blank sprint.
Next lesson: Action Required. The docs call it your signature concept; the code admits it has no column. Deciding whether to actually build it is the same argument again — a distinct concept needs a distinct home, not a field faked from an existing one.