Prototype workshop
Measure audit
Every measure in MEASURES.md, audited against the columns the seed actually carries. A measure is FABRICATED when the seed lacks a column it needs and a visual renders a number anyway — from a proxy column, a random generator, or a different measure reused. A plausible number nobody can compute is worse than an empty one, because nobody questions it. Read-only: nothing was fixed, no column was added, no visual was hidden, and the specification pack is untouched.
| Measure | Folder | Columns it needs | Columns the seed has | Verdict | Source of the rendered number |
|---|---|---|---|---|---|
| Projects | Counts | Project[Project ID], Project[statecode/Status] | ProjectRow.projectId, ProjectRow.status | COMPUTABLE | — |
| ✱ Projects in delivery | Counts | Project snapshot[Stage], Project snapshot[Snapshot date], Stage[Stage group] | ProjectSnapshotRow.stage, .snapshotDate; STAGES carries stageGroup | COMPUTABLE | — |
| Projects created | Counts | Project[Created on] | ProjectRow.createdOn | COMPUTABLE | — |
| Projects closed | Counts | Project snapshot[Status] across consecutive snapshot dates — a status TRANSITION | Project snapshot now carries one row per project per day with the status held on that date, so a transition is readable. closureTransitions() in src/model/snapshots.ts returns the day each project's status changed into Closed or Delivered. | COMPUTABLE | Was FABRICATED — the measure read Project[Closed on] on the dimension because every historic snapshot row carried the project's final status. The snapshot was rebuilt with real history and the measure now counts transitions. It returns 58 for the trailing twelve months, the same figure the proxy returned, which corroborates the rebuild rather than replacing one number with another. |
| Requests | Counts | Request[Created on] | RequestRow.createdOn | COMPUTABLE | — |
| Requests qualified | Counts | Request[statuscode] | RequestRow.status | COMPUTABLE | — |
| Open actions | Counts | Action[Action status] | ActionRow.status, ActionStatusRow.open | COMPUTABLE | — |
| Overdue actions | Counts | Action[Due date], Action[Action status] | ActionRow.dueDate, .status | COMPUTABLE | — |
| Projects with no activity | Counts | Project[Last activity date] | ProjectRow.lastActivityDate | COMPUTABLE | — |
| Restricted projects | Counts | Project[Restriction] | ProjectRow.restriction | COMPUTABLE | — |
| Gates passed | Schedule | Project gate[Gate status], [Actual date] | ProjectGateRow.gateStatus, .actualDate | COMPUTABLE | — |
| Gates passed on time | Schedule | Project gate[Actual date], [Planned date] | ProjectGateRow.actualDate, .plannedDate, .varianceDays | COMPUTABLE | — |
| On-time gate rate | Schedule | Project gate[Actual date], [Planned date], Date[Date] for the 90-day window | All present | COMPUTABLE | — |
| Gate variance days | Schedule | Project gate[Planned date], [Actual date] | ProjectGateRow.varianceDays, stored equal to actual minus planned | COMPUTABLE | — |
| Gates overdue | Schedule | Project gate[Gate status], [Planned date] | Both present | COMPUTABLE | — |
| Gates stalled | Schedule | Project gate[Gate status], [Planned date] | Both present | COMPUTABLE | — |
| Days in current stage | Schedule | A STAGE-ENTRY DATE: the snapshot date on which the project first held its present stage. DATEDIFF(that date, TODAY()). | Project snapshot carries one row per project per day with the stage held on that date, and no pre-computed answer. stageStandings() in src/model/snapshots.ts walks back to the first day of the run at the project's present stage and returns that DATEDIFF, flagging rows where the run reaches the first retained day rather than guessing past it. | COMPUTABLE | Was FABRICATED — a per-stage normal draw written onto the snapshot as the answer. The draw now sets the date the project entered its stage, and the measure computes the DATEDIFF. Mean moves from 18.6 to 28.4 days: the old figure was decremented by 7 per week walked back, which understated it. |
| Median days in stage | Schedule | The same stage-entry date, per project, then MEDIANX | Same as above, over the same snapshot history | COMPUTABLE | Was FABRICATED. This is the bar length on 3.2 Stage ageing; src/model/volume.ts now takes the median over stageStandings rather than over a stored column. Median moves from 12 to 23 days. |
| Projects overdue against target completion | Schedule | Project[Target completion], Project[Status] | ProjectRow.targetCompletion, .status | COMPUTABLE | — |
| Request-to-route cycle time | Flow | Request[Created on], Request[Route decided on] | RequestRow.createdOn, .routeDecidedOn | COMPUTABLE | — |
| Request-to-route cycle time (90th percentile) | Flow | The same two columns | Both present | COMPUTABLE | — |
| Request-to-qualification cycle time | Flow | Request[Created on], Request[Qualified on] | RequestRow.createdOn, .qualifiedOn | COMPUTABLE | — |
| Qualification rate | Flow | Request[statuscode], Request[Created on] | Both present | COMPUTABLE | — |
| Action age days | Flow | Action[Created on], Action[Action status] | Both present | COMPUTABLE | — |
| Median overdue age | Flow | Action[Due date], Action[Action status] | Both present | COMPUTABLE | — |
| Days blocked | Flow | A blocking-start date per cause, and a Blocker cause value on the fact. Seven causes, seven different clocks. | Each blocker row now carries its own cause key and a blockedSince date, derived from the row that actually holds the cause — the action, the gate, the approval step or the person-week. Days blocked is a DATEDIFF from it. | COMPUTABLE | Was FABRICATED — seven causes borrowing clocks behind a single fallback of days-since-last-activity. Each clock now runs from the moment its own condition became true, per MODEL.md §4: due date; planned date + 14; raised + SLA; assigned to the customer contact; assigned to the partner contact; the Monday the resource's unbroken over-committed run began; last activity + 30. The fallback at src/model/flow.ts:214 is gone. |
| Approval steps unresolved beyond SLA | Flow | Approval step[Raised on], [SLA days], [Resolved on] | ApprovalStepRow.raisedOn, .slaDays, .resolvedOn | COMPUTABLE | — |
| Planned hours | Effort | Assignment[Planned hours], Assignment[Week start] | AssignmentRow.plannedHours, .weekStart | COMPUTABLE | — |
| Available hours | Effort | Capacity[Available hours], Capacity[Week start] | CapacityRow.availableHours, .weekStart | COMPUTABLE | — |
| Unallocated hours | Effort | The two above | Both present | COMPUTABLE | — |
| Over-allocated hours | Effort | The two above, per person | Both present, keyed on Person email | COMPUTABLE | — |
| Allocation against capacity | Effort | The two above | Both present | COMPUTABLE | — |
| People over-allocated | Effort | Assignment[Planned hours], Capacity[Available hours], Person[Person ID] | All present | COMPUTABLE | — |
| People with no allocation | Effort | Person[Active], Assignment[Planned hours] | PersonRow.active, AssignmentRow.plannedHours | COMPUTABLE | — |
| ✱ Projects per project leader | Effort | Project[Project Leader], Person[Person ID], and a definition of "lead-role context" | ProjectRow.projectLeader (an email), PersonRow.personId, PersonRow.role | COMPUTABLE | — |
| Feedback completion rate | Quality | A feedback-captured flag, Project[Closed on] | ProjectRow.feedbackCaptured, .closedOn | COMPUTABLE | — |
| Projects closed without feedback | Quality | The same two columns | Both present | COMPUTABLE | — |
| Clearances expiring within 30 days | Quality | Person[Clearance], Person[Clearance expiry] | PersonRow.clearance, .clearanceExpiry | COMPUTABLE | — |
| ✱ Clearances expired | Quality | Person[Clearance], Person[Active] | Both present | COMPUTABLE | — |
The four fabrications, and what became of them
Three of the four shared one cause: the snapshot wrote each project's final status and a drawn stage age onto every historic row, so no project had ever moved. The snapshot was rebuilt as one row per project per day carrying the stage held on that date, and Days in current stage, Median days in stage and Projects closed are now read from it. The fourth, Days blocked, has one clock per cause running from the day that cause's own condition became true. 0 measures remain FABRICATED.
Checked for DEGENERATE
Every measure was evaluated over a twelve-month, a three-month and a one-month window, and every seed column scanned for single-valued and two-valued distributions. No measure scores DEGENERATE. Two findings sit just short of it and are recorded rather than fixed:
- On-time gate rate returns the same 62.6% on all three windows: the measure reads every gate ever passed and ignores the period. Its values do carry information — the twelve-month trend on 4.6 runs 46, 65, 54, 88, 59, 50, 58, 59, 59, 63, 57 — so it is filter-blind, not degenerate. The card and the line disagree by construction.
- Resource unavailable raises no blocker row in this seed. Six people are committed beyond capacity in the current week, but none is a Project Leader with the four consecutive over-committed weeks the cause requires. The cause is empty, not flat.
Fields no measure in MEASURES.md defines
| Field | Where it appears | What the prototype does today | Where the value comes from |
|---|---|---|---|
| Customer | 1.9 attention strip, 2.1 the week ahead, 4.1 blocker table, both drillthroughs | Rendered as its own column, showing "Withheld" where the reader holds no clearance for the project's restriction. | ProjectRow.customer, drawn from the CUSTOMERS pool in src/mocks/seed/names.ts and passed through customerLabel() in src/model/customer.ts. MEASURES.md defines no measure over it; MODEL.md names the account table for the Customer dimension. |
| Decision needed | 1.9 attention strip | Renders a sentence telling the reader what to do about the row. | A hardcoded four-entry lookup keyed on the attention signal. No data at all: CAUSE_DECISION in src/pages/OverviewPage.tsx:77, e.g. "Gate overdue": "Agree a new gate date, or escalate to the gate owner". Four signals are covered; the seven blocker causes on page 4 are not. |
| Project Leader (behind 2.3) | 2.3 Project Leaders by stage, 4.1 owner column, blocker ownership | Groups active projects by lead and stage group, top ten leads. | ProjectRow.projectLeader, an email chosen at random from the project's i-Hub in src/mocks/seed/index.ts:313 (const lead = pick(rng, leads)) and joined to Person by email. MEASURES.md defines no lead measure, and MODEL.md names no lead column on seco_project — only a hidden "Project Leader email" relationship key. |
| Request status — Qualified, Rejected, Raised | Request funnel (proposal screen) | Counts each state that exists in the window. No fourth state is invented to round the funnel out. | RequestRow.status, hardcoded by the generator: every request that became a project is written "Qualified" (src/mocks/seed/index.ts:757); the 80 that did not are "Rejected" on a 65% coin flip, otherwise "Raised" (line 775). The split is a seed constant, not a modelled transition. |
| Route decision timestamp | 1.5 request-to-route cycle time, and the funnel's route step | Feeds a median and a 90th percentile in days. | RequestRow.routeDecidedOn, generated as created + qualification days + a normal(6, 5) draw (src/mocks/seed/index.ts:747). The column exists in the seed, so the measure is computable — but MODEL.md names no route-decision column on seco_request, which is why the data source register flags 1.5. |
DAX intents too vague to audit
- Days blocked — the intent is "Cause-specific; see MODEL.md §4". Seven causes, no formula for any of them, and no statement of which date starts the clock. Not guessed here.
- Days in current stage — "DATEDIFF from the stage-entry snapshot to today" does not say how stage entry is identified from a snapshot table, nor whether the result is a per-project column or an aggregate. The two readings give different numbers.
- Projects closed — "COUNTROWS filtered on statuscode transitions" names neither the transition nor the date the period filters on.
- Projects in delivery — "at the latest snapshot date" is ambiguous between the global maximum and the maximum per project. Materially different on closed projects.
- Projects per project leader — "in a lead-role context" is undefined, and MODEL.md names no lead column on seco_project.
- Clearances expired — "As above, past expiry" inherits the previous row's intent; the clearance relationship it counts over is not named.
What the three verdicts mean
| COMPUTABLE | Every column the DAX intent needs exists in the seed. |
| MISSING COLUMN | The seed lacks a column the measure needs, and no visual renders it. |
| FABRICATED | The seed lacks a column the measure needs, and a visual renders a number anyway — from a proxy column, a random generator, or a different measure reused. |
| DEGENERATE | Computable from columns that exist, but from values carrying no information — a flat series, a constant, or a single distinct value. A plausible flat line is more dangerous than a blank, because a reader takes it for a finding. |
No measure scored MISSING COLUMN. That is itself the finding: every gap in this model is rendered. Not one of them is left blank, so not one of them announces itself to a reader.