Guard every metadata binding: views, datasets and app nav resolve against dulyObjects - #57

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings
Sep 1, 2026
Merged

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects#57
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#51

Adds test/metadata-bindings.test.ts — one guard walking dulyViews, dulyDatasets and dulyApps and resolving every reference against dulyObjects. Labelled a stopgap pending objectstack#14105 / #14107 / #14108, written to be deleted rather than maintained, per the test/flow-predicates.test.ts convention.

Scope taken is the widened scope from the PM comment on the card (views + nav + datasets), not the datasets-only scope in the body.

Two things the reviewer should read first

1. No dangling reference exists in shipped metadata. The guard resolves 204 references — 145 view, 34 dataset, 25 nav — and all 204 resolve. So no metadata file is touched by this PR; the only change is the test. That is a real result, not a skipped check: the per-surface counters and the joined-path assertion below are what stop "green" from meaning "the walk did nothing".

2. The card's premise that "nothing has ever checked the five view files or the app nav" is wrong.test/views.test.ts (landed in #49) already resolves the simple view surface and the nav objectName / viewName pair. The card is still real — but the value is in the superset, not in first coverage. The remaining holes it closes:

HoleWhere it was
Datasets not covered at alldatasets.test.ts pins caliber / date-macros / absences, never that a field path names anything real
Dotted paths resolved by nothingviews.test.ts opens its checker with if (!name || name.includes('.')) return — so duty.frequency, the one joined path this app ships, was unchecked everywhere
Hand-copied system-column listviews.test.ts carries business_unit_id (not a platform column) and omits owning_business_unit_id, tenant_id, user_id, deleted_at (which are). This file imports the spec's own SystemFieldName
A view bound to a platform object would false-positiveits bound object had to be in dulyObjects; platform names now resolve from isPlatformProvidedObjectName
No self-test on either file24 synthetic-fixture cases here pin both directions

The overlap is declared in the file header rather than resolved by deleting another card's guard — that is a follow-up, filed as #58. Until then it is benign: this file is the strict superset, so any disagreement reds here first.

What is checked

  • Viewscolumns[] (both the bare-string and { field } spellings), columns[].summary.field, columns[].prefix.field, filter[].field, sort[].field (plus the legacy "field desc" string), grouping.fields[].field, rowColor.field, the searchableFields / filterableFields / hiddenFields / fieldOrder lists, and every binding block — kanban, calendar, gantt, timeline, gallery, tree, map.
  • Datasets — base object, every include path, every dimension and measure field, and every filter key (the column is the key; the walk recurses $and / $or / $not and never mistakes an operator for a column). Also that a joined path's relationship is actually in include — every segment can name something real and still have no join to travel (ADR-0071).
  • Joined paths — through a reference-carrying field to a real field on the target. Hopping through a scalar is reported as its own, more legible failure.
  • Nav — every objectName, every viewName against the views declared on that object (a view of the right name on the wrong object is a finding), filters keys, and the no-viewName case when the object has no default list.

Beyond the card's enumerated list, and flagged because it is an addition: bulkActionDefs with operation: 'update' write fields — the static patch keys and the collected params[].name, which BulkActionDefSchema documents as merged over the patch. Same defect class, one layer down. Only update; on a custom def those are action params, not fields.

Narrowings, stated rather than hidden

  • A hop into a platform object is a boundary, not a resolution.@objectstack/spec exports the platform object name registry but no field lists, so owner.some_typo cannot be judged. The hop is verified, the path is recorded, and a test fails the day one is authored — the hole is loud rather than silent.
  • Field-bearing slots are hand-listed (the spec exports no slot table for view field references, unlike FLOW_NODE_EXPRESSION_PATHS for flow predicates), and the list polices itself: a tripwire scans the metadata for field-ish slots and fails on any the walk cannot read.
  • Form views are not walked. This app declares none; a tripwire fails the day one appears.
  • Tripwires likewise cover unknown nav item types and lookups targeting a name that is neither declared nor platform-provided.

Proving it can fail — four ablations, one per surface

Each leg: anchor asserted to occur exactly once before the edit, both the injected and the removed literal grepped afterwards, a non-empty git diff --stat required, and a trap … EXIT INT TERM restoring from HEAD (the guard was committed first, so this is a real restore point and not the index).

#Mutationpnpm validateguard
1view column categorycategoree_typoexit 0✓ Validation passed (338ms)exit 1
2dataset filter keylast_update_atlast_update_atttexit 0✓ Validation passed (334ms)exit 1
3nav viewName: 'stalled''not_moving'exit 0✓ Validation passed (430ms)exit 1
4joined path duty.frequencyduty.frequenceeexit 0✓ Validation passed (432ms)exit 1

Leg 3 is the one worth reading — the guard prints exactly what makes #14108 nasty:

app duly_app · nav 'nav_stalled' · viewName: "not_moving" — duly_task declares no list view named "not_moving" — the shell SILENTLY falls back to the default view and keeps this entry's authored label.

The label stays "Not moving". The screen looks right and shows the wrong rows.

Observed direction, not the template one: leg 4 reddened two tests, not one — the finding, plus the non-vacuity assertion that the joined path was reached at all. Diagnostics went up, which is the correct reading here: mutating the app's only multi-hop path both produces a finding and removes the evidence that the multi-hop walk runs.

All four restored; tree verified byte-identical to HEAD afterwards (git status --porcelain empty, git diff HEAD empty, all four anchors back at count 1).

Gates

