Skip to content

fix(lint): the object publish door refuses a dangling searchableFields / listViews column, measured clean on the shipped corpus (#15495) - #15637

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-15495-object-door-field-existence
Sep 5, 2026
Merged

fix(lint): the object publish door refuses a dangling searchableFields / listViews column, measured clean on the shipped corpus (#15495)#15637
baozhoutao merged 2 commits into
mainfrom
claude/issue-15495-object-door-field-existence

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15495

#15254 crossed the reference-integrity suite onto the object write door for the object's own field-name lists (highlightFields, publicSharing.redactFields). It left the two members that read the other field surfaces an object carries — its ADR-0061 search set and its built-in listViews — still declaring runtimeTypes: ['flow', 'view']. So on the only door a Studio, REST /meta or MCP author has, those two never judged the snapshot that arrived: an object published clean with searchableFields: ['gone_field'], or a list-view column resolving to nothing, and both failed the same silent way downstream (resolveSearchFields filters a stale search entry out without a word, so $search scans a narrower set than declared — or, once every entry is stale, the auto-default set the author never chose; a dangling column renders one field short).

This is a runtimeTypes edit plus its measurement and its pins. No new rule, no new finding class, no severity change.

1. The corpus measurement — done FIRST, before the widening

Both members were run as they would run at the door: the door's own snapshot shape (RuntimeStackContext, per-write, stack.objects only), evaluated differentially exactly as runtime-gate.ts does — context = every sibling object, candidate = context plus the written item, findings = candidate minus base. Population: every *.object.ts under packages/ and examples/, imported and collected by shape, deduplicated by object name.

Source Objects judged
packages/platform-objects 48
examples/app-showcase 24
packages/plugins/* 19
packages/services/* 12
examples/app-crm 6
packages/metadata-core 5
examples/app-todo 1
packages/qa/downstream-contract 1
Total 116

What those 116 objects actually populate, so the zero below is a fact about the rules and not only about the corpus:

  • 40 objects declare listViews, carrying 105 list views and 666 field-naming positions (columns, filter keys, grouping, rowColor, userFilters, filterableFields, hiddenFields, fieldOrder, and the kanban / calendar / gantt / timeline / gallery / map / tree bindings).
  • 5 searchableFields entries across the object-level canonical sets and the list-view narrowings.
Rule Findings at the door shape True dangling False positives Precision Crossed
validateSearchableFields 0 0 0 1.0 yes
validateListViewFieldRefs 0 0 0 1.0 yes
validateSortableFields 0 0 0 1.0 no — see below

Non-vacuity, in the same sweep. A synthetic object with one stale searchableFields entry and one stale list-view column is refused by both, at error:

validateSearchableFields:  searchable-field-unknown  :: objects[116].searchableFields[1]      :: error
validateListViewFieldRefs: list-view-field-unknown   :: objects[116].listViews.all.columns[1] :: error

And through the REAL door, before and after the widening (runRuntimeAuthoringRules({ type: 'object' }) over the same 116 objects, each with its 115 siblings as context):

door errors, total from the two members under measurement
before the crossing 2 0 errors, 0 advisories
after the crossing 2 0 errors, 0 advisories

The two pre-existing errors are not from these members and are not this PR's doing — they are the state of main:

Why neither crossing opens a false-positive channel. Both members resolve only against stack.objects, the one collection every per-write snapshot carries — the #9313 precondition, restated one surface in. Their views[] rungs find no stack.views on an object snapshot and contribute nothing: a rung that is absent, not a rung that reads dead. Every skip they take is snapshot-independent (an object outside the stack, an object with no readable field map, a registry-injected system column), and object inheritance is not implemented in the spec, so the "field the object inherits" class does not exist here.

2. What changed

packages/lint/src/reference-integrity-suite.ts — the member axis only:

Member runtimeTypes before after
validateSearchableFields ['flow', 'view'] ['flow', 'view', 'object']
validateListViewFieldRefs ['flow', 'view'] ['flow', 'view', 'object']
validateObjectFieldRefs ['flow', 'object'] unchanged
validateSortableFields ['flow', 'view'] unchanged

Each crossing records its population and its measurement in the member's own comment, per the #9313 / #15254 discipline. The stale "the ONLY member of this suite that names object" note on validateObjectFieldRefs is corrected to "the FIRST".

The runtimeAuthoringRulesFor('object') roster is UNCHANGEDvalidateReferenceIntegrity was already at this door (#15254 put it there), and which members judge an object snapshot is the suite's own finer axis:

validateFunctionalCompleteness, validateManagedApiMethods, validatePresetComparands,
validateReferenceIntegrity, lintAutonumberFormats, validateSecurityPosture,
validateRuleCompilability, validateRuleSchemaFormats          (before AND after)

Because that pin cannot move, the member surface gets its own, written out by name — the shape the view twin in runtime-gate.view-writes.test.ts uses, so a fourth crossing has to be argued there rather than arriving by drift:

members declaring object before after
validateObjectFieldRefs validateSearchableFields, validateListViewFieldRefs, validateObjectFieldRefs

packages/lint/src/runtime-gate.object-writes.test.ts — the member-surface pin (including an explicit not.toContain('validateSortableFields'), so the uncrossed sibling is a decision recorded at the pin), plus four door controls: a dangling searchableFields entry is REFUSED with searchable-field-unknown at objects.leave_request.searchableFields[1]; a dangling list-view column is REFUSED with list-view-field-unknown at objects.leave_request.listViews.all.columns[1]; a clean object carrying both declarations publishes with rulesRun naming the suite; and a view write still refuses both, unchanged — the crossing is additive.

packages/metadata-protocol/src/protocol-publish-drafts-object-field-refs.test.ts — the same two refusals end to end, through the real publishPackageDrafts (not the gate helper): outcome is not published, failedCount: 1, code: INVALID_METADATA, the receipt naming the rule id and the name-keyed path (#10064), plus the clean object publishing and the #4463 D1 control that a draft save is still never gated.

Not crossed: validateSortableFields. It measured equally clean over the same corpus, and leaving it out does mean an object's list-view columns gate while that same list view's sort does not. That is a deliberate stop, not an oversight: a crossing is an adjudicated act with its own non-vacuity control, and this card was scoped to the two axes its title names. The pin above is where that decision is recorded and where the third crossing would have to be argued.

3. Verification

Every exit code captured before any pipe.

Command Exit Verdict line
pnpm --filter '@objectstack/lint^...' build, '@objectstack/metadata-protocol^...' build 0 dependency closures built before anything was judged
pnpm --filter @objectstack/lint test 0 Test Files 95 passed (95) · Tests 2983 passed (2983)
pnpm --filter @objectstack/lint typecheck 0 check:test-typecheck: OK — @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.json
pnpm --filter @objectstack/metadata-protocol test 0 Test Files 163 passed, 2 skipped (165) · Tests 2387 passed, 10 skipped (2397)
pnpm --filter @objectstack/metadata-protocol typecheck 0 tsc --noEmit, clean (its tsconfig.json includes src/**/*, so the edited test file is in the program)
pnpm lint (eslint . --no-inline-config, whole repo) 0 clean, no narrowing claimed
node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack 0 53 commands derived at this HEAD from the 4 changed paths
51 of those 53 0 all green
pnpm check:dual-build-cjs-loads 3 PREREQUISITE NOT MET — this gate reads built output · NOT MEASURED (needs a whole-monorepo pnpm build; CI does it)
pnpm check:type-check-debt 3 Build the closure first · NOT MEASURED, same reason
pnpm check:pm-dispatch-gates 0 dispatch-gates self-test: 1445 cases pass. (no case edited)

Seven further families matched by path take a value from the workflow ($RUNNER_TEMP, matrix.shard) and have no value outside a CI run — NOT MEASURED here, by construction, and named as such by the derivation itself.

The gate union above was run at b8d8c7c18, the branch HEAD this PR is opened on (git rev-parse --short HEAD); the tree has not moved since.

The measurement harness was a throwaway outside the repo and was never committed — git status is clean.

4. Migration

A publish that used to succeed can now be refused, HTTP 422 / INVALID_METADATA. The receipt names the rule id and the offending path, name-keyed on the wire: objects.proj_task.searchableFields[1], objects.proj_task.listViews.all.columns[1]. Fix by rewriting the entry to the field's current API name (after a Studio label edit the derived name is the one to use), or by dropping it. os validate / os build / os lint already reported these findings at the same severity, so a code-authored stack can be repaired before it reaches a publish. Changeset: .changeset/object-door-searchable-listview-refusal.md (@objectstack/lint minor, @objectstack/metadata-protocol minor, with the Migration section).

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

…s / listViews column, measured clean on the shipped corpus (#15495)

#15254 crossed the reference-integrity suite onto the object write door for the
object's own field-name lists (`highlightFields`, `publicSharing.redactFields`).
The two members that read the OTHER field surfaces an object carries — its
ADR-0061 search set and its built-in `listViews` — kept
`runtimeTypes: ['flow', 'view']`, so on the only door a Studio, REST `/meta` or
MCP author has they never judged the snapshot that arrived there. An object
published clean with `searchableFields: ['gone_field']` or a list-view column
resolving to nothing, and both fail silently downstream.

Adds `object` to `validateSearchableFields` and `validateListViewFieldRefs` in
the reference-integrity suite. No new rule, no new finding class, no severity
change — the two members now reach the door where the author is.

Measured before crossing at the door's own snapshot shape (`RuntimeStackContext`,
per-write, `stack.objects` only, evaluated differentially as `runtime-gate.ts`
does) over every shipped object definition under `packages/` and `examples/`:
116 objects, 105 built-in list views on 40 objects, 666 list-view field-naming
positions and 5 `searchableFields` entries judged — 0 findings for both members,
precision 1.0, against synthetic probes that ARE refused.

Pins the member surface of the object door by name (the `view` twin's shape),
adds the four door controls, and adds the end-to-end refusals through the real
`publishPackageDrafts`. `validateSortableFields` measured equally clean and is
deliberately NOT crossed — that is its own adjudication.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@github-actions github-actions Bot added the size/m label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json d30ccb9bd963942d33072443f680a32664087346packageMentionDocs.

Which tree this was computed on

This run read content/docs from 24159cc161d8769927cf67152f3ab463336645ab — the merge of head b8d8c7c185c9d2256d8b86221c1a191f60e23e8b into base d30ccb9bd963942d33072443f680a32664087346, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 24159cc161d8769927cf67152f3ab463336645ab && git checkout 24159cc161d8769927cf67152f3ab463336645ab
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d30ccb9bd963942d33072443f680a32664087346 b8d8c7c185c9d2256d8b86221c1a191f60e23e8b && git checkout -B drift-repro d30ccb9bd963942d33072443f680a32664087346 && git merge --no-ff b8d8c7c185c9d2256d8b86221c1a191f60e23e8b

node scripts/docs-audit/affected-docs.mjs --json d30ccb9bd963942d33072443f680a32664087346

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 5, 2026
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

Reviewed against the three-dot diff origin/main...claude/issue-15495-object-door-field-existence (head b8d8c7c18, a merge of main over 475d84ae5; 4 files, +314/−10), not the self-report. git merge-tree against the armed #15635 (#15552, 19 packages/lint files): 0 conflicts — the two dispatches stayed disjoint as instructed.

Dev's verification quoted (on b8d8c7c18): corpus measurement at the door's own shape and differential over 116 shipped objects (40 with listViews, 105 list views, 666 field-naming positions, 5 searchableFields entries) — 0 findings / precision 1.0 for both members, with synthetic stale entries refused as the non-vacuity leg; through the real door BEFORE/AFTER: 2 total errors both times, 0 from the two members (the 2 are main's own: sys_email.highlightFields[1] and dc_account OWD); pnpm --filter @objectstack/lint test 95 passed / 2983 passed + check:test-typecheck: OK; @objectstack/metadata-protocol test 163 passed, 2 skipped / 2387 passed; whole-repo lint clean; derived family 53 commands, 51 green, 2 NOT MEASURED (PREREQUISITE NOT MET, whole-repo build), 1445 cases pass.

Ruling on the open question (sortable): A — a follow-up card with its own non-vacuity control and door test; ⛔ not here. The dev's pin records the decision where the third crossing must be argued. Out-of-scope finding filed by the dev: #15629 (sys_email.highlightFields[1] names to, the field is to_addresses — a true dangling reference on a shipped platform object, refused at the door today).

Flipping ready + enabling auto-merge. Fixes #15495.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 5, 2026 01:04
@baozhoutao
baozhoutao enabled auto-merge September 5, 2026 01:04
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit b4b37e5 Sep 5, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-15495-object-door-field-existence branch September 5, 2026 01:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The object write door still runs no field-existence rule for searchableFields or listViews — the same asymmetry #15254 closed one key over

2 participants