objectstack validate and build accept a dataset whose base object, include path and dimension/measure field paths all name nothing — the same walker already resolves date-macro tokens on the identical node #14105

Description

@os-warren

Summary

An ADR-0021 dataset can name a base object that does not exist, join a
relationship that does not exist, and bind every dimension and measure to
fields that do not exist, and objectstack validate exits 0 with
✓ Validation passed. objectstack build also exits 0 and writes the dangling dataset
into dist/objectstack.json.

The sting is that the author-time rule pass already walks these exact nodes: a bad
date-macro token in a measure filter is caught, path-precise, on the same traversal.
So the machinery and the traversal are both present — only the reference resolution is
missing.

This is one level below #7529 (closed by #8902, which refuses a widget → dataset
binding that names nothing). This is the dataset → object/field binding underneath it.
A board can now be proven to point at a real dataset, and that dataset can still point at
nothing.

Measured

@objectstack/spec 17.2.0, @objectstack/cli 17.2.0, on objectstack-ai/duly
(3 datasets over duly_task). Each row is one mutation applied on its own, confirmed on
disk before running (old spelling absent, new spelling present), then reverted.

#Mutationpnpm validate
1dimension field: 'period_key''period_kee' (base field)exit 0, passed
2dimension field: 'duty.frequency''duty.frequenci' (joined field)exit 0, passed
3measure field: 'last_update_at''last_update_att'exit 0, passed
4measure filter key last_update_atlast_update_atttexit 0, passed
5include: ['duty']['dutee'] (relationship does not exist)exit 0, passed
6object: 'duly_task''duly_tsk' (base object does not exist)exit 0, passed
7duplicate measure name (schema superRefine)exit 1 — caught
8filter value '{7_days_ago}''{7_fortnights_ago}'exit 1 — caught

Rows 7 and 8 are the control: datasets genuinely are in the validation path, so rows 1–6
are not "the file was never read".

Row 8's message shows the walker reaching all the way into a measure filter with an exact
path:

✗ Author-time rules failed (1 issue)
• dataset "duly_stagnation": Filter value "{7_fortnights_ago}" is not a resolvable placeholder.
It is sent to the data engine as a literal string, matches no record, and the surface renders empty.
rule: filter-token-unknown at datasets[1].measures[1].filter.last_update_at.$lt

That message is the whole argument for this issue. filter-token-unknown already stands
at datasets[1].measures[1].filter.last_update_at.$lt and reasons about the value.
Nothing standing in that same position resolves the key — or the sibling
measures[].field, dimensions[].field, include[], or object.

Why this matters more for a dataset than for most metadata

filter-token-unknown's own wording states the failure mode it exists to prevent:
"sent to the data engine as a literal string, matches no record, and the surface renders
empty."
A dangling field path produces the same outcome from the same node and is not
checked.

