The semantic layer (ADR-0021) everything on the manager side reads. Dashboards bind to datasets, never to objects directly — a widget with a dangling binding renders an empty chart and reports success.
Files you own
src/datasets/*.dataset.ts (new), pushed into dulyDatasets in src/datasets/index.ts
test/datasets.test.ts (new)
Datasets
duly_duty_health — the on-time picture.
- dimensions:
business_unit, owner, period_key, frequency, source
- measures: tasks due, done, done on time, late, skipped, on-time rate
On time = completed_at on or before due_date + grace_days. Late = past that and not done. Both derive from the stored, indexed columns — do not add a flag to duly_task to make this easier.
duly_stagnation — the early-warning picture.
- dimensions:
business_unit, owner
- measures: open tasks, open tasks untouched >7d, >14d, >30d, oldest
last_update_at
duly_workload — forward look, for spotting a period that is about to overload.
- dimensions:
business_unit, owner, due_date bucketed by week and month
- measures: tasks due
The filter that must be on every governed measure
source IN ('catalog', 'assigned').
self duties are the owner's own record-keeping. They are surfaced, never scored. A single on-time rate that quietly includes self-declared work punishes exactly the people who declared the most, which is how a system teaches everyone to declare nothing.
source is available as a dimension so someone can look at self-declared work deliberately. It is never in the default measure.
duly_log_entry does not appear in this issue
No dataset reads it. No dataset will. If a later ticket asks for one, that is a product decision, not an analytics one — file needs-user-decision rather than adding it.
Do not build
- any measure that counts items per person for comparison — no "tasks logged", no "most active", no contributor leaderboard dimension. This is the specific mechanism by which the caliber separation gets undone one reasonable-looking ticket at a time.
- a completion-percentage measure. Progress lives in
status and last_update_at; a percentage is a number nobody can verify, which is exactly why it becomes the number everyone reports.
Acceptance
- every governed measure is filtered to
source IN ('catalog','assigned'); assert it in the test
- on-time honours
grace_days — a task completed inside grace counts on time
- stagnation buckets are computed from
last_update_at, and an untouched-but-not-yet-due task still appears (stagnation is not lateness)
- no dataset references
duly_log_entry
- no measure ranks or compares item counts across people
Gates
pnpm validate && pnpm typecheck && pnpm test && pnpm build.
The semantic layer (ADR-0021) everything on the manager side reads. Dashboards bind to datasets, never to objects directly — a widget with a dangling binding renders an empty chart and reports success.
Files you own
src/datasets/*.dataset.ts(new), pushed intodulyDatasetsinsrc/datasets/index.tstest/datasets.test.ts(new)Datasets
duly_duty_health— the on-time picture.business_unit,owner,period_key,frequency,sourceOn time =
completed_aton or beforedue_date + grace_days. Late = past that and not done. Both derive from the stored, indexed columns — do not add a flag toduly_taskto make this easier.duly_stagnation— the early-warning picture.business_unit,ownerlast_update_atduly_workload— forward look, for spotting a period that is about to overload.business_unit,owner,due_datebucketed by week and monthThe filter that must be on every governed measure
source IN ('catalog', 'assigned').selfduties are the owner's own record-keeping. They are surfaced, never scored. A single on-time rate that quietly includes self-declared work punishes exactly the people who declared the most, which is how a system teaches everyone to declare nothing.sourceis available as a dimension so someone can look at self-declared work deliberately. It is never in the default measure.duly_log_entrydoes not appear in this issueNo dataset reads it. No dataset will. If a later ticket asks for one, that is a product decision, not an analytics one — file
needs-user-decisionrather than adding it.Do not build
statusandlast_update_at; a percentage is a number nobody can verify, which is exactly why it becomes the number everyone reports.Acceptance
source IN ('catalog','assigned'); assert it in the testgrace_days— a task completed inside grace counts on timelast_update_at, and an untouched-but-not-yet-due task still appears (stagnation is not lateness)duly_log_entryGates
pnpm validate && pnpm typecheck && pnpm test && pnpm build.