fix(lint): the object publish door refuses a dangling searchableFields / listViews column, measured clean on the shipped corpus (#15495) - #15637
Conversation
…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
…ject-door-field-existence
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not 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
Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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 |
|
ACCEPT — PM seat Reviewed against the three-dot diff
Dev's verification quoted (on 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 ( Flipping ready + enabling auto-merge. Fixes #15495. Generated by Claude Code |
Fixes #15495
#15254crossed 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-inlistViews— still declaringruntimeTypes: ['flow', 'view']. So on the only door a Studio, REST/metaor MCP author has, those two never judged the snapshot that arrived: an object published clean withsearchableFields: ['gone_field'], or a list-view column resolving to nothing, and both failed the same silent way downstream (resolveSearchFieldsfilters a stale search entry out without a word, so$searchscans 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
runtimeTypesedit 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.objectsonly), evaluated differentially exactly asruntime-gate.tsdoes — context = every sibling object, candidate = context plus the written item, findings = candidate minus base. Population: every*.object.tsunderpackages/andexamples/, imported and collected by shape, deduplicated by object name.packages/platform-objectsexamples/app-showcasepackages/plugins/*packages/services/*examples/app-crmpackages/metadata-coreexamples/app-todopackages/qa/downstream-contractWhat those 116 objects actually populate, so the zero below is a fact about the rules and not only about the corpus:
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).searchableFieldsentries across the object-level canonical sets and the list-view narrowings.validateSearchableFieldsvalidateListViewFieldRefsvalidateSortableFieldsNon-vacuity, in the same sweep. A synthetic object with one stale
searchableFieldsentry and one stale list-view column is refused by both, aterror: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):The two pre-existing errors are not from these members and are not this PR's doing — they are the state of
main:sys_email—object-field-ref-unknownatobjects.sys_email.highlightFields[1]: the entry saysto, the field isto_addresses. A true dangling reference in a shipped platform object, reported and not fixed here (packages/platform-objectsis outside this PR's scope). Filed as sys_email.highlightFields namesto, but the field isto_addresses— a dangling reference on a shipped platform object #15629.dc_account—security-owd-unset: a minimal QA contract fixture that declares nosharingModel. Plausibly intentional; recorded in sys_email.highlightFields namesto, but the field isto_addresses— a dangling reference on a shipped platform object #15629 rather than filed separately.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. Theirviews[]rungs find nostack.viewson 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:runtimeTypesbeforevalidateSearchableFields['flow', 'view']['flow', 'view', 'object']validateListViewFieldRefs['flow', 'view']['flow', 'view', 'object']validateObjectFieldRefs['flow', 'object']validateSortableFields['flow', 'view']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 onvalidateObjectFieldRefsis corrected to "the FIRST".The
runtimeAuthoringRulesFor('object')roster is UNCHANGED —validateReferenceIntegritywas already at this door (#15254 put it there), and which members judge an object snapshot is the suite's own finer axis:Because that pin cannot move, the member surface gets its own, written out by name — the shape the
viewtwin inruntime-gate.view-writes.test.tsuses, so a fourth crossing has to be argued there rather than arriving by drift:objectvalidateObjectFieldRefsvalidateSearchableFields,validateListViewFieldRefs,validateObjectFieldRefspackages/lint/src/runtime-gate.object-writes.test.ts— the member-surface pin (including an explicitnot.toContain('validateSortableFields'), so the uncrossed sibling is a decision recorded at the pin), plus four door controls: a danglingsearchableFieldsentry is REFUSED withsearchable-field-unknownatobjects.leave_request.searchableFields[1]; a dangling list-view column is REFUSED withlist-view-field-unknownatobjects.leave_request.listViews.all.columns[1]; a clean object carrying both declarations publishes withrulesRunnaming the suite; and aviewwrite 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 realpublishPackageDrafts(not the gate helper):outcomeis notpublished,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-viewcolumnsgate while that same list view'ssortdoes 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.
pnpm --filter '@objectstack/lint^...' build,'@objectstack/metadata-protocol^...' buildpnpm --filter @objectstack/lint testTest Files 95 passed (95)·Tests 2983 passed (2983)pnpm --filter @objectstack/lint typecheckcheck:test-typecheck: OK — @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.jsonpnpm --filter @objectstack/metadata-protocol testTest Files 163 passed, 2 skipped (165)·Tests 2387 passed, 10 skipped (2397)pnpm --filter @objectstack/metadata-protocol typechecktsc --noEmit, clean (itstsconfig.jsonincludessrc/**/*, so the edited test file is in the program)pnpm lint(eslint . --no-inline-config, whole repo)node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstackpnpm check:dual-build-cjs-loadsPREREQUISITE NOT MET — this gate reads built output· NOT MEASURED (needs a whole-monorepopnpm build; CI does it)pnpm check:type-check-debtBuild the closure first· NOT MEASURED, same reasonpnpm check:pm-dispatch-gatesdispatch-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 statusis 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 lintalready 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/lintminor,@objectstack/metadata-protocolminor, with the Migration section).🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code