Skip to content

Analytics datasets: duty health, stagnation, workload - #45

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-9-analytics-datasets
Sep 1, 2026
Merged

os-warren merged 3 commits into
mainfrom
claude/issue-9-analytics-datasets

Conversation

@os-warren

@os-warren os-warren commented Sep 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #9

The ADR-0021 semantic layer the manager side reads: three datasets pushed into
dulyDatasets, plus test/datasets.test.ts. Gates below were run on 634a5be, the
final commit.

What shipped

Dataset Dimensions Measures
duly_duty_health business_unit, owner, period_key, frequency (via duty join), source tasks_due, tasks_done, tasks_skipped
duly_stagnation business_unit, owner open_tasks, untouched_over_7d, untouched_over_14d, untouched_over_30d, oldest_last_update_at
duly_workload business_unit, owner, due_week, due_month tasks_due

Everything is a dimension/measure definition. There is no TypeScript reduce over query
results anywhere in the diff.

Shapes #10 needs to know before binding a widget

  • The stagnation buckets are CUMULATIVE, not disjoint. untouched_over_14d counts
    everything untouched_over_30d counts. Do not sum them and do not pie-chart them —
    stack them as thresholds, or difference them in the widget.
  • due_week and due_month are one column at two granularities, not two columns.
    Group by one or the other; crossing them gives one populated cell per week.
  • tasks_due is identical in both datasets that declare it — governed sources,
    cancelled excluded. One name, one meaning; pinned by a test so it cannot drift.
  • oldest_last_update_at is a timestamp, not a score. It answers "what is the worst
    thing here" by naming a date, not a person.
  • There is no on-time rate, no tasks_done_on_time and no tasks_late. See below —
    a dashboard cannot bind them yet, and that is a platform gap, not an oversight.

The on-time measures are absent, and filed upstream

The card's remaining three measures all reduce to one comparison:

completed_at <= due_date + duty.grace_days

One third of that works and two thirds do not, so it is worth splitting:

  1. Reaching the related field works. include: ['duty'] plus a dotted
    duty.someField path is what the semantic layer is for. The shipped frequency
    dimension is bound to duty.frequency and is the working proof, not an assumption.
  2. Column-to-column comparison is refused on the SQL path. { $lte: { $field: 'due_date' } }
    is declared, and filter.zod.ts's own "Execution support (#5041)" block records that
    driver-sql rejects it with INVALID_FILTER / 400 while the in-memory evaluator
    resolves it. For a dataset that split is worse than a uniform gap: the same measure
    would answer on a memory driver and 400 on a SQL one.
  3. There is no date arithmetic in the grammar at all. FILTER_OPERATORS is closed and
    nothing adds an interval to a column; DATE_MACRO_PARAM_RE is anchored to now, never
    to another column. And here the offset is itself a column. So even if #5222 landed, the
    comparison would still have no spelling.

Filed as objectstack-ai/objectstack#14104.

All three workarounds were rejected on the card's own terms: a denormalised grace_days
or grace_deadline on duly_task is a second writer that drifts (AGENTS.md rule 5); a
TypeScript reduce puts the number outside the layer where no widget can bind it; and an
on-time rate that silently drops grace marks late every task completed inside the grace
its own duty grants — wrong invisibly, which is how a number nobody trusts becomes the
number everybody reports.

Worth noting what the gap costs today: grace_days is authored on duly_catalog_item,
propagated to duly_duty, and read by nothing. This dataset was its only intended
consumer.

Second gap found while verifying: dataset bindings are not checked at all

pnpm validate and pnpm build both exit 0 on a dataset whose base object,
include path, and every dimension/measure field path name nothing. Measured, one
mutation at a time, each confirmed on disk and reverted:

Mutation pnpm validate
field: 'period_key' → 'period_kee' exit 0, passed
field: 'duty.frequency' → 'duty.frequenci' exit 0, passed
measure field: 'last_update_at' → 'last_update_att' exit 0, passed
filter key last_update_at → last_update_attt exit 0, passed
include: ['duty'] → ['dutee'] exit 0, passed
object: 'duly_task' → 'duly_tsk' exit 0, passed
duplicate measure name (control) exit 1 — caught
'{7_days_ago}' → '{7_fortnights_ago}' (control) exit 1 — caught

The two controls prove datasets really are in the validation path. The macro rejection
even prints at datasets[1].measures[1].filter.last_update_at.$lt — so the walker already
stands on the exact node and reasons about the value; nothing resolves the key, or the
sibling field / include / object.

Filed as objectstack-ai/objectstack#14105. It sits one level below #7529 (closed by
#8902, which refuses a widget to dataset binding that names nothing): a board can now be
proven to point at a real dataset, and that dataset can still point at nothing. Until it
lands, the field paths pinned in test/datasets.test.ts are the only thing between a typo
and a chart that renders empty while every gate reports success.

Caliber

src/datasets/governed.ts holds source IN ('catalog','assigned') in one place, and
every measure in every dataset carries it — with no exception list. There is
deliberately no ungoverned() counterpart: an exception list is the erosion path, since
the next reasonable-looking ticket adds one measure to it.

source stays a dimension on duly_duty_health, so the governed population can be
split by where the work came from ("how much of this unit's load is manager-assigned
rather than role-catalog?"). Self-declared work is surfaced in the operational views
(src/views/task.view.ts carries source as a column and scores nothing) and never in
the metric layer.

One judgement call worth a reviewer's eye. The card says source is available "so
someone can look at self-declared work deliberately". Read strictly, that could argue for
one deliberately ungoverned volume measure. I did not build one: the acceptance criterion
is "every governed measure is filtered", and any measure I marked ungoverned would be a
distinction I invented, which is precisely the loophole a later ticket walks through.
Uniform-and-testable beat flexible here. If the maintainer wants an explicitly ungoverned
surfacing measure, that is a product decision and a follow-up.

Deliberate absences, each pinned by a test

  • No dataset reads duly_log_entry — asserted over keys and values at any depth.
  • No measure ranks or compares item counts across people; no ranking dimension.
  • No completion-percentage measure.
  • No stagnation measure mentions due_date. Stagnation is not lateness: an untouched
    task that is not yet due still stagnates, which is the whole reason the signal beats a
    percentage.
  • No measure uses $field — a tripwire for the workaround in gap 2 above.
  • Nothing reads is_late / is_overdue / a denormalised grace_days.

Verification

pnpm validate && pnpm typecheck && pnpm test && pnpm build — all four exit 0 on
634a5be. 302 tests pass (24 new). The three datasets are present in
dist/objectstack.json with the expected dimension and measure counts.

The guards were ablated rather than trusted. Four mutations, each confirmed on disk
and reverted by a trap:

Mutation Result
drop governed() from one measure 2 tests fail
add 'self' to GOVERNED_SOURCES 1 test fails
add due_date to a stagnation bucket 1 test fails
add a tasks_logged / "Most active" measure 2 tests fail

That third row passed green on the first attempt — a phantom check. The walker used
Object.values, but in a filter condition the column is the key, so the single most
important assertion in the file was asserting nothing. Fixed in 74d1ba6; the table above
is the re-run after the fix. The commit is kept separate so the defect and its fix are
both legible.

File surface

src/datasets/ and test/datasets.test.ts only. objectstack.config.ts and
src/objects/ untouched. src/datasets/governed.ts is a helper module rather than a
*.dataset.ts, still inside the owned directory — flagged for completeness.


Generated by Claude Code

An ablation caught the stagnation guard passing with a due_date condition
injected: the walker used Object.values, but in a filter condition the column
is the key.
objectstack#14104 (no grace-aware on-time comparison) and objectstack#14105
(dataset field bindings unvalidated).
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 05:35
@os-warren
os-warren merged commit 81a3822 into main Sep 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Analytics datasets — duty health, on-time rate, stagnation

1 participant