Found while implementing #15494 (the indexObjectGraph seam guard); filed as a finding only, not claimed.
#15494 repairs ONE seam. The measurement that verified it turned up twelve more rules that crash on the identical input, through five more unguarded readers of stack.objects — including three inside the reference-integrity suite itself, so validateReferenceIntegrity({ objects: [null] }) still throws after #15494 lands.
Measured
On b7bf0bd9b (#15494's branch, seam guard applied), driving the whole AUTHORING_RULES table over { objects: [null, validObject] } — 42 rules, 13 throw:
| rule |
throws at |
message |
validateStackExpressions |
packages/lint/src/validate-expressions.ts:134 |
Cannot read properties of null (reading 'name') |
validateListViewMode |
packages/lint/src/validate-list-view-mode.ts:149 |
Cannot read properties of null (reading 'name') |
validateWidgetBindings |
packages/lint/src/validate-widget-bindings.ts:465 |
Cannot read properties of null (reading 'name') |
validateFilterTokens |
packages/lint/src/filter-walk.ts:155 |
Cannot read properties of null (reading 'name') |
validatePresetComparands |
packages/lint/src/filter-walk.ts:155 |
Cannot read properties of null (reading 'name') |
validateEmptyCombinators |
packages/lint/src/filter-walk.ts:155 |
Cannot read properties of null (reading 'name') |
validateReferenceIntegrity |
packages/lint/src/validate-object-references.ts:142 |
Cannot read properties of null (reading 'name') |
validateRecordTitle |
packages/lint/src/validate-record-title.ts:75 |
Cannot read properties of null (reading 'name') |
validateFormLayout |
packages/lint/src/validate-form-layout.ts:126 |
Cannot read properties of null (reading 'name') |
lintAutonumberFormats |
packages/lint/src/lint-autonumber-formats.ts:56 |
Cannot read properties of null (reading 'name') |
lintViewRefs |
packages/lint/src/lint-view-refs.ts:204 |
Cannot read properties of null (reading 'name') |
validateOrgAxisRedLines |
packages/lint/src/validate-org-axis-red-lines.ts:189 |
Cannot read properties of null (reading 'tenancy') |
validateSharingRuleEnforceability |
packages/lint/src/validate-sharing-rule-enforceability.ts:432 |
Cannot read properties of null (reading 'name') |
Control leg: the same 42 rules over { objects: [validObject] } — zero throws. Every row above is attributable to the junk entry alone, not to the fixture.
Three of those rows are members of the reference-integrity suite reached through two more SHARED indexers, so each row is worth more than one rule:
indexObjectSearchTargets (packages/lint/src/validate-searchable-fields.ts:287) — validateSearchableFields and validateSortableFields
indexObjectFields (packages/lint/src/validate-page-field-bindings.ts:358) — validatePageFieldBindings and validateReactPageProps
validateObjectReferences's own loop (packages/lint/src/validate-object-references.ts:142)
validateWidgetBindings is the fourth indexObjectGraph caller and #15494 does NOT fix it: it dereferences the entry at its own line 465, in the aggregate-coherence pass that runs BEFORE the seam. That file was held by an in-flight change while #15494 was implemented, which is the other reason it was left.
Why it matters
Same argument as #15494, one layer wider. These rules are pure (stack) = Finding[] and run on the raw lint path as well as the parsed one; at the runtime publish gate they are called inside the gate rather than behind a try/catch of their own. A throw there is an exception on a write path, not a skipped finding — and os lint / os validate / os compile die on the first one rather than reporting the stack.
A null member is not exotic: an empty list item in a YAML source deserialises to exactly that, as does a partial editor write.
Suggested shape
The same guard as #15494, at each reader, with the same verdict: DROP the non-record entry, do not report it. That decision was measured on this table — 28 of the 42 rules already judge the input in silence, and not one rule reports the junk entry (validateSecurityPosture emits security-owd-unset, but its path moves objects[1].sharingModel to objects[0].sharingModel when the junk entry is removed, so it is a finding about the VALID object).
Worth considering as one edit rather than thirteen: the package carries ~50 hand-copied asArray helpers, of which roughly a dozen already spell the defensive filter(isRec) and the rest do not. A single shared collection reader would close the class and keep it closed; the per-file guard closes it once.
Not this card
#15494 owns indexObjectGraph (packages/lint/src/object-graph.ts) and the publish-probe half in packages/metadata-protocol/src/build-probes.ts. Nothing above is in that PR.
Generated by Claude Code
Found while implementing #15494 (the
indexObjectGraphseam guard); filed as a finding only, not claimed.#15494 repairs ONE seam. The measurement that verified it turned up twelve more rules that crash on the identical input, through five more unguarded readers of
stack.objects— including three inside the reference-integrity suite itself, sovalidateReferenceIntegrity({ objects: [null] })still throws after #15494 lands.Measured
On
b7bf0bd9b(#15494's branch, seam guard applied), driving the wholeAUTHORING_RULEStable over{ objects: [null, validObject] }— 42 rules, 13 throw:validateStackExpressionspackages/lint/src/validate-expressions.ts:134validateListViewModepackages/lint/src/validate-list-view-mode.ts:149validateWidgetBindingspackages/lint/src/validate-widget-bindings.ts:465validateFilterTokenspackages/lint/src/filter-walk.ts:155validatePresetComparandspackages/lint/src/filter-walk.ts:155validateEmptyCombinatorspackages/lint/src/filter-walk.ts:155validateReferenceIntegritypackages/lint/src/validate-object-references.ts:142validateRecordTitlepackages/lint/src/validate-record-title.ts:75validateFormLayoutpackages/lint/src/validate-form-layout.ts:126lintAutonumberFormatspackages/lint/src/lint-autonumber-formats.ts:56lintViewRefspackages/lint/src/lint-view-refs.ts:204validateOrgAxisRedLinespackages/lint/src/validate-org-axis-red-lines.ts:189validateSharingRuleEnforceabilitypackages/lint/src/validate-sharing-rule-enforceability.ts:432Control leg: the same 42 rules over
{ objects: [validObject] }— zero throws. Every row above is attributable to the junk entry alone, not to the fixture.Three of those rows are members of the reference-integrity suite reached through two more SHARED indexers, so each row is worth more than one rule:
indexObjectSearchTargets(packages/lint/src/validate-searchable-fields.ts:287) —validateSearchableFieldsandvalidateSortableFieldsindexObjectFields(packages/lint/src/validate-page-field-bindings.ts:358) —validatePageFieldBindingsandvalidateReactPagePropsvalidateObjectReferences's own loop (packages/lint/src/validate-object-references.ts:142)validateWidgetBindingsis the fourthindexObjectGraphcaller and #15494 does NOT fix it: it dereferences the entry at its own line 465, in the aggregate-coherence pass that runs BEFORE the seam. That file was held by an in-flight change while #15494 was implemented, which is the other reason it was left.Why it matters
Same argument as #15494, one layer wider. These rules are pure
(stack) =Finding[]and run on the rawlintpath as well as the parsed one; at the runtime publish gate they are called inside the gate rather than behind a try/catch of their own. A throw there is an exception on a write path, not a skipped finding — andos lint/os validate/os compiledie on the first one rather than reporting the stack.A
nullmember is not exotic: an empty list item in a YAML source deserialises to exactly that, as does a partial editor write.Suggested shape
The same guard as #15494, at each reader, with the same verdict: DROP the non-record entry, do not report it. That decision was measured on this table — 28 of the 42 rules already judge the input in silence, and not one rule reports the junk entry (
validateSecurityPostureemitssecurity-owd-unset, but itspathmovesobjects[1].sharingModeltoobjects[0].sharingModelwhen the junk entry is removed, so it is a finding about the VALID object).Worth considering as one edit rather than thirteen: the package carries ~50 hand-copied
asArrayhelpers, of which roughly a dozen already spell the defensivefilter(isRec)and the rest do not. A single shared collection reader would close the class and keep it closed; the per-file guard closes it once.Not this card
#15494 owns
indexObjectGraph(packages/lint/src/object-graph.ts) and the publish-probe half inpackages/metadata-protocol/src/build-probes.ts. Nothing above is in that PR.Generated by Claude Code