A dataset is the semantic layer: dashboards and reports bind its dimensions and measures
by name (ADR-0021), and the consumer end is now guarded (#8902). So the surviving failure
is the quiet one — every binding resolves, the board renders, and the charts are empty or
subtly wrong because the dataset underneath addresses columns that do not exist.

Row 6 is the one I would prioritise: a dataset over a non-existent base object is
unambiguously meaningless, has no forward-reference story worth preserving, and is the
cheapest possible check.

Precedent in the tree

The platform already resolves qualified field reads elsewhere and produces a good message.
From the same repo's flow-condition validation:

unknown field `needs_colection` on `duly_assignment` — did you mean `needs_collection`?

That is the message shape this wants. The resolver exists; it is not wired to dataset
field paths.

Related: #14089 (bare identifiers in flow conditions have no validation leg) is the same
family — a validator with a per-surface hole — though a different surface.

Suggested scope

  1. Resolve Dataset.object against the object registry. (Cheapest, highest value.)
  2. Resolve each include[] path hop-by-hop against the object graph, honouring the
    ADR-0071 3-hop limit that is already enforced structurally.
  3. Resolve dimensions[].field and measures[].field — a base field, or a
    relationship[.relationship].field path whose relationship prefix must appear in
    include. That last clause is a second real check: a dot-path whose prefix was never
    declared in include is not joinable even if the field exists.
  4. Resolve filter keys in Dataset.filter and measures[].filter, reusing the
    traversal filter-token-unknown already performs on the values.

Timing: validate/build at the latest, matching #8902's "refuse at publish at the
latest" posture for the layer above.

Reproduction

  1. git clone objectstack-ai/duly, pnpm install.
  2. In src/datasets/duty-health.dataset.ts, change object: 'duly_task' to
    object: 'duly_tsk'.
  3. pnpm validate✓ Validation passed, exit 0.
  4. pnpm build → exit 0; dist/objectstack.json contains the dataset with the dangling
    base object.

Filed from the duly dogfood application (objectstack-ai/duly#9).

Metadata

Metadata

Assignees

Type

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

    objectstack validate and build accept a dataset whose base object, include path and dimension/measure field paths all name nothing — the same walker already resolves date-macro tokens on the identical node #14105

    Description

    @os-warren

    Summary

    An ADR-0021 dataset can name a base object that does not exist, join a
    relationship that does not exist, and bind every dimension and measure to
    fields that do not exist, and objectstack validate exits 0 with
    ✓ Validation passed. objectstack build also exits 0 and writes the dangling dataset
    into dist/objectstack.json.

    The sting is that the author-time rule pass already walks these exact nodes: a bad
    date-macro token in a measure filter is caught, path-precise, on the same traversal.
    So the machinery and the traversal are both present — only the reference resolution is
    missing.

    This is one level below #7529 (closed by #8902, which refuses a widget → dataset
    binding that names nothing). This is the dataset → object/field binding underneath it.
    A board can now be proven to point at a real dataset, and that dataset can still point at
    nothing.

    Measured

    @objectstack/spec 17.2.0, @objectstack/cli 17.2.0, on objectstack-ai/duly
    (3 datasets over duly_task). Each row is one mutation applied on its own, confirmed on
    disk before running (old spelling absent, new spelling present), then reverted.

    #Mutationpnpm validate
    1dimension field: 'period_key''period_kee' (base field)exit 0, passed
    2dimension field: 'duty.frequency''duty.frequenci' (joined field)exit 0, passed
    3measure field: 'last_update_at''last_update_att'exit 0, passed
    4measure filter key last_update_atlast_update_atttexit 0, passed
    5include: ['duty']['dutee'] (relationship does not exist)exit 0, passed
    6object: 'duly_task''duly_tsk' (base object does not exist)exit 0, passed
    7duplicate measure name (schema superRefine)exit 1 — caught
    8filter value '{7_days_ago}''{7_fortnights_ago}'exit 1 — caught

    Rows 7 and 8 are the control: datasets genuinely are in the validation path, so rows 1–6
    are not "the file was never read".

    Row 8's message shows the walker reaching all the way into a measure filter with an exact
    path:

    ✗ Author-time rules failed (1 issue)
    • dataset "duly_stagnation": Filter value "{7_fortnights_ago}" is not a resolvable placeholder.
    It is sent to the data engine as a literal string, matches no record, and the surface renders empty.
    rule: filter-token-unknown at datasets[1].measures[1].filter.last_update_at.$lt
    

    That message is the whole argument for this issue. filter-token-unknown already stands
    at datasets[1].measures[1].filter.last_update_at.$lt and reasons about the value.
    Nothing standing in that same position resolves the key — or the sibling
    measures[].field, dimensions[].field, include[], or object.

    Why this matters more for a dataset than for most metadata

    filter-token-unknown's own wording states the failure mode it exists to prevent:
    "sent to the data engine as a literal string, matches no record, and the surface renders
    empty."
    A dangling field path produces the same outcome from the same node and is not
    checked.

    A dataset is the semantic layer: dashboards and reports bind its dimensions and measures
    by name (ADR-0021), and the consumer end is now guarded (#8902). So the surviving failure
    is the quiet one — every binding resolves, the board renders, and the charts are empty or
    subtly wrong because the dataset underneath addresses columns that do not exist.

    Row 6 is the one I would prioritise: a dataset over a non-existent base object is
    unambiguously meaningless, has no forward-reference story worth preserving, and is the
    cheapest possible check.

    Precedent in the tree

    The platform already resolves qualified field reads elsewhere and produces a good message.
    From the same repo's flow-condition validation:

    unknown field `needs_colection` on `duly_assignment` — did you mean `needs_collection`?
    

    That is the message shape this wants. The resolver exists; it is not wired to dataset
    field paths.

    Related: #14089 (bare identifiers in flow conditions have no validation leg) is the same
    family — a validator with a per-surface hole — though a different surface.

    Suggested scope

    1. Resolve Dataset.object against the object registry. (Cheapest, highest value.)
    2. Resolve each include[] path hop-by-hop against the object graph, honouring the
      ADR-0071 3-hop limit that is already enforced structurally.
    3. Resolve dimensions[].field and measures[].field — a base field, or a
      relationship[.relationship].field path whose relationship prefix must appear in
      include. That last clause is a second real check: a dot-path whose prefix was never
      declared in include is not joinable even if the field exists.
    4. Resolve filter keys in Dataset.filter and measures[].filter, reusing the
      traversal filter-token-unknown already performs on the values.

    Timing: validate/build at the latest, matching #8902's "refuse at publish at the
    latest" posture for the layer above.

    Reproduction

    1. git clone objectstack-ai/duly, pnpm install.
    2. In src/datasets/duty-health.dataset.ts, change object: 'duly_task' to
      object: 'duly_tsk'.
    3. pnpm validate✓ Validation passed, exit 0.
    4. pnpm build → exit 0; dist/objectstack.json contains the dataset with the dangling
      base object.

    Filed from the duly dogfood application (objectstack-ai/duly#9).

    Metadata

    Metadata

    Assignees

    Type

    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

      objectstack validate and build accept a dataset whose base object, include path and dimension/measure field paths all name nothing — the same walker already resolves date-macro tokens on the identical node #14105

      Description

      @os-warren

      Summary

      An ADR-0021 dataset can name a base object that does not exist, join a
      relationship that does not exist, and bind every dimension and measure to
      fields that do not exist, and objectstack validate exits 0 with
      ✓ Validation passed. objectstack build also exits 0 and writes the dangling dataset
      into dist/objectstack.json.

      The sting is that the author-time rule pass already walks these exact nodes: a bad
      date-macro token in a measure filter is caught, path-precise, on the same traversal.
      So the machinery and the traversal are both present — only the reference resolution is
      missing.

      This is one level below #7529 (closed by #8902, which refuses a widget → dataset
      binding that names nothing). This is the dataset → object/field binding underneath it.
      A board can now be proven to point at a real dataset, and that dataset can still point at
      nothing.

      Measured

      @objectstack/spec 17.2.0, @objectstack/cli 17.2.0, on objectstack-ai/duly
      (3 datasets over duly_task). Each row is one mutation applied on its own, confirmed on
      disk before running (old spelling absent, new spelling present), then reverted.

      #Mutationpnpm validate
      1dimension field: 'period_key''period_kee' (base field)exit 0, passed
      2dimension field: 'duty.frequency''duty.frequenci' (joined field)exit 0, passed
      3measure field: 'last_update_at''last_update_att'exit 0, passed
      4measure filter key last_update_atlast_update_atttexit 0, passed
      5include: ['duty']['dutee'] (relationship does not exist)exit 0, passed
      6object: 'duly_task''duly_tsk' (base object does not exist)exit 0, passed
      7duplicate measure name (schema superRefine)exit 1 — caught
      8filter value '{7_days_ago}''{7_fortnights_ago}'exit 1 — caught

      Rows 7 and 8 are the control: datasets genuinely are in the validation path, so rows 1–6
      are not "the file was never read".

      Row 8's message shows the walker reaching all the way into a measure filter with an exact
      path:

      ✗ Author-time rules failed (1 issue)
      • dataset "duly_stagnation": Filter value "{7_fortnights_ago}" is not a resolvable placeholder.
      It is sent to the data engine as a literal string, matches no record, and the surface renders empty.
      rule: filter-token-unknown at datasets[1].measures[1].filter.last_update_at.$lt
      

      That message is the whole argument for this issue. filter-token-unknown already stands
      at datasets[1].measures[1].filter.last_update_at.$lt and reasons about the value.
      Nothing standing in that same position resolves the key — or the sibling
      measures[].field, dimensions[].field, include[], or object.

      Why this matters more for a dataset than for most metadata

      filter-token-unknown's own wording states the failure mode it exists to prevent:
      "sent to the data engine as a literal string, matches no record, and the surface renders
      empty."
      A dangling field path produces the same outcome from the same node and is not
      checked.

      A dataset is the semantic layer: dashboards and reports bind its dimensions and measures
      by name (ADR-0021), and the consumer end is now guarded (#8902). So the surviving failure
      is the quiet one — every binding resolves, the board renders, and the charts are empty or
      subtly wrong because the dataset underneath addresses columns that do not exist.

      Row 6 is the one I would prioritise: a dataset over a non-existent base object is
      unambiguously meaningless, has no forward-reference story worth preserving, and is the
      cheapest possible check.

      Precedent in the tree

      The platform already resolves qualified field reads elsewhere and produces a good message.
      From the same repo's flow-condition validation:

      unknown field `needs_colection` on `duly_assignment` — did you mean `needs_collection`?
      

      That is the message shape this wants. The resolver exists; it is not wired to dataset
      field paths.

      Related: #14089 (bare identifiers in flow conditions have no validation leg) is the same
      family — a validator with a per-surface hole — though a different surface.

      Suggested scope

      1. Resolve Dataset.object against the object registry. (Cheapest, highest value.)
      2. Resolve each include[] path hop-by-hop against the object graph, honouring the
        ADR-0071 3-hop limit that is already enforced structurally.
      3. Resolve dimensions[].field and measures[].field — a base field, or a
        relationship[.relationship].field path whose relationship prefix must appear in
        include. That last clause is a second real check: a dot-path whose prefix was never
        declared in include is not joinable even if the field exists.
      4. Resolve filter keys in Dataset.filter and measures[].filter, reusing the
        traversal filter-token-unknown already performs on the values.

      Timing: validate/build at the latest, matching #8902's "refuse at publish at the
      latest" posture for the layer above.

      Reproduction

      1. git clone objectstack-ai/duly, pnpm install.
      2. In src/datasets/duty-health.dataset.ts, change object: 'duly_task' to
        object: 'duly_tsk'.
      3. pnpm validate✓ Validation passed, exit 0.
      4. pnpm build → exit 0; dist/objectstack.json contains the dataset with the dangling
        base object.

      Filed from the duly dogfood application (objectstack-ai/duly#9).

      Metadata

      Metadata

      Assignees

      Type

      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

        objectstack validate and build accept a dataset whose base object, include path and dimension/measure field paths all name nothing — the same walker already resolves date-macro tokens on the identical node #14105

        Description

        @os-warren

        Summary

        An ADR-0021 dataset can name a base object that does not exist, join a
        relationship that does not exist, and bind every dimension and measure to
        fields that do not exist, and objectstack validate exits 0 with
        ✓ Validation passed. objectstack build also exits 0 and writes the dangling dataset
        into dist/objectstack.json.

        The sting is that the author-time rule pass already walks these exact nodes: a bad
        date-macro token in a measure filter is caught, path-precise, on the same traversal.
        So the machinery and the traversal are both present — only the reference resolution is
        missing.

        This is one level below #7529 (closed by #8902, which refuses a widget → dataset
        binding that names nothing). This is the dataset → object/field binding underneath it.
        A board can now be proven to point at a real dataset, and that dataset can still point at
        nothing.

        Measured

        @objectstack/spec 17.2.0, @objectstack/cli 17.2.0, on objectstack-ai/duly
        (3 datasets over duly_task). Each row is one mutation applied on its own, confirmed on
        disk before running (old spelling absent, new spelling present), then reverted.

        #Mutationpnpm validate
        1dimension field: 'period_key''period_kee' (base field)exit 0, passed
        2dimension field: 'duty.frequency''duty.frequenci' (joined field)exit 0, passed
        3measure field: 'last_update_at''last_update_att'exit 0, passed
        4measure filter key last_update_atlast_update_atttexit 0, passed
        5include: ['duty']['dutee'] (relationship does not exist)exit 0, passed
        6object: 'duly_task''duly_tsk' (base object does not exist)exit 0, passed
        7duplicate measure name (schema superRefine)exit 1 — caught
        8filter value '{7_days_ago}''{7_fortnights_ago}'exit 1 — caught

        Rows 7 and 8 are the control: datasets genuinely are in the validation path, so rows 1–6
        are not "the file was never read".

        Row 8's message shows the walker reaching all the way into a measure filter with an exact
        path:

        ✗ Author-time rules failed (1 issue)
        • dataset "duly_stagnation": Filter value "{7_fortnights_ago}" is not a resolvable placeholder.
        It is sent to the data engine as a literal string, matches no record, and the surface renders empty.
        rule: filter-token-unknown at datasets[1].measures[1].filter.last_update_at.$lt
        

        That message is the whole argument for this issue. filter-token-unknown already stands
        at datasets[1].measures[1].filter.last_update_at.$lt and reasons about the value.
        Nothing standing in that same position resolves the key — or the sibling
        measures[].field, dimensions[].field, include[], or object.

        Why this matters more for a dataset than for most metadata

        filter-token-unknown's own wording states the failure mode it exists to prevent:
        "sent to the data engine as a literal string, matches no record, and the surface renders
        empty."
        A dangling field path produces the same outcome from the same node and is not
        checked.

        A dataset is the semantic layer: dashboards and reports bind its dimensions and measures
        by name (ADR-0021), and the consumer end is now guarded (#8902). So the surviving failure
        is the quiet one — every binding resolves, the board renders, and the charts are empty or
        subtly wrong because the dataset underneath addresses columns that do not exist.

        Row 6 is the one I would prioritise: a dataset over a non-existent base object is
        unambiguously meaningless, has no forward-reference story worth preserving, and is the
        cheapest possible check.

        Precedent in the tree

        The platform already resolves qualified field reads elsewhere and produces a good message.
        From the same repo's flow-condition validation:

        unknown field `needs_colection` on `duly_assignment` — did you mean `needs_collection`?
        

        That is the message shape this wants. The resolver exists; it is not wired to dataset
        field paths.

        Related: #14089 (bare identifiers in flow conditions have no validation leg) is the same
        family — a validator with a per-surface hole — though a different surface.

        Suggested scope

        1. Resolve Dataset.object against the object registry. (Cheapest, highest value.)
        2. Resolve each include[] path hop-by-hop against the object graph, honouring the
          ADR-0071 3-hop limit that is already enforced structurally.
        3. Resolve dimensions[].field and measures[].field — a base field, or a
          relationship[.relationship].field path whose relationship prefix must appear in
          include. That last clause is a second real check: a dot-path whose prefix was never
          declared in include is not joinable even if the field exists.
        4. Resolve filter keys in Dataset.filter and measures[].filter, reusing the
          traversal filter-token-unknown already performs on the values.

        Timing: validate/build at the latest, matching #8902's "refuse at publish at the
        latest" posture for the layer above.

        Reproduction

        1. git clone objectstack-ai/duly, pnpm install.
        2. In src/datasets/duty-health.dataset.ts, change object: 'duly_task' to
          object: 'duly_tsk'.
        3. pnpm validate✓ Validation passed, exit 0.
        4. pnpm build → exit 0; dist/objectstack.json contains the dataset with the dangling
          base object.

        Filed from the duly dogfood application (objectstack-ai/duly#9).

        Metadata

        Metadata

        Assignees

        Type

        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

          objectstack validate and build accept a dataset whose base object, include path and dimension/measure field paths all name nothing — the same walker already resolves date-macro tokens on the identical node #14105

          Description

          @os-warren

          Summary

          An ADR-0021 dataset can name a base object that does not exist, join a
          relationship that does not exist, and bind every dimension and measure to
          fields that do not exist, and objectstack validate exits 0 with
          ✓ Validation passed. objectstack build also exits 0 and writes the dangling dataset
          into dist/objectstack.json.

          The sting is that the author-time rule pass already walks these exact nodes: a bad
          date-macro token in a measure filter is caught, path-precise, on the same traversal.
          So the machinery and the traversal are both present — only the reference resolution is
          missing.

          This is one level below #7529 (closed by #8902, which refuses a widget → dataset
          binding that names nothing). This is the dataset → object/field binding underneath it.
          A board can now be proven to point at a real dataset, and that dataset can still point at
          nothing.

          Measured

          @objectstack/spec 17.2.0, @objectstack/cli 17.2.0, on objectstack-ai/duly
          (3 datasets over duly_task). Each row is one mutation applied on its own, confirmed on
          disk before running (old spelling absent, new spelling present), then reverted.

          #Mutationpnpm validate
          1dimension field: 'period_key''period_kee' (base field)exit 0, passed
          2dimension field: 'duty.frequency''duty.frequenci' (joined field)exit 0, passed
          3measure field: 'last_update_at''last_update_att'exit 0, passed
          4measure filter key last_update_atlast_update_atttexit 0, passed
          5include: ['duty']['dutee'] (relationship does not exist)exit 0, passed
          6object: 'duly_task''duly_tsk' (base object does not exist)exit 0, passed
          7duplicate measure name (schema superRefine)exit 1 — caught
          8filter value '{7_days_ago}''{7_fortnights_ago}'exit 1 — caught

          Rows 7 and 8 are the control: datasets genuinely are in the validation path, so rows 1–6
          are not "the file was never read".

          Row 8's message shows the walker reaching all the way into a measure filter with an exact
          path:

          ✗ Author-time rules failed (1 issue)
          • dataset "duly_stagnation": Filter value "{7_fortnights_ago}" is not a resolvable placeholder.
          It is sent to the data engine as a literal string, matches no record, and the surface renders empty.
          rule: filter-token-unknown at datasets[1].measures[1].filter.last_update_at.$lt
          

          That message is the whole argument for this issue. filter-token-unknown already stands
          at datasets[1].measures[1].filter.last_update_at.$lt and reasons about the value.
          Nothing standing in that same position resolves the key — or the sibling
          measures[].field, dimensions[].field, include[], or object.

          Why this matters more for a dataset than for most metadata

          filter-token-unknown's own wording states the failure mode it exists to prevent:
          "sent to the data engine as a literal string, matches no record, and the surface renders
          empty."
          A dangling field path produces the same outcome from the same node and is not
          checked.

          A dataset is the semantic layer: dashboards and reports bind its dimensions and measures
          by name (ADR-0021), and the consumer end is now guarded (#8902). So the surviving failure
          is the quiet one — every binding resolves, the board renders, and the charts are empty or
          subtly wrong because the dataset underneath addresses columns that do not exist.

          Row 6 is the one I would prioritise: a dataset over a non-existent base object is
          unambiguously meaningless, has no forward-reference story worth preserving, and is the
          cheapest possible check.

          Precedent in the tree

          The platform already resolves qualified field reads elsewhere and produces a good message.
          From the same repo's flow-condition validation:

          unknown field `needs_colection` on `duly_assignment` — did you mean `needs_collection`?
          

          That is the message shape this wants. The resolver exists; it is not wired to dataset
          field paths.

          Related: #14089 (bare identifiers in flow conditions have no validation leg) is the same
          family — a validator with a per-surface hole — though a different surface.

          Suggested scope

          1. Resolve Dataset.object against the object registry. (Cheapest, highest value.)
          2. Resolve each include[] path hop-by-hop against the object graph, honouring the
            ADR-0071 3-hop limit that is already enforced structurally.
          3. Resolve dimensions[].field and measures[].field — a base field, or a
            relationship[.relationship].field path whose relationship prefix must appear in
            include. That last clause is a second real check: a dot-path whose prefix was never
            declared in include is not joinable even if the field exists.
          4. Resolve filter keys in Dataset.filter and measures[].filter, reusing the
            traversal filter-token-unknown already performs on the values.

          Timing: validate/build at the latest, matching #8902's "refuse at publish at the
          latest" posture for the layer above.

          Reproduction

          1. git clone objectstack-ai/duly, pnpm install.
          2. In src/datasets/duty-health.dataset.ts, change object: 'duly_task' to
            object: 'duly_tsk'.
          3. pnpm validate✓ Validation passed, exit 0.
          4. pnpm build → exit 0; dist/objectstack.json contains the dataset with the dangling
            base object.

          Filed from the duly dogfood application (objectstack-ai/duly#9).

          Metadata

          Metadata

          Assignees

          Type

          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

            objectstack validate and build accept a dataset whose base object, include path and dimension/measure field paths all name nothing — the same walker already resolves date-macro tokens on the identical node #14105

            Description

            @os-warren

            Summary

            An ADR-0021 dataset can name a base object that does not exist, join a
            relationship that does not exist, and bind every dimension and measure to
            fields that do not exist, and objectstack validate exits 0 with
            ✓ Validation passed. objectstack build also exits 0 and writes the dangling dataset
            into dist/objectstack.json.

            The sting is that the author-time rule pass already walks these exact nodes: a bad
            date-macro token in a measure filter is caught, path-precise, on the same traversal.
            So the machinery and the traversal are both present — only the reference resolution is
            missing.

            This is one level below #7529 (closed by #8902, which refuses a widget → dataset
            binding that names nothing). This is the dataset → object/field binding underneath it.
            A board can now be proven to point at a real dataset, and that dataset can still point at
            nothing.

            Measured

            @objectstack/spec 17.2.0, @objectstack/cli 17.2.0, on objectstack-ai/duly
            (3 datasets over duly_task). Each row is one mutation applied on its own, confirmed on
            disk before running (old spelling absent, new spelling present), then reverted.

            #Mutationpnpm validate
            1dimension field: 'period_key''period_kee' (base field)exit 0, passed
            2dimension field: 'duty.frequency''duty.frequenci' (joined field)exit 0, passed
            3measure field: 'last_update_at''last_update_att'exit 0, passed
            4measure filter key last_update_atlast_update_atttexit 0, passed
            5include: ['duty']['dutee'] (relationship does not exist)exit 0, passed
            6object: 'duly_task''duly_tsk' (base object does not exist)exit 0, passed
            7duplicate measure name (schema superRefine)exit 1 — caught
            8filter value '{7_days_ago}''{7_fortnights_ago}'exit 1 — caught

            Rows 7 and 8 are the control: datasets genuinely are in the validation path, so rows 1–6
            are not "the file was never read".

            Row 8's message shows the walker reaching all the way into a measure filter with an exact
            path:

            ✗ Author-time rules failed (1 issue)
            • dataset "duly_stagnation": Filter value "{7_fortnights_ago}" is not a resolvable placeholder.
            It is sent to the data engine as a literal string, matches no record, and the surface renders empty.
            rule: filter-token-unknown at datasets[1].measures[1].filter.last_update_at.$lt
            

            That message is the whole argument for this issue. filter-token-unknown already stands
            at datasets[1].measures[1].filter.last_update_at.$lt and reasons about the value.
            Nothing standing in that same position resolves the key — or the sibling
            measures[].field, dimensions[].field, include[], or object.

            Why this matters more for a dataset than for most metadata

            filter-token-unknown's own wording states the failure mode it exists to prevent:
            "sent to the data engine as a literal string, matches no record, and the surface renders
            empty."
            A dangling field path produces the same outcome from the same node and is not
            checked.

            A dataset is the semantic layer: dashboards and reports bind its dimensions and measures
            by name (ADR-0021), and the consumer end is now guarded (#8902). So the surviving failure
            is the quiet one — every binding resolves, the board renders, and the charts are empty or
            subtly wrong because the dataset underneath addresses columns that do not exist.

            Row 6 is the one I would prioritise: a dataset over a non-existent base object is
            unambiguously meaningless, has no forward-reference story worth preserving, and is the
            cheapest possible check.

            Precedent in the tree

            The platform already resolves qualified field reads elsewhere and produces a good message.
            From the same repo's flow-condition validation:

            unknown field `needs_colection` on `duly_assignment` — did you mean `needs_collection`?
            

            That is the message shape this wants. The resolver exists; it is not wired to dataset
            field paths.

            Related: #14089 (bare identifiers in flow conditions have no validation leg) is the same
            family — a validator with a per-surface hole — though a different surface.

            Suggested scope

            1. Resolve Dataset.object against the object registry. (Cheapest, highest value.)
            2. Resolve each include[] path hop-by-hop against the object graph, honouring the
              ADR-0071 3-hop limit that is already enforced structurally.
            3. Resolve dimensions[].field and measures[].field — a base field, or a
              relationship[.relationship].field path whose relationship prefix must appear in
              include. That last clause is a second real check: a dot-path whose prefix was never
              declared in include is not joinable even if the field exists.
            4. Resolve filter keys in Dataset.filter and measures[].filter, reusing the
              traversal filter-token-unknown already performs on the values.

            Timing: validate/build at the latest, matching #8902's "refuse at publish at the
            latest" posture for the layer above.

            Reproduction

            1. git clone objectstack-ai/duly, pnpm install.
            2. In src/datasets/duty-health.dataset.ts, change object: 'duly_task' to
              object: 'duly_tsk'.
            3. pnpm validate✓ Validation passed, exit 0.
            4. pnpm build → exit 0; dist/objectstack.json contains the dataset with the dangling
              base object.

            Filed from the duly dogfood application (objectstack-ai/duly#9).

            Metadata

            Metadata

            Assignees

            Type

            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

              objectstack validate and build accept a dataset whose base object, include path and dimension/measure field paths all name nothing — the same walker already resolves date-macro tokens on the identical node #14105

              Description

              @os-warren

              Summary

              An ADR-0021 dataset can name a base object that does not exist, join a
              relationship that does not exist, and bind every dimension and measure to
              fields that do not exist, and objectstack validate exits 0 with
              ✓ Validation passed. objectstack build also exits 0 and writes the dangling dataset
              into dist/objectstack.json.

              The sting is that the author-time rule pass already walks these exact nodes: a bad
              date-macro token in a measure filter is caught, path-precise, on the same traversal.
              So the machinery and the traversal are both present — only the reference resolution is
              missing.

              This is one level below #7529 (closed by #8902, which refuses a widget → dataset
              binding that names nothing). This is the dataset → object/field binding underneath it.
              A board can now be proven to point at a real dataset, and that dataset can still point at
              nothing.

              Measured

              @objectstack/spec 17.2.0, @objectstack/cli 17.2.0, on objectstack-ai/duly
              (3 datasets over duly_task). Each row is one mutation applied on its own, confirmed on
              disk before running (old spelling absent, new spelling present), then reverted.

              #Mutationpnpm validate
              1dimension field: 'period_key''period_kee' (base field)exit 0, passed
              2dimension field: 'duty.frequency''duty.frequenci' (joined field)exit 0, passed
              3measure field: 'last_update_at''last_update_att'exit 0, passed
              4measure filter key last_update_atlast_update_atttexit 0, passed
              5include: ['duty']['dutee'] (relationship does not exist)exit 0, passed
              6object: 'duly_task''duly_tsk' (base object does not exist)exit 0, passed
              7duplicate measure name (schema superRefine)exit 1 — caught
              8filter value '{7_days_ago}''{7_fortnights_ago}'exit 1 — caught

              Rows 7 and 8 are the control: datasets genuinely are in the validation path, so rows 1–6
              are not "the file was never read".

              Row 8's message shows the walker reaching all the way into a measure filter with an exact
              path:

              ✗ Author-time rules failed (1 issue)
              • dataset "duly_stagnation": Filter value "{7_fortnights_ago}" is not a resolvable placeholder.
              It is sent to the data engine as a literal string, matches no record, and the surface renders empty.
              rule: filter-token-unknown at datasets[1].measures[1].filter.last_update_at.$lt
              

              That message is the whole argument for this issue. filter-token-unknown already stands
              at datasets[1].measures[1].filter.last_update_at.$lt and reasons about the value.
              Nothing standing in that same position resolves the key — or the sibling
              measures[].field, dimensions[].field, include[], or object.

              Why this matters more for a dataset than for most metadata

              filter-token-unknown's own wording states the failure mode it exists to prevent:
              "sent to the data engine as a literal string, matches no record, and the surface renders
              empty."
              A dangling field path produces the same outcome from the same node and is not
              checked.

              A dataset is the semantic layer: dashboards and reports bind its dimensions and measures
              by name (ADR-0021), and the consumer end is now guarded (#8902). So the surviving failure
              is the quiet one — every binding resolves, the board renders, and the charts are empty or
              subtly wrong because the dataset underneath addresses columns that do not exist.

              Row 6 is the one I would prioritise: a dataset over a non-existent base object is
              unambiguously meaningless, has no forward-reference story worth preserving, and is the
              cheapest possible check.

              Precedent in the tree

              The platform already resolves qualified field reads elsewhere and produces a good message.
              From the same repo's flow-condition validation:

              unknown field `needs_colection` on `duly_assignment` — did you mean `needs_collection`?
              

              That is the message shape this wants. The resolver exists; it is not wired to dataset
              field paths.

              Related: #14089 (bare identifiers in flow conditions have no validation leg) is the same
              family — a validator with a per-surface hole — though a different surface.

              Suggested scope

              1. Resolve Dataset.object against the object registry. (Cheapest, highest value.)
              2. Resolve each include[] path hop-by-hop against the object graph, honouring the
                ADR-0071 3-hop limit that is already enforced structurally.
              3. Resolve dimensions[].field and measures[].field — a base field, or a
                relationship[.relationship].field path whose relationship prefix must appear in
                include. That last clause is a second real check: a dot-path whose prefix was never
                declared in include is not joinable even if the field exists.
              4. Resolve filter keys in Dataset.filter and measures[].filter, reusing the
                traversal filter-token-unknown already performs on the values.

              Timing: validate/build at the latest, matching #8902's "refuse at publish at the
              latest" posture for the layer above.

              Reproduction

              1. git clone objectstack-ai/duly, pnpm install.
              2. In src/datasets/duty-health.dataset.ts, change object: 'duly_task' to
                object: 'duly_tsk'.
              3. pnpm validate✓ Validation passed, exit 0.
              4. pnpm build → exit 0; dist/objectstack.json contains the dataset with the dangling
                base object.

              Filed from the duly dogfood application (objectstack-ai/duly#9).

              Metadata

              Metadata

              Assignees

              Type

              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

                objectstack validate and build accept a dataset whose base object, include path and dimension/measure field paths all name nothing — the same walker already resolves date-macro tokens on the identical node #14105

                Description

                @os-warren

                Summary

                An ADR-0021 dataset can name a base object that does not exist, join a
                relationship that does not exist, and bind every dimension and measure to
                fields that do not exist, and objectstack validate exits 0 with
                ✓ Validation passed. objectstack build also exits 0 and writes the dangling dataset
                into dist/objectstack.json.

                The sting is that the author-time rule pass already walks these exact nodes: a bad
                date-macro token in a measure filter is caught, path-precise, on the same traversal.
                So the machinery and the traversal are both present — only the reference resolution is
                missing.

                This is one level below #7529 (closed by #8902, which refuses a widget → dataset
                binding that names nothing). This is the dataset → object/field binding underneath it.
                A board can now be proven to point at a real dataset, and that dataset can still point at
                nothing.

                Measured

                @objectstack/spec 17.2.0, @objectstack/cli 17.2.0, on objectstack-ai/duly
                (3 datasets over duly_task). Each row is one mutation applied on its own, confirmed on
                disk before running (old spelling absent, new spelling present), then reverted.

                #Mutationpnpm validate
                1dimension field: 'period_key''period_kee' (base field)exit 0, passed
                2dimension field: 'duty.frequency''duty.frequenci' (joined field)exit 0, passed
                3measure field: 'last_update_at''last_update_att'exit 0, passed
                4measure filter key last_update_atlast_update_atttexit 0, passed
                5include: ['duty']['dutee'] (relationship does not exist)exit 0, passed
                6object: 'duly_task''duly_tsk' (base object does not exist)exit 0, passed
                7duplicate measure name (schema superRefine)exit 1 — caught
                8filter value '{7_days_ago}''{7_fortnights_ago}'exit 1 — caught

                Rows 7 and 8 are the control: datasets genuinely are in the validation path, so rows 1–6
                are not "the file was never read".

                Row 8's message shows the walker reaching all the way into a measure filter with an exact
                path:

                ✗ Author-time rules failed (1 issue)
                • dataset "duly_stagnation": Filter value "{7_fortnights_ago}" is not a resolvable placeholder.
                It is sent to the data engine as a literal string, matches no record, and the surface renders empty.
                rule: filter-token-unknown at datasets[1].measures[1].filter.last_update_at.$lt
                

                That message is the whole argument for this issue. filter-token-unknown already stands
                at datasets[1].measures[1].filter.last_update_at.$lt and reasons about the value.
                Nothing standing in that same position resolves the key — or the sibling
                measures[].field, dimensions[].field, include[], or object.

                Why this matters more for a dataset than for most metadata

                filter-token-unknown's own wording states the failure mode it exists to prevent:
                "sent to the data engine as a literal string, matches no record, and the surface renders
                empty."
                A dangling field path produces the same outcome from the same node and is not
                checked.

                A dataset is the semantic layer: dashboards and reports bind its dimensions and measures
                by name (ADR-0021), and the consumer end is now guarded (#8902). So the surviving failure
                is the quiet one — every binding resolves, the board renders, and the charts are empty or
                subtly wrong because the dataset underneath addresses columns that do not exist.

                Row 6 is the one I would prioritise: a dataset over a non-existent base object is
                unambiguously meaningless, has no forward-reference story worth preserving, and is the
                cheapest possible check.

                Precedent in the tree

                The platform already resolves qualified field reads elsewhere and produces a good message.
                From the same repo's flow-condition validation:

                unknown field `needs_colection` on `duly_assignment` — did you mean `needs_collection`?
                

                That is the message shape this wants. The resolver exists; it is not wired to dataset
                field paths.

                Related: #14089 (bare identifiers in flow conditions have no validation leg) is the same
                family — a validator with a per-surface hole — though a different surface.

                Suggested scope

                1. Resolve Dataset.object against the object registry. (Cheapest, highest value.)
                2. Resolve each include[] path hop-by-hop against the object graph, honouring the
                  ADR-0071 3-hop limit that is already enforced structurally.
                3. Resolve dimensions[].field and measures[].field — a base field, or a
                  relationship[.relationship].field path whose relationship prefix must appear in
                  include. That last clause is a second real check: a dot-path whose prefix was never
                  declared in include is not joinable even if the field exists.
                4. Resolve filter keys in Dataset.filter and measures[].filter, reusing the
                  traversal filter-token-unknown already performs on the values.

                Timing: validate/build at the latest, matching #8902's "refuse at publish at the
                latest" posture for the layer above.

                Reproduction

                1. git clone objectstack-ai/duly, pnpm install.
                2. In src/datasets/duty-health.dataset.ts, change object: 'duly_task' to
                  object: 'duly_tsk'.
                3. pnpm validate✓ Validation passed, exit 0.
                4. pnpm build → exit 0; dist/objectstack.json contains the dataset with the dangling
                  base object.

                Filed from the duly dogfood application (objectstack-ai/duly#9).

                Metadata

                Metadata

                Assignees

                Type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions