Analytics datasets: duty health, stagnation, workload - #45
Merged
Merged
Conversation
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
marked this pull request as ready for review
September 1, 2026 05:35
This was referenced Sep 1, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #9
The ADR-0021 semantic layer the manager side reads: three datasets pushed into
dulyDatasets, plustest/datasets.test.ts. Gates below were run on634a5be, thefinal commit.
What shipped
duly_duty_healthbusiness_unit,owner,period_key,frequency(viadutyjoin),sourcetasks_due,tasks_done,tasks_skippedduly_stagnationbusiness_unit,owneropen_tasks,untouched_over_7d,untouched_over_14d,untouched_over_30d,oldest_last_update_atduly_workloadbusiness_unit,owner,due_week,due_monthtasks_dueEverything 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
untouched_over_14dcountseverything
untouched_over_30dcounts. Do not sum them and do not pie-chart them —stack them as thresholds, or difference them in the widget.
due_weekanddue_monthare one column at two granularities, not two columns.Group by one or the other; crossing them gives one populated cell per week.
tasks_dueis 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_atis a timestamp, not a score. It answers "what is the worstthing here" by naming a date, not a person.
tasks_done_on_timeand notasks_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:
One third of that works and two thirds do not, so it is worth splitting:
include: ['duty']plus a dottedduty.someFieldpath is what the semantic layer is for. The shippedfrequencydimension is bound to
duty.frequencyand is the working proof, not an assumption.{ $lte: { $field: 'due_date' } }is declared, and
filter.zod.ts's own "Execution support (#5041)" block records thatdriver-sqlrejects it withINVALID_FILTER/ 400 while the in-memory evaluatorresolves 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.
FILTER_OPERATORSis closed andnothing adds an interval to a column;
DATE_MACRO_PARAM_REis anchored to now, neverto 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_daysor
grace_deadlineonduly_taskis a second writer that drifts (AGENTS.md rule 5); aTypeScript 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_daysis authored onduly_catalog_item,propagated to
duly_duty, and read by nothing. This dataset was its only intendedconsumer.
Second gap found while verifying: dataset bindings are not checked at all
pnpm validateandpnpm buildboth exit 0 on a dataset whose baseobject,includepath, and every dimension/measurefieldpath name nothing. Measured, onemutation at a time, each confirmed on disk and reverted:
pnpm validatefield: 'period_key'→'period_kee'field: 'duty.frequency'→'duty.frequenci'field: 'last_update_at'→'last_update_att'last_update_at→last_update_atttinclude: ['duty']→['dutee']object: 'duly_task'→'duly_tsk''{7_days_ago}'→'{7_fortnights_ago}'(control)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 alreadystands 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.tsare the only thing between a typoand a chart that renders empty while every gate reports success.
Caliber
src/datasets/governed.tsholdssource IN ('catalog','assigned')in one place, andevery measure in every dataset carries it — with no exception list. There is
deliberately no
ungoverned()counterpart: an exception list is the erosion path, sincethe next reasonable-looking ticket adds one measure to it.
sourcestays a dimension onduly_duty_health, so the governed population can besplit 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.tscarriessourceas a column and scores nothing) and never inthe metric layer.
One judgement call worth a reviewer's eye. The card says
sourceis available "sosomeone 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
duly_log_entry— asserted over keys and values at any depth.due_date. Stagnation is not lateness: an untouchedtask that is not yet due still stagnates, which is the whole reason the signal beats a
percentage.
$field— a tripwire for the workaround in gap 2 above.is_late/is_overdue/ a denormalisedgrace_days.Verification
pnpm validate && pnpm typecheck && pnpm test && pnpm build— all four exit 0 on634a5be. 302 tests pass (24 new). The three datasets are present indist/objectstack.jsonwith the expected dimension and measure counts.The guards were ablated rather than trusted. Four mutations, each confirmed on disk
and reverted by a trap:
governed()from one measure'self'toGOVERNED_SOURCESdue_dateto a stagnation buckettasks_logged/ "Most active" measureThat 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 mostimportant assertion in the file was asserting nothing. Fixed in
74d1ba6; the table aboveis the re-run after the fix. The commit is kept separate so the defect and its fix are
both legible.
File surface
src/datasets/andtest/datasets.test.tsonly.objectstack.config.tsandsrc/objects/untouched.src/datasets/governed.tsis a helper module rather than a*.dataset.ts, still inside the owned directory — flagged for completeness.Generated by Claude Code