main moved while this was in flight (#54 source defaults, #56 hierarchy-security — both touch objects this guard resolves against). It is merged in at 27d31a0; no field was renamed or removed, only default: flags on select options.

Gates re-run on the final commit 27d31a0, after the merge:

pnpm validate exit 0 ✓ Validation passed (400ms)
pnpm typecheck exit 0 tsc --noEmit
pnpm test exit 0 Test Files 13 passed (13) · Tests 407 passed (407)
pnpm build exit 0 Artifact: dist/objectstack.json (96.2 KB)

Exit codes captured before any pipe. 407 tests = 375 on merged main + 32 added. (The pre-merge reading at cf62af9 was 402 = 370 + 32; #54 and #56 added five.)

No changeset: this repo carries no changeset tooling, and the change is test-only.


Generated by Claude Code

Stopgap pending objectstack#14105 / #14107 / #14108: no field path in the
UI or analytics layer is resolved at author time, and both `validate` and
`build` exit 0 on a dangling one.
Walks dulyViews, dulyDatasets and dulyApps, resolving every reference
against dulyObjects: view columns / filter / sort / grouping / binding
blocks / bulk-action patch keys, dataset base object / include /
dimension / measure fields / filter KEYS, joined paths through a real
lookup, and every nav objectName + viewName. Platform objects resolve
from the spec's own registry; system columns from SystemFieldName.
Includes a synthetic-fixture self-test pinning both directions on all
four surfaces, plus tripwires for unknown field slots, unknown nav item
types, form views and platform-object boundaries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Picks up #54 (source defaults to self) and #56 (hierarchy-security). Both
touch objects this guard resolves against; no field was renamed or removed,
only `default:` flags on select options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 06:13
@os-warren
os-warren merged commit 92438fb into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stopgap: guard dataset field paths — validate and build both pass on a dataset bound to nothing

2 participants

@os-warren@claude
, '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

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects - #57

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings
Sep 1, 2026
Merged

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects#57
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#51

Adds test/metadata-bindings.test.ts — one guard walking dulyViews, dulyDatasets and dulyApps and resolving every reference against dulyObjects. Labelled a stopgap pending objectstack#14105 / #14107 / #14108, written to be deleted rather than maintained, per the test/flow-predicates.test.ts convention.

Scope taken is the widened scope from the PM comment on the card (views + nav + datasets), not the datasets-only scope in the body.

Two things the reviewer should read first

1. No dangling reference exists in shipped metadata. The guard resolves 204 references — 145 view, 34 dataset, 25 nav — and all 204 resolve. So no metadata file is touched by this PR; the only change is the test. That is a real result, not a skipped check: the per-surface counters and the joined-path assertion below are what stop "green" from meaning "the walk did nothing".

2. The card's premise that "nothing has ever checked the five view files or the app nav" is wrong.test/views.test.ts (landed in #49) already resolves the simple view surface and the nav objectName / viewName pair. The card is still real — but the value is in the superset, not in first coverage. The remaining holes it closes:

HoleWhere it was
Datasets not covered at alldatasets.test.ts pins caliber / date-macros / absences, never that a field path names anything real
Dotted paths resolved by nothingviews.test.ts opens its checker with if (!name || name.includes('.')) return — so duty.frequency, the one joined path this app ships, was unchecked everywhere
Hand-copied system-column listviews.test.ts carries business_unit_id (not a platform column) and omits owning_business_unit_id, tenant_id, user_id, deleted_at (which are). This file imports the spec's own SystemFieldName
A view bound to a platform object would false-positiveits bound object had to be in dulyObjects; platform names now resolve from isPlatformProvidedObjectName
No self-test on either file24 synthetic-fixture cases here pin both directions

The overlap is declared in the file header rather than resolved by deleting another card's guard — that is a follow-up, filed as #58. Until then it is benign: this file is the strict superset, so any disagreement reds here first.

What is checked

  • Viewscolumns[] (both the bare-string and { field } spellings), columns[].summary.field, columns[].prefix.field, filter[].field, sort[].field (plus the legacy "field desc" string), grouping.fields[].field, rowColor.field, the searchableFields / filterableFields / hiddenFields / fieldOrder lists, and every binding block — kanban, calendar, gantt, timeline, gallery, tree, map.
  • Datasets — base object, every include path, every dimension and measure field, and every filter key (the column is the key; the walk recurses $and / $or / $not and never mistakes an operator for a column). Also that a joined path's relationship is actually in include — every segment can name something real and still have no join to travel (ADR-0071).
  • Joined paths — through a reference-carrying field to a real field on the target. Hopping through a scalar is reported as its own, more legible failure.
  • Nav — every objectName, every viewName against the views declared on that object (a view of the right name on the wrong object is a finding), filters keys, and the no-viewName case when the object has no default list.

Beyond the card's enumerated list, and flagged because it is an addition: bulkActionDefs with operation: 'update' write fields — the static patch keys and the collected params[].name, which BulkActionDefSchema documents as merged over the patch. Same defect class, one layer down. Only update; on a custom def those are action params, not fields.

Narrowings, stated rather than hidden

  • A hop into a platform object is a boundary, not a resolution.@objectstack/spec exports the platform object name registry but no field lists, so owner.some_typo cannot be judged. The hop is verified, the path is recorded, and a test fails the day one is authored — the hole is loud rather than silent.
  • Field-bearing slots are hand-listed (the spec exports no slot table for view field references, unlike FLOW_NODE_EXPRESSION_PATHS for flow predicates), and the list polices itself: a tripwire scans the metadata for field-ish slots and fails on any the walk cannot read.
  • Form views are not walked. This app declares none; a tripwire fails the day one appears.
  • Tripwires likewise cover unknown nav item types and lookups targeting a name that is neither declared nor platform-provided.

Proving it can fail — four ablations, one per surface

Each leg: anchor asserted to occur exactly once before the edit, both the injected and the removed literal grepped afterwards, a non-empty git diff --stat required, and a trap … EXIT INT TERM restoring from HEAD (the guard was committed first, so this is a real restore point and not the index).

#Mutationpnpm validateguard
1view column categorycategoree_typoexit 0✓ Validation passed (338ms)exit 1
2dataset filter keylast_update_atlast_update_atttexit 0✓ Validation passed (334ms)exit 1
3nav viewName: 'stalled''not_moving'exit 0✓ Validation passed (430ms)exit 1
4joined path duty.frequencyduty.frequenceeexit 0✓ Validation passed (432ms)exit 1

Leg 3 is the one worth reading — the guard prints exactly what makes #14108 nasty:

app duly_app · nav 'nav_stalled' · viewName: "not_moving" — duly_task declares no list view named "not_moving" — the shell SILENTLY falls back to the default view and keeps this entry's authored label.

The label stays "Not moving". The screen looks right and shows the wrong rows.

Observed direction, not the template one: leg 4 reddened two tests, not one — the finding, plus the non-vacuity assertion that the joined path was reached at all. Diagnostics went up, which is the correct reading here: mutating the app's only multi-hop path both produces a finding and removes the evidence that the multi-hop walk runs.

All four restored; tree verified byte-identical to HEAD afterwards (git status --porcelain empty, git diff HEAD empty, all four anchors back at count 1).

Gates

main moved while this was in flight (#54 source defaults, #56 hierarchy-security — both touch objects this guard resolves against). It is merged in at 27d31a0; no field was renamed or removed, only default: flags on select options.

Gates re-run on the final commit 27d31a0, after the merge:

pnpm validate exit 0 ✓ Validation passed (400ms)
pnpm typecheck exit 0 tsc --noEmit
pnpm test exit 0 Test Files 13 passed (13) · Tests 407 passed (407)
pnpm build exit 0 Artifact: dist/objectstack.json (96.2 KB)

Exit codes captured before any pipe. 407 tests = 375 on merged main + 32 added. (The pre-merge reading at cf62af9 was 402 = 370 + 32; #54 and #56 added five.)

No changeset: this repo carries no changeset tooling, and the change is test-only.


Generated by Claude Code

Stopgap pending objectstack#14105 / #14107 / #14108: no field path in the
UI or analytics layer is resolved at author time, and both `validate` and
`build` exit 0 on a dangling one.
Walks dulyViews, dulyDatasets and dulyApps, resolving every reference
against dulyObjects: view columns / filter / sort / grouping / binding
blocks / bulk-action patch keys, dataset base object / include /
dimension / measure fields / filter KEYS, joined paths through a real
lookup, and every nav objectName + viewName. Platform objects resolve
from the spec's own registry; system columns from SystemFieldName.
Includes a synthetic-fixture self-test pinning both directions on all
four surfaces, plus tripwires for unknown field slots, unknown nav item
types, form views and platform-object boundaries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Picks up #54 (source defaults to self) and #56 (hierarchy-security). Both
touch objects this guard resolves against; no field was renamed or removed,
only `default:` flags on select options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 06:13
@os-warren
os-warren merged commit 92438fb into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stopgap: guard dataset field paths — validate and build both pass on a dataset bound to nothing

2 participants

@os-warren@claude
, '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

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects - #57

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings
Sep 1, 2026
Merged

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects#57
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#51

Adds test/metadata-bindings.test.ts — one guard walking dulyViews, dulyDatasets and dulyApps and resolving every reference against dulyObjects. Labelled a stopgap pending objectstack#14105 / #14107 / #14108, written to be deleted rather than maintained, per the test/flow-predicates.test.ts convention.

Scope taken is the widened scope from the PM comment on the card (views + nav + datasets), not the datasets-only scope in the body.

Two things the reviewer should read first

1. No dangling reference exists in shipped metadata. The guard resolves 204 references — 145 view, 34 dataset, 25 nav — and all 204 resolve. So no metadata file is touched by this PR; the only change is the test. That is a real result, not a skipped check: the per-surface counters and the joined-path assertion below are what stop "green" from meaning "the walk did nothing".

2. The card's premise that "nothing has ever checked the five view files or the app nav" is wrong.test/views.test.ts (landed in #49) already resolves the simple view surface and the nav objectName / viewName pair. The card is still real — but the value is in the superset, not in first coverage. The remaining holes it closes:

HoleWhere it was
Datasets not covered at alldatasets.test.ts pins caliber / date-macros / absences, never that a field path names anything real
Dotted paths resolved by nothingviews.test.ts opens its checker with if (!name || name.includes('.')) return — so duty.frequency, the one joined path this app ships, was unchecked everywhere
Hand-copied system-column listviews.test.ts carries business_unit_id (not a platform column) and omits owning_business_unit_id, tenant_id, user_id, deleted_at (which are). This file imports the spec's own SystemFieldName
A view bound to a platform object would false-positiveits bound object had to be in dulyObjects; platform names now resolve from isPlatformProvidedObjectName
No self-test on either file24 synthetic-fixture cases here pin both directions

The overlap is declared in the file header rather than resolved by deleting another card's guard — that is a follow-up, filed as #58. Until then it is benign: this file is the strict superset, so any disagreement reds here first.

What is checked

  • Viewscolumns[] (both the bare-string and { field } spellings), columns[].summary.field, columns[].prefix.field, filter[].field, sort[].field (plus the legacy "field desc" string), grouping.fields[].field, rowColor.field, the searchableFields / filterableFields / hiddenFields / fieldOrder lists, and every binding block — kanban, calendar, gantt, timeline, gallery, tree, map.
  • Datasets — base object, every include path, every dimension and measure field, and every filter key (the column is the key; the walk recurses $and / $or / $not and never mistakes an operator for a column). Also that a joined path's relationship is actually in include — every segment can name something real and still have no join to travel (ADR-0071).
  • Joined paths — through a reference-carrying field to a real field on the target. Hopping through a scalar is reported as its own, more legible failure.
  • Nav — every objectName, every viewName against the views declared on that object (a view of the right name on the wrong object is a finding), filters keys, and the no-viewName case when the object has no default list.

Beyond the card's enumerated list, and flagged because it is an addition: bulkActionDefs with operation: 'update' write fields — the static patch keys and the collected params[].name, which BulkActionDefSchema documents as merged over the patch. Same defect class, one layer down. Only update; on a custom def those are action params, not fields.

Narrowings, stated rather than hidden

  • A hop into a platform object is a boundary, not a resolution.@objectstack/spec exports the platform object name registry but no field lists, so owner.some_typo cannot be judged. The hop is verified, the path is recorded, and a test fails the day one is authored — the hole is loud rather than silent.
  • Field-bearing slots are hand-listed (the spec exports no slot table for view field references, unlike FLOW_NODE_EXPRESSION_PATHS for flow predicates), and the list polices itself: a tripwire scans the metadata for field-ish slots and fails on any the walk cannot read.
  • Form views are not walked. This app declares none; a tripwire fails the day one appears.
  • Tripwires likewise cover unknown nav item types and lookups targeting a name that is neither declared nor platform-provided.

Proving it can fail — four ablations, one per surface

Each leg: anchor asserted to occur exactly once before the edit, both the injected and the removed literal grepped afterwards, a non-empty git diff --stat required, and a trap … EXIT INT TERM restoring from HEAD (the guard was committed first, so this is a real restore point and not the index).

#Mutationpnpm validateguard
1view column categorycategoree_typoexit 0✓ Validation passed (338ms)exit 1
2dataset filter keylast_update_atlast_update_atttexit 0✓ Validation passed (334ms)exit 1
3nav viewName: 'stalled''not_moving'exit 0✓ Validation passed (430ms)exit 1
4joined path duty.frequencyduty.frequenceeexit 0✓ Validation passed (432ms)exit 1

Leg 3 is the one worth reading — the guard prints exactly what makes #14108 nasty:

app duly_app · nav 'nav_stalled' · viewName: "not_moving" — duly_task declares no list view named "not_moving" — the shell SILENTLY falls back to the default view and keeps this entry's authored label.

The label stays "Not moving". The screen looks right and shows the wrong rows.

Observed direction, not the template one: leg 4 reddened two tests, not one — the finding, plus the non-vacuity assertion that the joined path was reached at all. Diagnostics went up, which is the correct reading here: mutating the app's only multi-hop path both produces a finding and removes the evidence that the multi-hop walk runs.

All four restored; tree verified byte-identical to HEAD afterwards (git status --porcelain empty, git diff HEAD empty, all four anchors back at count 1).

Gates

main moved while this was in flight (#54 source defaults, #56 hierarchy-security — both touch objects this guard resolves against). It is merged in at 27d31a0; no field was renamed or removed, only default: flags on select options.

Gates re-run on the final commit 27d31a0, after the merge:

pnpm validate exit 0 ✓ Validation passed (400ms)
pnpm typecheck exit 0 tsc --noEmit
pnpm test exit 0 Test Files 13 passed (13) · Tests 407 passed (407)
pnpm build exit 0 Artifact: dist/objectstack.json (96.2 KB)

Exit codes captured before any pipe. 407 tests = 375 on merged main + 32 added. (The pre-merge reading at cf62af9 was 402 = 370 + 32; #54 and #56 added five.)

No changeset: this repo carries no changeset tooling, and the change is test-only.


Generated by Claude Code

Stopgap pending objectstack#14105 / #14107 / #14108: no field path in the
UI or analytics layer is resolved at author time, and both `validate` and
`build` exit 0 on a dangling one.
Walks dulyViews, dulyDatasets and dulyApps, resolving every reference
against dulyObjects: view columns / filter / sort / grouping / binding
blocks / bulk-action patch keys, dataset base object / include /
dimension / measure fields / filter KEYS, joined paths through a real
lookup, and every nav objectName + viewName. Platform objects resolve
from the spec's own registry; system columns from SystemFieldName.
Includes a synthetic-fixture self-test pinning both directions on all
four surfaces, plus tripwires for unknown field slots, unknown nav item
types, form views and platform-object boundaries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Picks up #54 (source defaults to self) and #56 (hierarchy-security). Both
touch objects this guard resolves against; no field was renamed or removed,
only `default:` flags on select options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 06:13
@os-warren
os-warren merged commit 92438fb into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stopgap: guard dataset field paths — validate and build both pass on a dataset bound to nothing

2 participants

@os-warren@claude
, '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

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects - #57

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings
Sep 1, 2026
Merged

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects#57
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#51

Adds test/metadata-bindings.test.ts — one guard walking dulyViews, dulyDatasets and dulyApps and resolving every reference against dulyObjects. Labelled a stopgap pending objectstack#14105 / #14107 / #14108, written to be deleted rather than maintained, per the test/flow-predicates.test.ts convention.

Scope taken is the widened scope from the PM comment on the card (views + nav + datasets), not the datasets-only scope in the body.

Two things the reviewer should read first

1. No dangling reference exists in shipped metadata. The guard resolves 204 references — 145 view, 34 dataset, 25 nav — and all 204 resolve. So no metadata file is touched by this PR; the only change is the test. That is a real result, not a skipped check: the per-surface counters and the joined-path assertion below are what stop "green" from meaning "the walk did nothing".

2. The card's premise that "nothing has ever checked the five view files or the app nav" is wrong.test/views.test.ts (landed in #49) already resolves the simple view surface and the nav objectName / viewName pair. The card is still real — but the value is in the superset, not in first coverage. The remaining holes it closes:

HoleWhere it was
Datasets not covered at alldatasets.test.ts pins caliber / date-macros / absences, never that a field path names anything real
Dotted paths resolved by nothingviews.test.ts opens its checker with if (!name || name.includes('.')) return — so duty.frequency, the one joined path this app ships, was unchecked everywhere
Hand-copied system-column listviews.test.ts carries business_unit_id (not a platform column) and omits owning_business_unit_id, tenant_id, user_id, deleted_at (which are). This file imports the spec's own SystemFieldName
A view bound to a platform object would false-positiveits bound object had to be in dulyObjects; platform names now resolve from isPlatformProvidedObjectName
No self-test on either file24 synthetic-fixture cases here pin both directions

The overlap is declared in the file header rather than resolved by deleting another card's guard — that is a follow-up, filed as #58. Until then it is benign: this file is the strict superset, so any disagreement reds here first.

What is checked

  • Viewscolumns[] (both the bare-string and { field } spellings), columns[].summary.field, columns[].prefix.field, filter[].field, sort[].field (plus the legacy "field desc" string), grouping.fields[].field, rowColor.field, the searchableFields / filterableFields / hiddenFields / fieldOrder lists, and every binding block — kanban, calendar, gantt, timeline, gallery, tree, map.
  • Datasets — base object, every include path, every dimension and measure field, and every filter key (the column is the key; the walk recurses $and / $or / $not and never mistakes an operator for a column). Also that a joined path's relationship is actually in include — every segment can name something real and still have no join to travel (ADR-0071).
  • Joined paths — through a reference-carrying field to a real field on the target. Hopping through a scalar is reported as its own, more legible failure.
  • Nav — every objectName, every viewName against the views declared on that object (a view of the right name on the wrong object is a finding), filters keys, and the no-viewName case when the object has no default list.

Beyond the card's enumerated list, and flagged because it is an addition: bulkActionDefs with operation: 'update' write fields — the static patch keys and the collected params[].name, which BulkActionDefSchema documents as merged over the patch. Same defect class, one layer down. Only update; on a custom def those are action params, not fields.

Narrowings, stated rather than hidden

  • A hop into a platform object is a boundary, not a resolution.@objectstack/spec exports the platform object name registry but no field lists, so owner.some_typo cannot be judged. The hop is verified, the path is recorded, and a test fails the day one is authored — the hole is loud rather than silent.
  • Field-bearing slots are hand-listed (the spec exports no slot table for view field references, unlike FLOW_NODE_EXPRESSION_PATHS for flow predicates), and the list polices itself: a tripwire scans the metadata for field-ish slots and fails on any the walk cannot read.
  • Form views are not walked. This app declares none; a tripwire fails the day one appears.
  • Tripwires likewise cover unknown nav item types and lookups targeting a name that is neither declared nor platform-provided.

Proving it can fail — four ablations, one per surface

Each leg: anchor asserted to occur exactly once before the edit, both the injected and the removed literal grepped afterwards, a non-empty git diff --stat required, and a trap … EXIT INT TERM restoring from HEAD (the guard was committed first, so this is a real restore point and not the index).

#Mutationpnpm validateguard
1view column categorycategoree_typoexit 0✓ Validation passed (338ms)exit 1
2dataset filter keylast_update_atlast_update_atttexit 0✓ Validation passed (334ms)exit 1
3nav viewName: 'stalled''not_moving'exit 0✓ Validation passed (430ms)exit 1
4joined path duty.frequencyduty.frequenceeexit 0✓ Validation passed (432ms)exit 1

Leg 3 is the one worth reading — the guard prints exactly what makes #14108 nasty:

app duly_app · nav 'nav_stalled' · viewName: "not_moving" — duly_task declares no list view named "not_moving" — the shell SILENTLY falls back to the default view and keeps this entry's authored label.

The label stays "Not moving". The screen looks right and shows the wrong rows.

Observed direction, not the template one: leg 4 reddened two tests, not one — the finding, plus the non-vacuity assertion that the joined path was reached at all. Diagnostics went up, which is the correct reading here: mutating the app's only multi-hop path both produces a finding and removes the evidence that the multi-hop walk runs.

All four restored; tree verified byte-identical to HEAD afterwards (git status --porcelain empty, git diff HEAD empty, all four anchors back at count 1).

Gates

main moved while this was in flight (#54 source defaults, #56 hierarchy-security — both touch objects this guard resolves against). It is merged in at 27d31a0; no field was renamed or removed, only default: flags on select options.

Gates re-run on the final commit 27d31a0, after the merge:

pnpm validate exit 0 ✓ Validation passed (400ms)
pnpm typecheck exit 0 tsc --noEmit
pnpm test exit 0 Test Files 13 passed (13) · Tests 407 passed (407)
pnpm build exit 0 Artifact: dist/objectstack.json (96.2 KB)

Exit codes captured before any pipe. 407 tests = 375 on merged main + 32 added. (The pre-merge reading at cf62af9 was 402 = 370 + 32; #54 and #56 added five.)

No changeset: this repo carries no changeset tooling, and the change is test-only.


Generated by Claude Code

Stopgap pending objectstack#14105 / #14107 / #14108: no field path in the
UI or analytics layer is resolved at author time, and both `validate` and
`build` exit 0 on a dangling one.
Walks dulyViews, dulyDatasets and dulyApps, resolving every reference
against dulyObjects: view columns / filter / sort / grouping / binding
blocks / bulk-action patch keys, dataset base object / include /
dimension / measure fields / filter KEYS, joined paths through a real
lookup, and every nav objectName + viewName. Platform objects resolve
from the spec's own registry; system columns from SystemFieldName.
Includes a synthetic-fixture self-test pinning both directions on all
four surfaces, plus tripwires for unknown field slots, unknown nav item
types, form views and platform-object boundaries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Picks up #54 (source defaults to self) and #56 (hierarchy-security). Both
touch objects this guard resolves against; no field was renamed or removed,
only `default:` flags on select options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 06:13
@os-warren
os-warren merged commit 92438fb into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stopgap: guard dataset field paths — validate and build both pass on a dataset bound to nothing

2 participants

@os-warren@claude
, '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

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects - #57

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings
Sep 1, 2026
Merged

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects#57
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#51

Adds test/metadata-bindings.test.ts — one guard walking dulyViews, dulyDatasets and dulyApps and resolving every reference against dulyObjects. Labelled a stopgap pending objectstack#14105 / #14107 / #14108, written to be deleted rather than maintained, per the test/flow-predicates.test.ts convention.

Scope taken is the widened scope from the PM comment on the card (views + nav + datasets), not the datasets-only scope in the body.

Two things the reviewer should read first

1. No dangling reference exists in shipped metadata. The guard resolves 204 references — 145 view, 34 dataset, 25 nav — and all 204 resolve. So no metadata file is touched by this PR; the only change is the test. That is a real result, not a skipped check: the per-surface counters and the joined-path assertion below are what stop "green" from meaning "the walk did nothing".

2. The card's premise that "nothing has ever checked the five view files or the app nav" is wrong.test/views.test.ts (landed in #49) already resolves the simple view surface and the nav objectName / viewName pair. The card is still real — but the value is in the superset, not in first coverage. The remaining holes it closes:

HoleWhere it was
Datasets not covered at alldatasets.test.ts pins caliber / date-macros / absences, never that a field path names anything real
Dotted paths resolved by nothingviews.test.ts opens its checker with if (!name || name.includes('.')) return — so duty.frequency, the one joined path this app ships, was unchecked everywhere
Hand-copied system-column listviews.test.ts carries business_unit_id (not a platform column) and omits owning_business_unit_id, tenant_id, user_id, deleted_at (which are). This file imports the spec's own SystemFieldName
A view bound to a platform object would false-positiveits bound object had to be in dulyObjects; platform names now resolve from isPlatformProvidedObjectName
No self-test on either file24 synthetic-fixture cases here pin both directions

The overlap is declared in the file header rather than resolved by deleting another card's guard — that is a follow-up, filed as #58. Until then it is benign: this file is the strict superset, so any disagreement reds here first.

What is checked

  • Viewscolumns[] (both the bare-string and { field } spellings), columns[].summary.field, columns[].prefix.field, filter[].field, sort[].field (plus the legacy "field desc" string), grouping.fields[].field, rowColor.field, the searchableFields / filterableFields / hiddenFields / fieldOrder lists, and every binding block — kanban, calendar, gantt, timeline, gallery, tree, map.
  • Datasets — base object, every include path, every dimension and measure field, and every filter key (the column is the key; the walk recurses $and / $or / $not and never mistakes an operator for a column). Also that a joined path's relationship is actually in include — every segment can name something real and still have no join to travel (ADR-0071).
  • Joined paths — through a reference-carrying field to a real field on the target. Hopping through a scalar is reported as its own, more legible failure.
  • Nav — every objectName, every viewName against the views declared on that object (a view of the right name on the wrong object is a finding), filters keys, and the no-viewName case when the object has no default list.

Beyond the card's enumerated list, and flagged because it is an addition: bulkActionDefs with operation: 'update' write fields — the static patch keys and the collected params[].name, which BulkActionDefSchema documents as merged over the patch. Same defect class, one layer down. Only update; on a custom def those are action params, not fields.

Narrowings, stated rather than hidden

  • A hop into a platform object is a boundary, not a resolution.@objectstack/spec exports the platform object name registry but no field lists, so owner.some_typo cannot be judged. The hop is verified, the path is recorded, and a test fails the day one is authored — the hole is loud rather than silent.
  • Field-bearing slots are hand-listed (the spec exports no slot table for view field references, unlike FLOW_NODE_EXPRESSION_PATHS for flow predicates), and the list polices itself: a tripwire scans the metadata for field-ish slots and fails on any the walk cannot read.
  • Form views are not walked. This app declares none; a tripwire fails the day one appears.
  • Tripwires likewise cover unknown nav item types and lookups targeting a name that is neither declared nor platform-provided.

Proving it can fail — four ablations, one per surface

Each leg: anchor asserted to occur exactly once before the edit, both the injected and the removed literal grepped afterwards, a non-empty git diff --stat required, and a trap … EXIT INT TERM restoring from HEAD (the guard was committed first, so this is a real restore point and not the index).

#Mutationpnpm validateguard
1view column categorycategoree_typoexit 0✓ Validation passed (338ms)exit 1
2dataset filter keylast_update_atlast_update_atttexit 0✓ Validation passed (334ms)exit 1
3nav viewName: 'stalled''not_moving'exit 0✓ Validation passed (430ms)exit 1
4joined path duty.frequencyduty.frequenceeexit 0✓ Validation passed (432ms)exit 1

Leg 3 is the one worth reading — the guard prints exactly what makes #14108 nasty:

app duly_app · nav 'nav_stalled' · viewName: "not_moving" — duly_task declares no list view named "not_moving" — the shell SILENTLY falls back to the default view and keeps this entry's authored label.

The label stays "Not moving". The screen looks right and shows the wrong rows.

Observed direction, not the template one: leg 4 reddened two tests, not one — the finding, plus the non-vacuity assertion that the joined path was reached at all. Diagnostics went up, which is the correct reading here: mutating the app's only multi-hop path both produces a finding and removes the evidence that the multi-hop walk runs.

All four restored; tree verified byte-identical to HEAD afterwards (git status --porcelain empty, git diff HEAD empty, all four anchors back at count 1).

Gates

main moved while this was in flight (#54 source defaults, #56 hierarchy-security — both touch objects this guard resolves against). It is merged in at 27d31a0; no field was renamed or removed, only default: flags on select options.

Gates re-run on the final commit 27d31a0, after the merge:

pnpm validate exit 0 ✓ Validation passed (400ms)
pnpm typecheck exit 0 tsc --noEmit
pnpm test exit 0 Test Files 13 passed (13) · Tests 407 passed (407)
pnpm build exit 0 Artifact: dist/objectstack.json (96.2 KB)

Exit codes captured before any pipe. 407 tests = 375 on merged main + 32 added. (The pre-merge reading at cf62af9 was 402 = 370 + 32; #54 and #56 added five.)

No changeset: this repo carries no changeset tooling, and the change is test-only.


Generated by Claude Code

Stopgap pending objectstack#14105 / #14107 / #14108: no field path in the
UI or analytics layer is resolved at author time, and both `validate` and
`build` exit 0 on a dangling one.
Walks dulyViews, dulyDatasets and dulyApps, resolving every reference
against dulyObjects: view columns / filter / sort / grouping / binding
blocks / bulk-action patch keys, dataset base object / include /
dimension / measure fields / filter KEYS, joined paths through a real
lookup, and every nav objectName + viewName. Platform objects resolve
from the spec's own registry; system columns from SystemFieldName.
Includes a synthetic-fixture self-test pinning both directions on all
four surfaces, plus tripwires for unknown field slots, unknown nav item
types, form views and platform-object boundaries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Picks up #54 (source defaults to self) and #56 (hierarchy-security). Both
touch objects this guard resolves against; no field was renamed or removed,
only `default:` flags on select options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 06:13
@os-warren
os-warren merged commit 92438fb into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stopgap: guard dataset field paths — validate and build both pass on a dataset bound to nothing

2 participants

@os-warren@claude
, '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

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects - #57

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings
Sep 1, 2026
Merged

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects#57
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#51

Adds test/metadata-bindings.test.ts — one guard walking dulyViews, dulyDatasets and dulyApps and resolving every reference against dulyObjects. Labelled a stopgap pending objectstack#14105 / #14107 / #14108, written to be deleted rather than maintained, per the test/flow-predicates.test.ts convention.

Scope taken is the widened scope from the PM comment on the card (views + nav + datasets), not the datasets-only scope in the body.

Two things the reviewer should read first

1. No dangling reference exists in shipped metadata. The guard resolves 204 references — 145 view, 34 dataset, 25 nav — and all 204 resolve. So no metadata file is touched by this PR; the only change is the test. That is a real result, not a skipped check: the per-surface counters and the joined-path assertion below are what stop "green" from meaning "the walk did nothing".

2. The card's premise that "nothing has ever checked the five view files or the app nav" is wrong.test/views.test.ts (landed in #49) already resolves the simple view surface and the nav objectName / viewName pair. The card is still real — but the value is in the superset, not in first coverage. The remaining holes it closes:

HoleWhere it was
Datasets not covered at alldatasets.test.ts pins caliber / date-macros / absences, never that a field path names anything real
Dotted paths resolved by nothingviews.test.ts opens its checker with if (!name || name.includes('.')) return — so duty.frequency, the one joined path this app ships, was unchecked everywhere
Hand-copied system-column listviews.test.ts carries business_unit_id (not a platform column) and omits owning_business_unit_id, tenant_id, user_id, deleted_at (which are). This file imports the spec's own SystemFieldName
A view bound to a platform object would false-positiveits bound object had to be in dulyObjects; platform names now resolve from isPlatformProvidedObjectName
No self-test on either file24 synthetic-fixture cases here pin both directions

The overlap is declared in the file header rather than resolved by deleting another card's guard — that is a follow-up, filed as #58. Until then it is benign: this file is the strict superset, so any disagreement reds here first.

What is checked

  • Viewscolumns[] (both the bare-string and { field } spellings), columns[].summary.field, columns[].prefix.field, filter[].field, sort[].field (plus the legacy "field desc" string), grouping.fields[].field, rowColor.field, the searchableFields / filterableFields / hiddenFields / fieldOrder lists, and every binding block — kanban, calendar, gantt, timeline, gallery, tree, map.
  • Datasets — base object, every include path, every dimension and measure field, and every filter key (the column is the key; the walk recurses $and / $or / $not and never mistakes an operator for a column). Also that a joined path's relationship is actually in include — every segment can name something real and still have no join to travel (ADR-0071).
  • Joined paths — through a reference-carrying field to a real field on the target. Hopping through a scalar is reported as its own, more legible failure.
  • Nav — every objectName, every viewName against the views declared on that object (a view of the right name on the wrong object is a finding), filters keys, and the no-viewName case when the object has no default list.

Beyond the card's enumerated list, and flagged because it is an addition: bulkActionDefs with operation: 'update' write fields — the static patch keys and the collected params[].name, which BulkActionDefSchema documents as merged over the patch. Same defect class, one layer down. Only update; on a custom def those are action params, not fields.

Narrowings, stated rather than hidden

  • A hop into a platform object is a boundary, not a resolution.@objectstack/spec exports the platform object name registry but no field lists, so owner.some_typo cannot be judged. The hop is verified, the path is recorded, and a test fails the day one is authored — the hole is loud rather than silent.
  • Field-bearing slots are hand-listed (the spec exports no slot table for view field references, unlike FLOW_NODE_EXPRESSION_PATHS for flow predicates), and the list polices itself: a tripwire scans the metadata for field-ish slots and fails on any the walk cannot read.
  • Form views are not walked. This app declares none; a tripwire fails the day one appears.
  • Tripwires likewise cover unknown nav item types and lookups targeting a name that is neither declared nor platform-provided.

Proving it can fail — four ablations, one per surface

Each leg: anchor asserted to occur exactly once before the edit, both the injected and the removed literal grepped afterwards, a non-empty git diff --stat required, and a trap … EXIT INT TERM restoring from HEAD (the guard was committed first, so this is a real restore point and not the index).

#Mutationpnpm validateguard
1view column categorycategoree_typoexit 0✓ Validation passed (338ms)exit 1
2dataset filter keylast_update_atlast_update_atttexit 0✓ Validation passed (334ms)exit 1
3nav viewName: 'stalled''not_moving'exit 0✓ Validation passed (430ms)exit 1
4joined path duty.frequencyduty.frequenceeexit 0✓ Validation passed (432ms)exit 1

Leg 3 is the one worth reading — the guard prints exactly what makes #14108 nasty:

app duly_app · nav 'nav_stalled' · viewName: "not_moving" — duly_task declares no list view named "not_moving" — the shell SILENTLY falls back to the default view and keeps this entry's authored label.

The label stays "Not moving". The screen looks right and shows the wrong rows.

Observed direction, not the template one: leg 4 reddened two tests, not one — the finding, plus the non-vacuity assertion that the joined path was reached at all. Diagnostics went up, which is the correct reading here: mutating the app's only multi-hop path both produces a finding and removes the evidence that the multi-hop walk runs.

All four restored; tree verified byte-identical to HEAD afterwards (git status --porcelain empty, git diff HEAD empty, all four anchors back at count 1).

Gates

main moved while this was in flight (#54 source defaults, #56 hierarchy-security — both touch objects this guard resolves against). It is merged in at 27d31a0; no field was renamed or removed, only default: flags on select options.

Gates re-run on the final commit 27d31a0, after the merge:

pnpm validate exit 0 ✓ Validation passed (400ms)
pnpm typecheck exit 0 tsc --noEmit
pnpm test exit 0 Test Files 13 passed (13) · Tests 407 passed (407)
pnpm build exit 0 Artifact: dist/objectstack.json (96.2 KB)

Exit codes captured before any pipe. 407 tests = 375 on merged main + 32 added. (The pre-merge reading at cf62af9 was 402 = 370 + 32; #54 and #56 added five.)

No changeset: this repo carries no changeset tooling, and the change is test-only.


Generated by Claude Code

Stopgap pending objectstack#14105 / #14107 / #14108: no field path in the
UI or analytics layer is resolved at author time, and both `validate` and
`build` exit 0 on a dangling one.
Walks dulyViews, dulyDatasets and dulyApps, resolving every reference
against dulyObjects: view columns / filter / sort / grouping / binding
blocks / bulk-action patch keys, dataset base object / include /
dimension / measure fields / filter KEYS, joined paths through a real
lookup, and every nav objectName + viewName. Platform objects resolve
from the spec's own registry; system columns from SystemFieldName.
Includes a synthetic-fixture self-test pinning both directions on all
four surfaces, plus tripwires for unknown field slots, unknown nav item
types, form views and platform-object boundaries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Picks up #54 (source defaults to self) and #56 (hierarchy-security). Both
touch objects this guard resolves against; no field was renamed or removed,
only `default:` flags on select options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 06:13
@os-warren
os-warren merged commit 92438fb into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stopgap: guard dataset field paths — validate and build both pass on a dataset bound to nothing

2 participants

@os-warren@claude
, '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

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects - #57

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings
Sep 1, 2026
Merged

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects#57
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#51

Adds test/metadata-bindings.test.ts — one guard walking dulyViews, dulyDatasets and dulyApps and resolving every reference against dulyObjects. Labelled a stopgap pending objectstack#14105 / #14107 / #14108, written to be deleted rather than maintained, per the test/flow-predicates.test.ts convention.

Scope taken is the widened scope from the PM comment on the card (views + nav + datasets), not the datasets-only scope in the body.

Two things the reviewer should read first

1. No dangling reference exists in shipped metadata. The guard resolves 204 references — 145 view, 34 dataset, 25 nav — and all 204 resolve. So no metadata file is touched by this PR; the only change is the test. That is a real result, not a skipped check: the per-surface counters and the joined-path assertion below are what stop "green" from meaning "the walk did nothing".

2. The card's premise that "nothing has ever checked the five view files or the app nav" is wrong.test/views.test.ts (landed in #49) already resolves the simple view surface and the nav objectName / viewName pair. The card is still real — but the value is in the superset, not in first coverage. The remaining holes it closes:

HoleWhere it was
Datasets not covered at alldatasets.test.ts pins caliber / date-macros / absences, never that a field path names anything real
Dotted paths resolved by nothingviews.test.ts opens its checker with if (!name || name.includes('.')) return — so duty.frequency, the one joined path this app ships, was unchecked everywhere
Hand-copied system-column listviews.test.ts carries business_unit_id (not a platform column) and omits owning_business_unit_id, tenant_id, user_id, deleted_at (which are). This file imports the spec's own SystemFieldName
A view bound to a platform object would false-positiveits bound object had to be in dulyObjects; platform names now resolve from isPlatformProvidedObjectName
No self-test on either file24 synthetic-fixture cases here pin both directions

The overlap is declared in the file header rather than resolved by deleting another card's guard — that is a follow-up, filed as #58. Until then it is benign: this file is the strict superset, so any disagreement reds here first.

What is checked

  • Viewscolumns[] (both the bare-string and { field } spellings), columns[].summary.field, columns[].prefix.field, filter[].field, sort[].field (plus the legacy "field desc" string), grouping.fields[].field, rowColor.field, the searchableFields / filterableFields / hiddenFields / fieldOrder lists, and every binding block — kanban, calendar, gantt, timeline, gallery, tree, map.
  • Datasets — base object, every include path, every dimension and measure field, and every filter key (the column is the key; the walk recurses $and / $or / $not and never mistakes an operator for a column). Also that a joined path's relationship is actually in include — every segment can name something real and still have no join to travel (ADR-0071).
  • Joined paths — through a reference-carrying field to a real field on the target. Hopping through a scalar is reported as its own, more legible failure.
  • Nav — every objectName, every viewName against the views declared on that object (a view of the right name on the wrong object is a finding), filters keys, and the no-viewName case when the object has no default list.

Beyond the card's enumerated list, and flagged because it is an addition: bulkActionDefs with operation: 'update' write fields — the static patch keys and the collected params[].name, which BulkActionDefSchema documents as merged over the patch. Same defect class, one layer down. Only update; on a custom def those are action params, not fields.

Narrowings, stated rather than hidden

  • A hop into a platform object is a boundary, not a resolution.@objectstack/spec exports the platform object name registry but no field lists, so owner.some_typo cannot be judged. The hop is verified, the path is recorded, and a test fails the day one is authored — the hole is loud rather than silent.
  • Field-bearing slots are hand-listed (the spec exports no slot table for view field references, unlike FLOW_NODE_EXPRESSION_PATHS for flow predicates), and the list polices itself: a tripwire scans the metadata for field-ish slots and fails on any the walk cannot read.
  • Form views are not walked. This app declares none; a tripwire fails the day one appears.
  • Tripwires likewise cover unknown nav item types and lookups targeting a name that is neither declared nor platform-provided.

Proving it can fail — four ablations, one per surface

Each leg: anchor asserted to occur exactly once before the edit, both the injected and the removed literal grepped afterwards, a non-empty git diff --stat required, and a trap … EXIT INT TERM restoring from HEAD (the guard was committed first, so this is a real restore point and not the index).

#Mutationpnpm validateguard
1view column categorycategoree_typoexit 0✓ Validation passed (338ms)exit 1
2dataset filter keylast_update_atlast_update_atttexit 0✓ Validation passed (334ms)exit 1
3nav viewName: 'stalled''not_moving'exit 0✓ Validation passed (430ms)exit 1
4joined path duty.frequencyduty.frequenceeexit 0✓ Validation passed (432ms)exit 1

Leg 3 is the one worth reading — the guard prints exactly what makes #14108 nasty:

app duly_app · nav 'nav_stalled' · viewName: "not_moving" — duly_task declares no list view named "not_moving" — the shell SILENTLY falls back to the default view and keeps this entry's authored label.

The label stays "Not moving". The screen looks right and shows the wrong rows.

Observed direction, not the template one: leg 4 reddened two tests, not one — the finding, plus the non-vacuity assertion that the joined path was reached at all. Diagnostics went up, which is the correct reading here: mutating the app's only multi-hop path both produces a finding and removes the evidence that the multi-hop walk runs.

All four restored; tree verified byte-identical to HEAD afterwards (git status --porcelain empty, git diff HEAD empty, all four anchors back at count 1).

Gates

main moved while this was in flight (#54 source defaults, #56 hierarchy-security — both touch objects this guard resolves against). It is merged in at 27d31a0; no field was renamed or removed, only default: flags on select options.

Gates re-run on the final commit 27d31a0, after the merge:

pnpm validate exit 0 ✓ Validation passed (400ms)
pnpm typecheck exit 0 tsc --noEmit
pnpm test exit 0 Test Files 13 passed (13) · Tests 407 passed (407)
pnpm build exit 0 Artifact: dist/objectstack.json (96.2 KB)

Exit codes captured before any pipe. 407 tests = 375 on merged main + 32 added. (The pre-merge reading at cf62af9 was 402 = 370 + 32; #54 and #56 added five.)

No changeset: this repo carries no changeset tooling, and the change is test-only.


Generated by Claude Code

Stopgap pending objectstack#14105 / #14107 / #14108: no field path in the
UI or analytics layer is resolved at author time, and both `validate` and
`build` exit 0 on a dangling one.
Walks dulyViews, dulyDatasets and dulyApps, resolving every reference
against dulyObjects: view columns / filter / sort / grouping / binding
blocks / bulk-action patch keys, dataset base object / include /
dimension / measure fields / filter KEYS, joined paths through a real
lookup, and every nav objectName + viewName. Platform objects resolve
from the spec's own registry; system columns from SystemFieldName.
Includes a synthetic-fixture self-test pinning both directions on all
four surfaces, plus tripwires for unknown field slots, unknown nav item
types, form views and platform-object boundaries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Picks up #54 (source defaults to self) and #56 (hierarchy-security). Both
touch objects this guard resolves against; no field was renamed or removed,
only `default:` flags on select options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 06:13
@os-warren
os-warren merged commit 92438fb into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stopgap: guard dataset field paths — validate and build both pass on a dataset bound to nothing

2 participants

@os-warren@claude
, '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

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects - #57

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings
Sep 1, 2026
Merged

Guard every metadata binding: views, datasets and app nav resolve against dulyObjects#57
os-warren merged 2 commits into
mainfrom
claude/issue-51-metadata-bindings

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#51

Adds test/metadata-bindings.test.ts — one guard walking dulyViews, dulyDatasets and dulyApps and resolving every reference against dulyObjects. Labelled a stopgap pending objectstack#14105 / #14107 / #14108, written to be deleted rather than maintained, per the test/flow-predicates.test.ts convention.

Scope taken is the widened scope from the PM comment on the card (views + nav + datasets), not the datasets-only scope in the body.

Two things the reviewer should read first

1. No dangling reference exists in shipped metadata. The guard resolves 204 references — 145 view, 34 dataset, 25 nav — and all 204 resolve. So no metadata file is touched by this PR; the only change is the test. That is a real result, not a skipped check: the per-surface counters and the joined-path assertion below are what stop "green" from meaning "the walk did nothing".

2. The card's premise that "nothing has ever checked the five view files or the app nav" is wrong.test/views.test.ts (landed in #49) already resolves the simple view surface and the nav objectName / viewName pair. The card is still real — but the value is in the superset, not in first coverage. The remaining holes it closes:

HoleWhere it was
Datasets not covered at alldatasets.test.ts pins caliber / date-macros / absences, never that a field path names anything real
Dotted paths resolved by nothingviews.test.ts opens its checker with if (!name || name.includes('.')) return — so duty.frequency, the one joined path this app ships, was unchecked everywhere
Hand-copied system-column listviews.test.ts carries business_unit_id (not a platform column) and omits owning_business_unit_id, tenant_id, user_id, deleted_at (which are). This file imports the spec's own SystemFieldName
A view bound to a platform object would false-positiveits bound object had to be in dulyObjects; platform names now resolve from isPlatformProvidedObjectName
No self-test on either file24 synthetic-fixture cases here pin both directions

The overlap is declared in the file header rather than resolved by deleting another card's guard — that is a follow-up, filed as #58. Until then it is benign: this file is the strict superset, so any disagreement reds here first.

What is checked

  • Viewscolumns[] (both the bare-string and { field } spellings), columns[].summary.field, columns[].prefix.field, filter[].field, sort[].field (plus the legacy "field desc" string), grouping.fields[].field, rowColor.field, the searchableFields / filterableFields / hiddenFields / fieldOrder lists, and every binding block — kanban, calendar, gantt, timeline, gallery, tree, map.
  • Datasets — base object, every include path, every dimension and measure field, and every filter key (the column is the key; the walk recurses $and / $or / $not and never mistakes an operator for a column). Also that a joined path's relationship is actually in include — every segment can name something real and still have no join to travel (ADR-0071).
  • Joined paths — through a reference-carrying field to a real field on the target. Hopping through a scalar is reported as its own, more legible failure.
  • Nav — every objectName, every viewName against the views declared on that object (a view of the right name on the wrong object is a finding), filters keys, and the no-viewName case when the object has no default list.

Beyond the card's enumerated list, and flagged because it is an addition: bulkActionDefs with operation: 'update' write fields — the static patch keys and the collected params[].name, which BulkActionDefSchema documents as merged over the patch. Same defect class, one layer down. Only update; on a custom def those are action params, not fields.

Narrowings, stated rather than hidden

  • A hop into a platform object is a boundary, not a resolution.@objectstack/spec exports the platform object name registry but no field lists, so owner.some_typo cannot be judged. The hop is verified, the path is recorded, and a test fails the day one is authored — the hole is loud rather than silent.
  • Field-bearing slots are hand-listed (the spec exports no slot table for view field references, unlike FLOW_NODE_EXPRESSION_PATHS for flow predicates), and the list polices itself: a tripwire scans the metadata for field-ish slots and fails on any the walk cannot read.
  • Form views are not walked. This app declares none; a tripwire fails the day one appears.
  • Tripwires likewise cover unknown nav item types and lookups targeting a name that is neither declared nor platform-provided.

Proving it can fail — four ablations, one per surface

Each leg: anchor asserted to occur exactly once before the edit, both the injected and the removed literal grepped afterwards, a non-empty git diff --stat required, and a trap … EXIT INT TERM restoring from HEAD (the guard was committed first, so this is a real restore point and not the index).

#Mutationpnpm validateguard
1view column categorycategoree_typoexit 0✓ Validation passed (338ms)exit 1
2dataset filter keylast_update_atlast_update_atttexit 0✓ Validation passed (334ms)exit 1
3nav viewName: 'stalled''not_moving'exit 0✓ Validation passed (430ms)exit 1
4joined path duty.frequencyduty.frequenceeexit 0✓ Validation passed (432ms)exit 1

Leg 3 is the one worth reading — the guard prints exactly what makes #14108 nasty:

app duly_app · nav 'nav_stalled' · viewName: "not_moving" — duly_task declares no list view named "not_moving" — the shell SILENTLY falls back to the default view and keeps this entry's authored label.

The label stays "Not moving". The screen looks right and shows the wrong rows.

Observed direction, not the template one: leg 4 reddened two tests, not one — the finding, plus the non-vacuity assertion that the joined path was reached at all. Diagnostics went up, which is the correct reading here: mutating the app's only multi-hop path both produces a finding and removes the evidence that the multi-hop walk runs.

All four restored; tree verified byte-identical to HEAD afterwards (git status --porcelain empty, git diff HEAD empty, all four anchors back at count 1).

Gates

main moved while this was in flight (#54 source defaults, #56 hierarchy-security — both touch objects this guard resolves against). It is merged in at 27d31a0; no field was renamed or removed, only default: flags on select options.

Gates re-run on the final commit 27d31a0, after the merge:

pnpm validate exit 0 ✓ Validation passed (400ms)
pnpm typecheck exit 0 tsc --noEmit
pnpm test exit 0 Test Files 13 passed (13) · Tests 407 passed (407)
pnpm build exit 0 Artifact: dist/objectstack.json (96.2 KB)

Exit codes captured before any pipe. 407 tests = 375 on merged main + 32 added. (The pre-merge reading at cf62af9 was 402 = 370 + 32; #54 and #56 added five.)

No changeset: this repo carries no changeset tooling, and the change is test-only.


Generated by Claude Code

Stopgap pending objectstack#14105 / #14107 / #14108: no field path in the
UI or analytics layer is resolved at author time, and both `validate` and
`build` exit 0 on a dangling one.
Walks dulyViews, dulyDatasets and dulyApps, resolving every reference
against dulyObjects: view columns / filter / sort / grouping / binding
blocks / bulk-action patch keys, dataset base object / include /
dimension / measure fields / filter KEYS, joined paths through a real
lookup, and every nav objectName + viewName. Platform objects resolve
from the spec's own registry; system columns from SystemFieldName.
Includes a synthetic-fixture self-test pinning both directions on all
four surfaces, plus tripwires for unknown field slots, unknown nav item
types, form views and platform-object boundaries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Picks up #54 (source defaults to self) and #56 (hierarchy-security). Both
touch objects this guard resolves against; no field was renamed or removed,
only `default:` flags on select options.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 06:13
@os-warren
os-warren merged commit 92438fb into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stopgap: guard dataset field paths — validate and build both pass on a dataset bound to nothing

2 participants

@os-warren@claude