An ADR-0021 dataset measure cannot express a deadline that is another column plus an offset held in a third column — the grace-aware on-time rate has no spelling #14104

Description

@os-warren

Summary

A dataset measure (ui/dataset.zod.ts, ADR-0021) cannot express the single most common
governance metric there is: "was this completed by its deadline, where the deadline is
a stored date plus a grace period that lives on the parent record?"

The concrete comparison, from the duly application (objectstack-ai/duly#9):

completed_at <= due_date + duty.grace_days

completed_at and due_date are columns on the base object duly_task.
grace_days is a number column on the related duly_duty. All three are stored and
indexed. There is no spelling for this in the measure surface, so the on-time rate, the
"done on time" count and the "late" count all have to be dropped from the dataset.

Recorded as a capability gap rather than a bug — nothing misbehaves, and the refusal in
part 2 below is loud and correct. The gap is that the semantic layer cannot say the
thing, and the workarounds available to an application author are all worse than the gap.

What works, so the gap is precisely located

Reaching the related field works, and this is worth stating first — it is the half a
reader will assume is broken. include: ['duty'] plus a duty.<field> path is exactly
what the semantic layer is for: joins are compiled from include (ADR-0071, ≤3 hops) and
the author writes no ON clause. duly_duty_health ships a frequency dimension bound to
duty.frequency today and it validates, builds and appears in the artifact.

So a measure can readduty.grace_days. It cannot compare against it.

What does not work — two independent missing pieces

1. There is no date arithmetic anywhere in the filter grammar

FILTER_OPERATORS (data/filter.zod.ts) is closed: equality, ordering, set, range,
string, null/exists. Nothing adds an interval to a column.

The {N_days_ago} vocabulary is not a substitute — DATE_MACRO_PARAM_RE
(data/date-macros.zod.ts) is anchored to now, never to another column. {7_days_ago}
can express "untouched for a week"; nothing can express "due_date plus seven days".

And here the offset is not even a literal: it is itself a column (duty.grace_days),
which is strictly harder than a fixed interval.

2. Column-to-column comparison is declared but refused on the SQL path

Even setting grace aside, the grace-free form completed_at <= due_date has no usable
spelling either. FieldReferenceSchema declares it:

{completed_at: {$lte: {$field: 'due_date'}}}

but filter.zod.ts's own "Execution support (#5041)" block records that driver-sql (and
driver-sqlite-wasm, which inherits its compiler) reject it with INVALID_FILTER / HTTP
400, while the in-memory evaluator resolves it. SQL compilation is tracked as #5222.

For a dataset that split is worse than a uniform gap. A dataset is a definition that
outlives any one query and is bound by name from dashboards and reports. A measure using
$field would answer on a memory-driver deployment and 400 on a SQL one — the semantic
layer's correctness would become a property of the deployment.

So even if #5222 landed tomorrow, part 1 would still leave due_date + duty.grace_days
unspellable. The two are additive, not alternatives.

Why the application-side workarounds were all rejected

Listing these because each one is what an author reaches for, and each one is how this
gap would have become permanent and invisible:

  • Denormalise grace_days (or a pre-computed grace_deadline) onto the task. A second
    writer that drifts the day a duty's grace is edited. Forbidden by the app's own rules and
    wrong regardless — it converts a definition into a snapshot.
  • Reduce the rate in TypeScript over query results. Puts the number outside the
    semantic layer, where no dashboard widget can bind it — which defeats the point of
    ADR-0021 — and duplicates the definition per consumer.
  • Ship an on-time rate that silently ignores grace. Wrong in the direction that
    matters: it marks late every task completed inside the grace its own duty grants. And
    wrong invisibly, which is how a number nobody trusts becomes the number everybody
    reports.

The application therefore ships the dataset without the on-time measures rather than
with approximated ones.

What the gap currently costs, concretely

In duly, grace_days is authored on duly_catalog_item, propagated to duly_duty at
instantiation, and read by nothing. This dataset was its only intended consumer. It is
a declared, populated, propagated field with zero enforcement — an ADR-0049
declared-but-unenforced shape created purely by the absence of a way to consume it.

Possible directions (not a proposal — the semantics need a ruling)

Sketching these only to show the design space is not empty:

  1. An offset on the field reference{ $lte: { $field: 'due_date', addDays: { $field: 'duty.grace_days' } } }.
    Smallest surface; rides on [spec] SqlDriver 将 $field 编译为列对列比较(cross-field comparison push-down) #5222; keeps the layer SQL-free.
  2. A derived-measure operator over a date difference — the layer already has
    derived: { op, of } for combining measures by name; a days_between measure plus an
    ordering test would be a natural extension of that vocabulary.
  3. A declared computed dimension on the dataset — riskier, since the whole point of
    ADR-0021's author surface is that it is smaller than a query and carries no SQL.

Whichever way it goes, the NULL semantics need stating (a duty with no grace, a task with
no due date) — the same care #5146 took over $not.

Reproduction

objectstack-ai/duly at claude/issue-9-analytics-datasets, pnpm validate. Add to
src/datasets/duty-health.dataset.ts:

{name: 'tasks_done_on_time',aggregate: 'count',filter: {source: {$in: ['catalog','assigned']},status: 'done',completed_at: {$lte: {$field: 'due_date'}},// no way to add duty.grace_days},}

This parses and validates clean — the schema accepts $field — and then refuses at
runtime on any SQL driver. There is no variant of it that adds the grace offset at all.

Environment

@objectstack/spec 17.2.0, @objectstack/cli 17.2.0.

Filed from the duly dogfood application (objectstack-ai/duly#9), which is shipping the
dataset with the affected measures omitted rather than approximated.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    An ADR-0021 dataset measure cannot express a deadline that is another column plus an offset held in a third column — the grace-aware on-time rate has no spelling #14104

    Description

    @os-warren

    Summary

    A dataset measure (ui/dataset.zod.ts, ADR-0021) cannot express the single most common
    governance metric there is: "was this completed by its deadline, where the deadline is
    a stored date plus a grace period that lives on the parent record?"

    The concrete comparison, from the duly application (objectstack-ai/duly#9):

    completed_at <= due_date + duty.grace_days
    

    completed_at and due_date are columns on the base object duly_task.
    grace_days is a number column on the related duly_duty. All three are stored and
    indexed. There is no spelling for this in the measure surface, so the on-time rate, the
    "done on time" count and the "late" count all have to be dropped from the dataset.

    Recorded as a capability gap rather than a bug — nothing misbehaves, and the refusal in
    part 2 below is loud and correct. The gap is that the semantic layer cannot say the
    thing, and the workarounds available to an application author are all worse than the gap.

    What works, so the gap is precisely located

    Reaching the related field works, and this is worth stating first — it is the half a
    reader will assume is broken. include: ['duty'] plus a duty.<field> path is exactly
    what the semantic layer is for: joins are compiled from include (ADR-0071, ≤3 hops) and
    the author writes no ON clause. duly_duty_health ships a frequency dimension bound to
    duty.frequency today and it validates, builds and appears in the artifact.

    So a measure can readduty.grace_days. It cannot compare against it.

    What does not work — two independent missing pieces

    1. There is no date arithmetic anywhere in the filter grammar

    FILTER_OPERATORS (data/filter.zod.ts) is closed: equality, ordering, set, range,
    string, null/exists. Nothing adds an interval to a column.

    The {N_days_ago} vocabulary is not a substitute — DATE_MACRO_PARAM_RE
    (data/date-macros.zod.ts) is anchored to now, never to another column. {7_days_ago}
    can express "untouched for a week"; nothing can express "due_date plus seven days".

    And here the offset is not even a literal: it is itself a column (duty.grace_days),
    which is strictly harder than a fixed interval.

    2. Column-to-column comparison is declared but refused on the SQL path

    Even setting grace aside, the grace-free form completed_at <= due_date has no usable
    spelling either. FieldReferenceSchema declares it:

    {completed_at: {$lte: {$field: 'due_date'}}}

    but filter.zod.ts's own "Execution support (#5041)" block records that driver-sql (and
    driver-sqlite-wasm, which inherits its compiler) reject it with INVALID_FILTER / HTTP
    400, while the in-memory evaluator resolves it. SQL compilation is tracked as #5222.

    For a dataset that split is worse than a uniform gap. A dataset is a definition that
    outlives any one query and is bound by name from dashboards and reports. A measure using
    $field would answer on a memory-driver deployment and 400 on a SQL one — the semantic
    layer's correctness would become a property of the deployment.

    So even if #5222 landed tomorrow, part 1 would still leave due_date + duty.grace_days
    unspellable. The two are additive, not alternatives.

    Why the application-side workarounds were all rejected

    Listing these because each one is what an author reaches for, and each one is how this
    gap would have become permanent and invisible:

    • Denormalise grace_days (or a pre-computed grace_deadline) onto the task. A second
      writer that drifts the day a duty's grace is edited. Forbidden by the app's own rules and
      wrong regardless — it converts a definition into a snapshot.
    • Reduce the rate in TypeScript over query results. Puts the number outside the
      semantic layer, where no dashboard widget can bind it — which defeats the point of
      ADR-0021 — and duplicates the definition per consumer.
    • Ship an on-time rate that silently ignores grace. Wrong in the direction that
      matters: it marks late every task completed inside the grace its own duty grants. And
      wrong invisibly, which is how a number nobody trusts becomes the number everybody
      reports.

    The application therefore ships the dataset without the on-time measures rather than
    with approximated ones.

    What the gap currently costs, concretely

    In duly, grace_days is authored on duly_catalog_item, propagated to duly_duty at
    instantiation, and read by nothing. This dataset was its only intended consumer. It is
    a declared, populated, propagated field with zero enforcement — an ADR-0049
    declared-but-unenforced shape created purely by the absence of a way to consume it.

    Possible directions (not a proposal — the semantics need a ruling)

    Sketching these only to show the design space is not empty:

    1. An offset on the field reference{ $lte: { $field: 'due_date', addDays: { $field: 'duty.grace_days' } } }.
      Smallest surface; rides on [spec] SqlDriver 将 $field 编译为列对列比较(cross-field comparison push-down) #5222; keeps the layer SQL-free.
    2. A derived-measure operator over a date difference — the layer already has
      derived: { op, of } for combining measures by name; a days_between measure plus an
      ordering test would be a natural extension of that vocabulary.
    3. A declared computed dimension on the dataset — riskier, since the whole point of
      ADR-0021's author surface is that it is smaller than a query and carries no SQL.

    Whichever way it goes, the NULL semantics need stating (a duty with no grace, a task with
    no due date) — the same care #5146 took over $not.

    Reproduction

    objectstack-ai/duly at claude/issue-9-analytics-datasets, pnpm validate. Add to
    src/datasets/duty-health.dataset.ts:

    {name: 'tasks_done_on_time',aggregate: 'count',filter: {source: {$in: ['catalog','assigned']},status: 'done',completed_at: {$lte: {$field: 'due_date'}},// no way to add duty.grace_days},}

    This parses and validates clean — the schema accepts $field — and then refuses at
    runtime on any SQL driver. There is no variant of it that adds the grace offset at all.

    Environment

    @objectstack/spec 17.2.0, @objectstack/cli 17.2.0.

    Filed from the duly dogfood application (objectstack-ai/duly#9), which is shipping the
    dataset with the affected measures omitted rather than approximated.

    Activity

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      An ADR-0021 dataset measure cannot express a deadline that is another column plus an offset held in a third column — the grace-aware on-time rate has no spelling #14104

      Description

      @os-warren

      Summary

      A dataset measure (ui/dataset.zod.ts, ADR-0021) cannot express the single most common
      governance metric there is: "was this completed by its deadline, where the deadline is
      a stored date plus a grace period that lives on the parent record?"

      The concrete comparison, from the duly application (objectstack-ai/duly#9):

      completed_at <= due_date + duty.grace_days
      

      completed_at and due_date are columns on the base object duly_task.
      grace_days is a number column on the related duly_duty. All three are stored and
      indexed. There is no spelling for this in the measure surface, so the on-time rate, the
      "done on time" count and the "late" count all have to be dropped from the dataset.

      Recorded as a capability gap rather than a bug — nothing misbehaves, and the refusal in
      part 2 below is loud and correct. The gap is that the semantic layer cannot say the
      thing, and the workarounds available to an application author are all worse than the gap.

      What works, so the gap is precisely located

      Reaching the related field works, and this is worth stating first — it is the half a
      reader will assume is broken. include: ['duty'] plus a duty.<field> path is exactly
      what the semantic layer is for: joins are compiled from include (ADR-0071, ≤3 hops) and
      the author writes no ON clause. duly_duty_health ships a frequency dimension bound to
      duty.frequency today and it validates, builds and appears in the artifact.

      So a measure can readduty.grace_days. It cannot compare against it.

      What does not work — two independent missing pieces

      1. There is no date arithmetic anywhere in the filter grammar

      FILTER_OPERATORS (data/filter.zod.ts) is closed: equality, ordering, set, range,
      string, null/exists. Nothing adds an interval to a column.

      The {N_days_ago} vocabulary is not a substitute — DATE_MACRO_PARAM_RE
      (data/date-macros.zod.ts) is anchored to now, never to another column. {7_days_ago}
      can express "untouched for a week"; nothing can express "due_date plus seven days".

      And here the offset is not even a literal: it is itself a column (duty.grace_days),
      which is strictly harder than a fixed interval.

      2. Column-to-column comparison is declared but refused on the SQL path

      Even setting grace aside, the grace-free form completed_at <= due_date has no usable
      spelling either. FieldReferenceSchema declares it:

      {completed_at: {$lte: {$field: 'due_date'}}}

      but filter.zod.ts's own "Execution support (#5041)" block records that driver-sql (and
      driver-sqlite-wasm, which inherits its compiler) reject it with INVALID_FILTER / HTTP
      400, while the in-memory evaluator resolves it. SQL compilation is tracked as #5222.

      For a dataset that split is worse than a uniform gap. A dataset is a definition that
      outlives any one query and is bound by name from dashboards and reports. A measure using
      $field would answer on a memory-driver deployment and 400 on a SQL one — the semantic
      layer's correctness would become a property of the deployment.

      So even if #5222 landed tomorrow, part 1 would still leave due_date + duty.grace_days
      unspellable. The two are additive, not alternatives.

      Why the application-side workarounds were all rejected

      Listing these because each one is what an author reaches for, and each one is how this
      gap would have become permanent and invisible:

      • Denormalise grace_days (or a pre-computed grace_deadline) onto the task. A second
        writer that drifts the day a duty's grace is edited. Forbidden by the app's own rules and
        wrong regardless — it converts a definition into a snapshot.
      • Reduce the rate in TypeScript over query results. Puts the number outside the
        semantic layer, where no dashboard widget can bind it — which defeats the point of
        ADR-0021 — and duplicates the definition per consumer.
      • Ship an on-time rate that silently ignores grace. Wrong in the direction that
        matters: it marks late every task completed inside the grace its own duty grants. And
        wrong invisibly, which is how a number nobody trusts becomes the number everybody
        reports.

      The application therefore ships the dataset without the on-time measures rather than
      with approximated ones.

      What the gap currently costs, concretely

      In duly, grace_days is authored on duly_catalog_item, propagated to duly_duty at
      instantiation, and read by nothing. This dataset was its only intended consumer. It is
      a declared, populated, propagated field with zero enforcement — an ADR-0049
      declared-but-unenforced shape created purely by the absence of a way to consume it.

      Possible directions (not a proposal — the semantics need a ruling)

      Sketching these only to show the design space is not empty:

      1. An offset on the field reference{ $lte: { $field: 'due_date', addDays: { $field: 'duty.grace_days' } } }.
        Smallest surface; rides on [spec] SqlDriver 将 $field 编译为列对列比较(cross-field comparison push-down) #5222; keeps the layer SQL-free.
      2. A derived-measure operator over a date difference — the layer already has
        derived: { op, of } for combining measures by name; a days_between measure plus an
        ordering test would be a natural extension of that vocabulary.
      3. A declared computed dimension on the dataset — riskier, since the whole point of
        ADR-0021's author surface is that it is smaller than a query and carries no SQL.

      Whichever way it goes, the NULL semantics need stating (a duty with no grace, a task with
      no due date) — the same care #5146 took over $not.

      Reproduction

      objectstack-ai/duly at claude/issue-9-analytics-datasets, pnpm validate. Add to
      src/datasets/duty-health.dataset.ts:

      {name: 'tasks_done_on_time',aggregate: 'count',filter: {source: {$in: ['catalog','assigned']},status: 'done',completed_at: {$lte: {$field: 'due_date'}},// no way to add duty.grace_days},}

      This parses and validates clean — the schema accepts $field — and then refuses at
      runtime on any SQL driver. There is no variant of it that adds the grace offset at all.

      Environment

      @objectstack/spec 17.2.0, @objectstack/cli 17.2.0.

      Filed from the duly dogfood application (objectstack-ai/duly#9), which is shipping the
      dataset with the affected measures omitted rather than approximated.

      Activity

      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

      Metadata

      Metadata

      Assignees

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        An ADR-0021 dataset measure cannot express a deadline that is another column plus an offset held in a third column — the grace-aware on-time rate has no spelling #14104

        Description

        @os-warren

        Summary

        A dataset measure (ui/dataset.zod.ts, ADR-0021) cannot express the single most common
        governance metric there is: "was this completed by its deadline, where the deadline is
        a stored date plus a grace period that lives on the parent record?"

        The concrete comparison, from the duly application (objectstack-ai/duly#9):

        completed_at <= due_date + duty.grace_days
        

        completed_at and due_date are columns on the base object duly_task.
        grace_days is a number column on the related duly_duty. All three are stored and
        indexed. There is no spelling for this in the measure surface, so the on-time rate, the
        "done on time" count and the "late" count all have to be dropped from the dataset.

        Recorded as a capability gap rather than a bug — nothing misbehaves, and the refusal in
        part 2 below is loud and correct. The gap is that the semantic layer cannot say the
        thing, and the workarounds available to an application author are all worse than the gap.

        What works, so the gap is precisely located

        Reaching the related field works, and this is worth stating first — it is the half a
        reader will assume is broken. include: ['duty'] plus a duty.<field> path is exactly
        what the semantic layer is for: joins are compiled from include (ADR-0071, ≤3 hops) and
        the author writes no ON clause. duly_duty_health ships a frequency dimension bound to
        duty.frequency today and it validates, builds and appears in the artifact.

        So a measure can readduty.grace_days. It cannot compare against it.

        What does not work — two independent missing pieces

        1. There is no date arithmetic anywhere in the filter grammar

        FILTER_OPERATORS (data/filter.zod.ts) is closed: equality, ordering, set, range,
        string, null/exists. Nothing adds an interval to a column.

        The {N_days_ago} vocabulary is not a substitute — DATE_MACRO_PARAM_RE
        (data/date-macros.zod.ts) is anchored to now, never to another column. {7_days_ago}
        can express "untouched for a week"; nothing can express "due_date plus seven days".

        And here the offset is not even a literal: it is itself a column (duty.grace_days),
        which is strictly harder than a fixed interval.

        2. Column-to-column comparison is declared but refused on the SQL path

        Even setting grace aside, the grace-free form completed_at <= due_date has no usable
        spelling either. FieldReferenceSchema declares it:

        {completed_at: {$lte: {$field: 'due_date'}}}

        but filter.zod.ts's own "Execution support (#5041)" block records that driver-sql (and
        driver-sqlite-wasm, which inherits its compiler) reject it with INVALID_FILTER / HTTP
        400, while the in-memory evaluator resolves it. SQL compilation is tracked as #5222.

        For a dataset that split is worse than a uniform gap. A dataset is a definition that
        outlives any one query and is bound by name from dashboards and reports. A measure using
        $field would answer on a memory-driver deployment and 400 on a SQL one — the semantic
        layer's correctness would become a property of the deployment.

        So even if #5222 landed tomorrow, part 1 would still leave due_date + duty.grace_days
        unspellable. The two are additive, not alternatives.

        Why the application-side workarounds were all rejected

        Listing these because each one is what an author reaches for, and each one is how this
        gap would have become permanent and invisible:

        • Denormalise grace_days (or a pre-computed grace_deadline) onto the task. A second
          writer that drifts the day a duty's grace is edited. Forbidden by the app's own rules and
          wrong regardless — it converts a definition into a snapshot.
        • Reduce the rate in TypeScript over query results. Puts the number outside the
          semantic layer, where no dashboard widget can bind it — which defeats the point of
          ADR-0021 — and duplicates the definition per consumer.
        • Ship an on-time rate that silently ignores grace. Wrong in the direction that
          matters: it marks late every task completed inside the grace its own duty grants. And
          wrong invisibly, which is how a number nobody trusts becomes the number everybody
          reports.

        The application therefore ships the dataset without the on-time measures rather than
        with approximated ones.

        What the gap currently costs, concretely

        In duly, grace_days is authored on duly_catalog_item, propagated to duly_duty at
        instantiation, and read by nothing. This dataset was its only intended consumer. It is
        a declared, populated, propagated field with zero enforcement — an ADR-0049
        declared-but-unenforced shape created purely by the absence of a way to consume it.

        Possible directions (not a proposal — the semantics need a ruling)

        Sketching these only to show the design space is not empty:

        1. An offset on the field reference{ $lte: { $field: 'due_date', addDays: { $field: 'duty.grace_days' } } }.
          Smallest surface; rides on [spec] SqlDriver 将 $field 编译为列对列比较(cross-field comparison push-down) #5222; keeps the layer SQL-free.
        2. A derived-measure operator over a date difference — the layer already has
          derived: { op, of } for combining measures by name; a days_between measure plus an
          ordering test would be a natural extension of that vocabulary.
        3. A declared computed dimension on the dataset — riskier, since the whole point of
          ADR-0021's author surface is that it is smaller than a query and carries no SQL.

        Whichever way it goes, the NULL semantics need stating (a duty with no grace, a task with
        no due date) — the same care #5146 took over $not.

        Reproduction

        objectstack-ai/duly at claude/issue-9-analytics-datasets, pnpm validate. Add to
        src/datasets/duty-health.dataset.ts:

        {name: 'tasks_done_on_time',aggregate: 'count',filter: {source: {$in: ['catalog','assigned']},status: 'done',completed_at: {$lte: {$field: 'due_date'}},// no way to add duty.grace_days},}

        This parses and validates clean — the schema accepts $field — and then refuses at
        runtime on any SQL driver. There is no variant of it that adds the grace offset at all.

        Environment

        @objectstack/spec 17.2.0, @objectstack/cli 17.2.0.

        Filed from the duly dogfood application (objectstack-ai/duly#9), which is shipping the
        dataset with the affected measures omitted rather than approximated.

        Activity

        Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

        Metadata

        Metadata

        Assignees

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          An ADR-0021 dataset measure cannot express a deadline that is another column plus an offset held in a third column — the grace-aware on-time rate has no spelling #14104

          Description

          @os-warren

          Summary

          A dataset measure (ui/dataset.zod.ts, ADR-0021) cannot express the single most common
          governance metric there is: "was this completed by its deadline, where the deadline is
          a stored date plus a grace period that lives on the parent record?"

          The concrete comparison, from the duly application (objectstack-ai/duly#9):

          completed_at <= due_date + duty.grace_days
          

          completed_at and due_date are columns on the base object duly_task.
          grace_days is a number column on the related duly_duty. All three are stored and
          indexed. There is no spelling for this in the measure surface, so the on-time rate, the
          "done on time" count and the "late" count all have to be dropped from the dataset.

          Recorded as a capability gap rather than a bug — nothing misbehaves, and the refusal in
          part 2 below is loud and correct. The gap is that the semantic layer cannot say the
          thing, and the workarounds available to an application author are all worse than the gap.

          What works, so the gap is precisely located

          Reaching the related field works, and this is worth stating first — it is the half a
          reader will assume is broken. include: ['duty'] plus a duty.<field> path is exactly
          what the semantic layer is for: joins are compiled from include (ADR-0071, ≤3 hops) and
          the author writes no ON clause. duly_duty_health ships a frequency dimension bound to
          duty.frequency today and it validates, builds and appears in the artifact.

          So a measure can readduty.grace_days. It cannot compare against it.

          What does not work — two independent missing pieces

          1. There is no date arithmetic anywhere in the filter grammar

          FILTER_OPERATORS (data/filter.zod.ts) is closed: equality, ordering, set, range,
          string, null/exists. Nothing adds an interval to a column.

          The {N_days_ago} vocabulary is not a substitute — DATE_MACRO_PARAM_RE
          (data/date-macros.zod.ts) is anchored to now, never to another column. {7_days_ago}
          can express "untouched for a week"; nothing can express "due_date plus seven days".

          And here the offset is not even a literal: it is itself a column (duty.grace_days),
          which is strictly harder than a fixed interval.

          2. Column-to-column comparison is declared but refused on the SQL path

          Even setting grace aside, the grace-free form completed_at <= due_date has no usable
          spelling either. FieldReferenceSchema declares it:

          {completed_at: {$lte: {$field: 'due_date'}}}

          but filter.zod.ts's own "Execution support (#5041)" block records that driver-sql (and
          driver-sqlite-wasm, which inherits its compiler) reject it with INVALID_FILTER / HTTP
          400, while the in-memory evaluator resolves it. SQL compilation is tracked as #5222.

          For a dataset that split is worse than a uniform gap. A dataset is a definition that
          outlives any one query and is bound by name from dashboards and reports. A measure using
          $field would answer on a memory-driver deployment and 400 on a SQL one — the semantic
          layer's correctness would become a property of the deployment.

          So even if #5222 landed tomorrow, part 1 would still leave due_date + duty.grace_days
          unspellable. The two are additive, not alternatives.

          Why the application-side workarounds were all rejected

          Listing these because each one is what an author reaches for, and each one is how this
          gap would have become permanent and invisible:

          • Denormalise grace_days (or a pre-computed grace_deadline) onto the task. A second
            writer that drifts the day a duty's grace is edited. Forbidden by the app's own rules and
            wrong regardless — it converts a definition into a snapshot.
          • Reduce the rate in TypeScript over query results. Puts the number outside the
            semantic layer, where no dashboard widget can bind it — which defeats the point of
            ADR-0021 — and duplicates the definition per consumer.
          • Ship an on-time rate that silently ignores grace. Wrong in the direction that
            matters: it marks late every task completed inside the grace its own duty grants. And
            wrong invisibly, which is how a number nobody trusts becomes the number everybody
            reports.

          The application therefore ships the dataset without the on-time measures rather than
          with approximated ones.

          What the gap currently costs, concretely

          In duly, grace_days is authored on duly_catalog_item, propagated to duly_duty at
          instantiation, and read by nothing. This dataset was its only intended consumer. It is
          a declared, populated, propagated field with zero enforcement — an ADR-0049
          declared-but-unenforced shape created purely by the absence of a way to consume it.

          Possible directions (not a proposal — the semantics need a ruling)

          Sketching these only to show the design space is not empty:

          1. An offset on the field reference{ $lte: { $field: 'due_date', addDays: { $field: 'duty.grace_days' } } }.
            Smallest surface; rides on [spec] SqlDriver 将 $field 编译为列对列比较(cross-field comparison push-down) #5222; keeps the layer SQL-free.
          2. A derived-measure operator over a date difference — the layer already has
            derived: { op, of } for combining measures by name; a days_between measure plus an
            ordering test would be a natural extension of that vocabulary.
          3. A declared computed dimension on the dataset — riskier, since the whole point of
            ADR-0021's author surface is that it is smaller than a query and carries no SQL.

          Whichever way it goes, the NULL semantics need stating (a duty with no grace, a task with
          no due date) — the same care #5146 took over $not.

          Reproduction

          objectstack-ai/duly at claude/issue-9-analytics-datasets, pnpm validate. Add to
          src/datasets/duty-health.dataset.ts:

          {name: 'tasks_done_on_time',aggregate: 'count',filter: {source: {$in: ['catalog','assigned']},status: 'done',completed_at: {$lte: {$field: 'due_date'}},// no way to add duty.grace_days},}

          This parses and validates clean — the schema accepts $field — and then refuses at
          runtime on any SQL driver. There is no variant of it that adds the grace offset at all.

          Environment

          @objectstack/spec 17.2.0, @objectstack/cli 17.2.0.

          Filed from the duly dogfood application (objectstack-ai/duly#9), which is shipping the
          dataset with the affected measures omitted rather than approximated.

          Activity

          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

          Metadata

          Metadata

          Assignees

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            An ADR-0021 dataset measure cannot express a deadline that is another column plus an offset held in a third column — the grace-aware on-time rate has no spelling #14104

            Description

            @os-warren

            Summary

            A dataset measure (ui/dataset.zod.ts, ADR-0021) cannot express the single most common
            governance metric there is: "was this completed by its deadline, where the deadline is
            a stored date plus a grace period that lives on the parent record?"

            The concrete comparison, from the duly application (objectstack-ai/duly#9):

            completed_at <= due_date + duty.grace_days
            

            completed_at and due_date are columns on the base object duly_task.
            grace_days is a number column on the related duly_duty. All three are stored and
            indexed. There is no spelling for this in the measure surface, so the on-time rate, the
            "done on time" count and the "late" count all have to be dropped from the dataset.

            Recorded as a capability gap rather than a bug — nothing misbehaves, and the refusal in
            part 2 below is loud and correct. The gap is that the semantic layer cannot say the
            thing, and the workarounds available to an application author are all worse than the gap.

            What works, so the gap is precisely located

            Reaching the related field works, and this is worth stating first — it is the half a
            reader will assume is broken. include: ['duty'] plus a duty.<field> path is exactly
            what the semantic layer is for: joins are compiled from include (ADR-0071, ≤3 hops) and
            the author writes no ON clause. duly_duty_health ships a frequency dimension bound to
            duty.frequency today and it validates, builds and appears in the artifact.

            So a measure can readduty.grace_days. It cannot compare against it.

            What does not work — two independent missing pieces

            1. There is no date arithmetic anywhere in the filter grammar

            FILTER_OPERATORS (data/filter.zod.ts) is closed: equality, ordering, set, range,
            string, null/exists. Nothing adds an interval to a column.

            The {N_days_ago} vocabulary is not a substitute — DATE_MACRO_PARAM_RE
            (data/date-macros.zod.ts) is anchored to now, never to another column. {7_days_ago}
            can express "untouched for a week"; nothing can express "due_date plus seven days".

            And here the offset is not even a literal: it is itself a column (duty.grace_days),
            which is strictly harder than a fixed interval.

            2. Column-to-column comparison is declared but refused on the SQL path

            Even setting grace aside, the grace-free form completed_at <= due_date has no usable
            spelling either. FieldReferenceSchema declares it:

            {completed_at: {$lte: {$field: 'due_date'}}}

            but filter.zod.ts's own "Execution support (#5041)" block records that driver-sql (and
            driver-sqlite-wasm, which inherits its compiler) reject it with INVALID_FILTER / HTTP
            400, while the in-memory evaluator resolves it. SQL compilation is tracked as #5222.

            For a dataset that split is worse than a uniform gap. A dataset is a definition that
            outlives any one query and is bound by name from dashboards and reports. A measure using
            $field would answer on a memory-driver deployment and 400 on a SQL one — the semantic
            layer's correctness would become a property of the deployment.

            So even if #5222 landed tomorrow, part 1 would still leave due_date + duty.grace_days
            unspellable. The two are additive, not alternatives.

            Why the application-side workarounds were all rejected

            Listing these because each one is what an author reaches for, and each one is how this
            gap would have become permanent and invisible:

            • Denormalise grace_days (or a pre-computed grace_deadline) onto the task. A second
              writer that drifts the day a duty's grace is edited. Forbidden by the app's own rules and
              wrong regardless — it converts a definition into a snapshot.
            • Reduce the rate in TypeScript over query results. Puts the number outside the
              semantic layer, where no dashboard widget can bind it — which defeats the point of
              ADR-0021 — and duplicates the definition per consumer.
            • Ship an on-time rate that silently ignores grace. Wrong in the direction that
              matters: it marks late every task completed inside the grace its own duty grants. And
              wrong invisibly, which is how a number nobody trusts becomes the number everybody
              reports.

            The application therefore ships the dataset without the on-time measures rather than
            with approximated ones.

            What the gap currently costs, concretely

            In duly, grace_days is authored on duly_catalog_item, propagated to duly_duty at
            instantiation, and read by nothing. This dataset was its only intended consumer. It is
            a declared, populated, propagated field with zero enforcement — an ADR-0049
            declared-but-unenforced shape created purely by the absence of a way to consume it.

            Possible directions (not a proposal — the semantics need a ruling)

            Sketching these only to show the design space is not empty:

            1. An offset on the field reference{ $lte: { $field: 'due_date', addDays: { $field: 'duty.grace_days' } } }.
              Smallest surface; rides on [spec] SqlDriver 将 $field 编译为列对列比较(cross-field comparison push-down) #5222; keeps the layer SQL-free.
            2. A derived-measure operator over a date difference — the layer already has
              derived: { op, of } for combining measures by name; a days_between measure plus an
              ordering test would be a natural extension of that vocabulary.
            3. A declared computed dimension on the dataset — riskier, since the whole point of
              ADR-0021's author surface is that it is smaller than a query and carries no SQL.

            Whichever way it goes, the NULL semantics need stating (a duty with no grace, a task with
            no due date) — the same care #5146 took over $not.

            Reproduction

            objectstack-ai/duly at claude/issue-9-analytics-datasets, pnpm validate. Add to
            src/datasets/duty-health.dataset.ts:

            {name: 'tasks_done_on_time',aggregate: 'count',filter: {source: {$in: ['catalog','assigned']},status: 'done',completed_at: {$lte: {$field: 'due_date'}},// no way to add duty.grace_days},}

            This parses and validates clean — the schema accepts $field — and then refuses at
            runtime on any SQL driver. There is no variant of it that adds the grace offset at all.

            Environment

            @objectstack/spec 17.2.0, @objectstack/cli 17.2.0.

            Filed from the duly dogfood application (objectstack-ai/duly#9), which is shipping the
            dataset with the affected measures omitted rather than approximated.

            Activity

            Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

            Metadata

            Metadata

            Assignees

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              An ADR-0021 dataset measure cannot express a deadline that is another column plus an offset held in a third column — the grace-aware on-time rate has no spelling #14104

              Description

              @os-warren

              Summary

              A dataset measure (ui/dataset.zod.ts, ADR-0021) cannot express the single most common
              governance metric there is: "was this completed by its deadline, where the deadline is
              a stored date plus a grace period that lives on the parent record?"

              The concrete comparison, from the duly application (objectstack-ai/duly#9):

              completed_at <= due_date + duty.grace_days
              

              completed_at and due_date are columns on the base object duly_task.
              grace_days is a number column on the related duly_duty. All three are stored and
              indexed. There is no spelling for this in the measure surface, so the on-time rate, the
              "done on time" count and the "late" count all have to be dropped from the dataset.

              Recorded as a capability gap rather than a bug — nothing misbehaves, and the refusal in
              part 2 below is loud and correct. The gap is that the semantic layer cannot say the
              thing, and the workarounds available to an application author are all worse than the gap.

              What works, so the gap is precisely located

              Reaching the related field works, and this is worth stating first — it is the half a
              reader will assume is broken. include: ['duty'] plus a duty.<field> path is exactly
              what the semantic layer is for: joins are compiled from include (ADR-0071, ≤3 hops) and
              the author writes no ON clause. duly_duty_health ships a frequency dimension bound to
              duty.frequency today and it validates, builds and appears in the artifact.

              So a measure can readduty.grace_days. It cannot compare against it.

              What does not work — two independent missing pieces

              1. There is no date arithmetic anywhere in the filter grammar

              FILTER_OPERATORS (data/filter.zod.ts) is closed: equality, ordering, set, range,
              string, null/exists. Nothing adds an interval to a column.

              The {N_days_ago} vocabulary is not a substitute — DATE_MACRO_PARAM_RE
              (data/date-macros.zod.ts) is anchored to now, never to another column. {7_days_ago}
              can express "untouched for a week"; nothing can express "due_date plus seven days".

              And here the offset is not even a literal: it is itself a column (duty.grace_days),
              which is strictly harder than a fixed interval.

              2. Column-to-column comparison is declared but refused on the SQL path

              Even setting grace aside, the grace-free form completed_at <= due_date has no usable
              spelling either. FieldReferenceSchema declares it:

              {completed_at: {$lte: {$field: 'due_date'}}}

              but filter.zod.ts's own "Execution support (#5041)" block records that driver-sql (and
              driver-sqlite-wasm, which inherits its compiler) reject it with INVALID_FILTER / HTTP
              400, while the in-memory evaluator resolves it. SQL compilation is tracked as #5222.

              For a dataset that split is worse than a uniform gap. A dataset is a definition that
              outlives any one query and is bound by name from dashboards and reports. A measure using
              $field would answer on a memory-driver deployment and 400 on a SQL one — the semantic
              layer's correctness would become a property of the deployment.

              So even if #5222 landed tomorrow, part 1 would still leave due_date + duty.grace_days
              unspellable. The two are additive, not alternatives.

              Why the application-side workarounds were all rejected

              Listing these because each one is what an author reaches for, and each one is how this
              gap would have become permanent and invisible:

              • Denormalise grace_days (or a pre-computed grace_deadline) onto the task. A second
                writer that drifts the day a duty's grace is edited. Forbidden by the app's own rules and
                wrong regardless — it converts a definition into a snapshot.
              • Reduce the rate in TypeScript over query results. Puts the number outside the
                semantic layer, where no dashboard widget can bind it — which defeats the point of
                ADR-0021 — and duplicates the definition per consumer.
              • Ship an on-time rate that silently ignores grace. Wrong in the direction that
                matters: it marks late every task completed inside the grace its own duty grants. And
                wrong invisibly, which is how a number nobody trusts becomes the number everybody
                reports.

              The application therefore ships the dataset without the on-time measures rather than
              with approximated ones.

              What the gap currently costs, concretely

              In duly, grace_days is authored on duly_catalog_item, propagated to duly_duty at
              instantiation, and read by nothing. This dataset was its only intended consumer. It is
              a declared, populated, propagated field with zero enforcement — an ADR-0049
              declared-but-unenforced shape created purely by the absence of a way to consume it.

              Possible directions (not a proposal — the semantics need a ruling)

              Sketching these only to show the design space is not empty:

              1. An offset on the field reference{ $lte: { $field: 'due_date', addDays: { $field: 'duty.grace_days' } } }.
                Smallest surface; rides on [spec] SqlDriver 将 $field 编译为列对列比较(cross-field comparison push-down) #5222; keeps the layer SQL-free.
              2. A derived-measure operator over a date difference — the layer already has
                derived: { op, of } for combining measures by name; a days_between measure plus an
                ordering test would be a natural extension of that vocabulary.
              3. A declared computed dimension on the dataset — riskier, since the whole point of
                ADR-0021's author surface is that it is smaller than a query and carries no SQL.

              Whichever way it goes, the NULL semantics need stating (a duty with no grace, a task with
              no due date) — the same care #5146 took over $not.

              Reproduction

              objectstack-ai/duly at claude/issue-9-analytics-datasets, pnpm validate. Add to
              src/datasets/duty-health.dataset.ts:

              {name: 'tasks_done_on_time',aggregate: 'count',filter: {source: {$in: ['catalog','assigned']},status: 'done',completed_at: {$lte: {$field: 'due_date'}},// no way to add duty.grace_days},}

              This parses and validates clean — the schema accepts $field — and then refuses at
              runtime on any SQL driver. There is no variant of it that adds the grace offset at all.

              Environment

              @objectstack/spec 17.2.0, @objectstack/cli 17.2.0.

              Filed from the duly dogfood application (objectstack-ai/duly#9), which is shipping the
              dataset with the affected measures omitted rather than approximated.

              Activity

              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

              Metadata

              Metadata

              Assignees

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                An ADR-0021 dataset measure cannot express a deadline that is another column plus an offset held in a third column — the grace-aware on-time rate has no spelling #14104

                Description

                @os-warren

                Summary

                A dataset measure (ui/dataset.zod.ts, ADR-0021) cannot express the single most common
                governance metric there is: "was this completed by its deadline, where the deadline is
                a stored date plus a grace period that lives on the parent record?"

                The concrete comparison, from the duly application (objectstack-ai/duly#9):

                completed_at <= due_date + duty.grace_days
                

                completed_at and due_date are columns on the base object duly_task.
                grace_days is a number column on the related duly_duty. All three are stored and
                indexed. There is no spelling for this in the measure surface, so the on-time rate, the
                "done on time" count and the "late" count all have to be dropped from the dataset.

                Recorded as a capability gap rather than a bug — nothing misbehaves, and the refusal in
                part 2 below is loud and correct. The gap is that the semantic layer cannot say the
                thing, and the workarounds available to an application author are all worse than the gap.

                What works, so the gap is precisely located

                Reaching the related field works, and this is worth stating first — it is the half a
                reader will assume is broken. include: ['duty'] plus a duty.<field> path is exactly
                what the semantic layer is for: joins are compiled from include (ADR-0071, ≤3 hops) and
                the author writes no ON clause. duly_duty_health ships a frequency dimension bound to
                duty.frequency today and it validates, builds and appears in the artifact.

                So a measure can readduty.grace_days. It cannot compare against it.

                What does not work — two independent missing pieces

                1. There is no date arithmetic anywhere in the filter grammar

                FILTER_OPERATORS (data/filter.zod.ts) is closed: equality, ordering, set, range,
                string, null/exists. Nothing adds an interval to a column.

                The {N_days_ago} vocabulary is not a substitute — DATE_MACRO_PARAM_RE
                (data/date-macros.zod.ts) is anchored to now, never to another column. {7_days_ago}
                can express "untouched for a week"; nothing can express "due_date plus seven days".

                And here the offset is not even a literal: it is itself a column (duty.grace_days),
                which is strictly harder than a fixed interval.

                2. Column-to-column comparison is declared but refused on the SQL path

                Even setting grace aside, the grace-free form completed_at <= due_date has no usable
                spelling either. FieldReferenceSchema declares it:

                {completed_at: {$lte: {$field: 'due_date'}}}

                but filter.zod.ts's own "Execution support (#5041)" block records that driver-sql (and
                driver-sqlite-wasm, which inherits its compiler) reject it with INVALID_FILTER / HTTP
                400, while the in-memory evaluator resolves it. SQL compilation is tracked as #5222.

                For a dataset that split is worse than a uniform gap. A dataset is a definition that
                outlives any one query and is bound by name from dashboards and reports. A measure using
                $field would answer on a memory-driver deployment and 400 on a SQL one — the semantic
                layer's correctness would become a property of the deployment.

                So even if #5222 landed tomorrow, part 1 would still leave due_date + duty.grace_days
                unspellable. The two are additive, not alternatives.

                Why the application-side workarounds were all rejected

                Listing these because each one is what an author reaches for, and each one is how this
                gap would have become permanent and invisible:

                • Denormalise grace_days (or a pre-computed grace_deadline) onto the task. A second
                  writer that drifts the day a duty's grace is edited. Forbidden by the app's own rules and
                  wrong regardless — it converts a definition into a snapshot.
                • Reduce the rate in TypeScript over query results. Puts the number outside the
                  semantic layer, where no dashboard widget can bind it — which defeats the point of
                  ADR-0021 — and duplicates the definition per consumer.
                • Ship an on-time rate that silently ignores grace. Wrong in the direction that
                  matters: it marks late every task completed inside the grace its own duty grants. And
                  wrong invisibly, which is how a number nobody trusts becomes the number everybody
                  reports.

                The application therefore ships the dataset without the on-time measures rather than
                with approximated ones.

                What the gap currently costs, concretely

                In duly, grace_days is authored on duly_catalog_item, propagated to duly_duty at
                instantiation, and read by nothing. This dataset was its only intended consumer. It is
                a declared, populated, propagated field with zero enforcement — an ADR-0049
                declared-but-unenforced shape created purely by the absence of a way to consume it.

                Possible directions (not a proposal — the semantics need a ruling)

                Sketching these only to show the design space is not empty:

                1. An offset on the field reference{ $lte: { $field: 'due_date', addDays: { $field: 'duty.grace_days' } } }.
                  Smallest surface; rides on [spec] SqlDriver 将 $field 编译为列对列比较(cross-field comparison push-down) #5222; keeps the layer SQL-free.
                2. A derived-measure operator over a date difference — the layer already has
                  derived: { op, of } for combining measures by name; a days_between measure plus an
                  ordering test would be a natural extension of that vocabulary.
                3. A declared computed dimension on the dataset — riskier, since the whole point of
                  ADR-0021's author surface is that it is smaller than a query and carries no SQL.

                Whichever way it goes, the NULL semantics need stating (a duty with no grace, a task with
                no due date) — the same care #5146 took over $not.

                Reproduction

                objectstack-ai/duly at claude/issue-9-analytics-datasets, pnpm validate. Add to
                src/datasets/duty-health.dataset.ts:

                {name: 'tasks_done_on_time',aggregate: 'count',filter: {source: {$in: ['catalog','assigned']},status: 'done',completed_at: {$lte: {$field: 'due_date'}},// no way to add duty.grace_days},}

                This parses and validates clean — the schema accepts $field — and then refuses at
                runtime on any SQL driver. There is no variant of it that adds the grace offset at all.

                Environment

                @objectstack/spec 17.2.0, @objectstack/cli 17.2.0.

                Filed from the duly dogfood application (objectstack-ai/duly#9), which is shipping the
                dataset with the affected measures omitted rather than approximated.

                Activity

                Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                Metadata

                Metadata

                Assignees

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions