SECO — Operational reportsMeasure audit

0 of 41 decided

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.

COMPUTABLE39MISSING COLUMN0FABRICATED0DEGENERATE0of 39 measures · ✱ marks a DAX intent too vague to audit cleanly (3)
MeasureFolderColumns it needsColumns the seed hasVerdictSource of the rendered number
ProjectsCountsProject[Project ID], Project[statecode/Status]ProjectRow.projectId, ProjectRow.statusCOMPUTABLE—
✱ Projects in deliveryCountsProject snapshot[Stage], Project snapshot[Snapshot date], Stage[Stage group]ProjectSnapshotRow.stage, .snapshotDate; STAGES carries stageGroupCOMPUTABLE—
Projects createdCountsProject[Created on]ProjectRow.createdOnCOMPUTABLE—
Projects closedCountsProject snapshot[Status] across consecutive snapshot dates — a status TRANSITIONProject 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.COMPUTABLEWas 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.
RequestsCountsRequest[Created on]RequestRow.createdOnCOMPUTABLE—
Requests qualifiedCountsRequest[statuscode]RequestRow.statusCOMPUTABLE—
Open actionsCountsAction[Action status]ActionRow.status, ActionStatusRow.openCOMPUTABLE—
Overdue actionsCountsAction[Due date], Action[Action status]ActionRow.dueDate, .statusCOMPUTABLE—
Projects with no activityCountsProject[Last activity date]ProjectRow.lastActivityDateCOMPUTABLE—
Restricted projectsCountsProject[Restriction]ProjectRow.restrictionCOMPUTABLE—
Gates passedScheduleProject gate[Gate status], [Actual date]ProjectGateRow.gateStatus, .actualDateCOMPUTABLE—
Gates passed on timeScheduleProject gate[Actual date], [Planned date]ProjectGateRow.actualDate, .plannedDate, .varianceDaysCOMPUTABLE—
On-time gate rateScheduleProject gate[Actual date], [Planned date], Date[Date] for the 90-day windowAll presentCOMPUTABLE—
Gate variance daysScheduleProject gate[Planned date], [Actual date]ProjectGateRow.varianceDays, stored equal to actual minus plannedCOMPUTABLE—
Gates overdueScheduleProject gate[Gate status], [Planned date]Both presentCOMPUTABLE—
Gates stalledScheduleProject gate[Gate status], [Planned date]Both presentCOMPUTABLE—
Days in current stageScheduleA 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.COMPUTABLEWas 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 stageScheduleThe same stage-entry date, per project, then MEDIANXSame as above, over the same snapshot historyCOMPUTABLEWas 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 completionScheduleProject[Target completion], Project[Status]ProjectRow.targetCompletion, .statusCOMPUTABLE—
Request-to-route cycle timeFlowRequest[Created on], Request[Route decided on]RequestRow.createdOn, .routeDecidedOnCOMPUTABLE—
Request-to-route cycle time (90th percentile)FlowThe same two columnsBoth presentCOMPUTABLE—
Request-to-qualification cycle timeFlowRequest[Created on], Request[Qualified on]RequestRow.createdOn, .qualifiedOnCOMPUTABLE—
Qualification rateFlowRequest[statuscode], Request[Created on]Both presentCOMPUTABLE—
Action age daysFlowAction[Created on], Action[Action status]Both presentCOMPUTABLE—
Median overdue ageFlowAction[Due date], Action[Action status]Both presentCOMPUTABLE—
Days blockedFlowA 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.COMPUTABLEWas 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 SLAFlowApproval step[Raised on], [SLA days], [Resolved on]ApprovalStepRow.raisedOn, .slaDays, .resolvedOnCOMPUTABLE—
Planned hoursEffortAssignment[Planned hours], Assignment[Week start]AssignmentRow.plannedHours, .weekStartCOMPUTABLE—
Available hoursEffortCapacity[Available hours], Capacity[Week start]CapacityRow.availableHours, .weekStartCOMPUTABLE—
Unallocated hoursEffortThe two aboveBoth presentCOMPUTABLE—
Over-allocated hoursEffortThe two above, per personBoth present, keyed on Person emailCOMPUTABLE—
Allocation against capacityEffortThe two aboveBoth presentCOMPUTABLE—
People over-allocatedEffortAssignment[Planned hours], Capacity[Available hours], Person[Person ID]All presentCOMPUTABLE—
People with no allocationEffortPerson[Active], Assignment[Planned hours]PersonRow.active, AssignmentRow.plannedHoursCOMPUTABLE—
✱ Projects per project leaderEffortProject[Project Leader], Person[Person ID], and a definition of "lead-role context"ProjectRow.projectLeader (an email), PersonRow.personId, PersonRow.roleCOMPUTABLE—
Feedback completion rateQualityA feedback-captured flag, Project[Closed on]ProjectRow.feedbackCaptured, .closedOnCOMPUTABLE—
Projects closed without feedbackQualityThe same two columnsBoth presentCOMPUTABLE—
Clearances expiring within 30 daysQualityPerson[Clearance], Person[Clearance expiry]PersonRow.clearance, .clearanceExpiryCOMPUTABLE—
✱ Clearances expiredQualityPerson[Clearance], Person[Active]Both presentCOMPUTABLE—

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

FieldWhere it appearsWhat the prototype does todayWhere the value comes from
Customer1.9 attention strip, 2.1 the week ahead, 4.1 blocker table, both drillthroughsRendered 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 needed1.9 attention stripRenders 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 ownershipGroups 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, RaisedRequest 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 timestamp1.5 request-to-route cycle time, and the funnel's route stepFeeds 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

COMPUTABLEEvery column the DAX intent needs exists in the seed.
MISSING COLUMNThe seed lacks a column the measure needs, and no visual renders it.
FABRICATEDThe 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.
DEGENERATEComputable 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.