Repository navigation
fix(lint): security-role-word visits objects[].fieldGroups[] — the heading above the fields it already judged - #18850
Conversation
The ADR-0090 D3 vocabulary freeze visited seven declaration surfaces and not the field-group header that sits directly above the fields it already polices, so on one record page a field labelled "Role Of Record" was refused while the group header "Account & Role" walked through. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Behavioural pins for the new surface (heading beside the field it heads, the `key` spelling in the fix-it, the silent shape, the system-object exemption), the meta-guard rows that make an undeclared read on it fail before review, and the breaking changeset with its ADR-0087 disposition. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…eldgroups-role-word
📓 Docs Drift Check1 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 — 4 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 fbc20eb416ff90715f981a3ab7a86f7b0b8b21b8 && git checkout fbc20eb416ff90715f981a3ab7a86f7b0b8b21b8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 631dcbd4b93c3f86d02080bc129a611f0619ed23 d510921fc820431cc97029efc20fc331c02eebec && git checkout -B drift-repro 631dcbd4b93c3f86d02080bc129a611f0619ed23 && git merge --no-ff d510921fc820431cc97029efc20fc331c02eebec
node scripts/docs-audit/affected-docs.mjs --json 631dcbd4b93c3f86d02080bc129a611f0619ed23 |
Fixes #18306
validateSecurityRoleWord— the ADR-0090 D3 vocabulary freeze — visited seven declaration surfaces and not the field-group heading that renders directly above the fields it was already judging.objects[].fieldGroups[]is now visited, both halves of it.Clause-②: no (narrowing) — the rule refuses more than it did. No key is added to any published payload and no public surface grows, so
lanes/spec.md's widening test is not met; narrowing is still a semantic-surface change, which is why it is declared rather than shipped silently.The card did not pre-judge the answer, so this is the reading that decided it
The card offered two acceptable endings: add the visit, or determine that field groups are presentation-only and record that reasoning in the docblock. The evidence went one way.
"Presentation-only" cannot be the discriminator, because the rule already refuses the word in labels that carry no permission semantics at all. Measured on this tree, before any change: a stack whose field is labelled
Role Of Recordproduceserror security-role-word @ objects[0].fields.duty.label.object.labelandaction.labelare refused on the same footing. If presentation were the test, three of the seven surfaces would not be scanned.What the ban actually says. ADR-0090 D3: "'role' is a reserved-forbidden word in identifiers, UI copy, and documentation, enforced by lint", and the ADR's own rule table reads "The word
rolein identifiers/labels → error". A field group declares both halves:keyis an identifier (Field.groupassigns membership by it, and a layout section'sgroupinherits the whole group by it, ADR-0085 §5), andlabelis the section header an admin reads. It is inside the ban by the ban's own terms.What excludes pages, views and components is a different fact, and it survives untouched:
rolethere is the HTML/ARIA attribute — a machine word with a fixed foreign meaning, not a word the author picked. No such collision exists on a group header.So the gap was the #7220 shape, one grain finer than the one this function's own split exists to avoid. On a single record page a field labelled
Role Of Recordwas refused while the group heading directly above it,Account & Role, walked through: the author renames the field and the heading keeps the word.keyis visited besidelabelfor the same reason the other six surfaces visit name beside label. Refusingfields: { role_data }while admittingfieldGroups: [{ key: 'role_data' }]is that same shape again. This is inside the surface the card named (objects[].fieldGroups[]), not a widening to a new one.Not widened, deliberately:
listViews,recordTypesand the other label-bearing surfaces. They are unmeasured here, not judged; each needs the reading this one got before it is in or out. The docblock says so, so the next reader need not re-derive it.LIT control — silent before, reported after
Both runs are the same harness over the same fixtures, on this branch, either side of the rule edit. The findings are the whole output, not a summary.
fieldGroups: [{ key: 'assignment', label: 'Account & Role' }]error security-role-word @ objects[0].fieldGroups[0].labelfieldGroups: [{ key: 'role_info', label: 'Assignment' }]error security-role-word @ objects[0].fieldGroups[0].keyAfter, verbatim:
The fix-it says
key, notname, becauseObjectFieldGroupSchemaspells the identifierkeyand declaresnameas a rejected alias — a message namingnamewould point the author at a key the schema refuses.DARK control — everything else reads 0 change
Diffing the two full harness runs, the four lines quoted above are the only lines that differ. Specifically:
diffover that block reports 0 linescontact/work/status/notes/Payroll — Controlled Rollout)sys_member.rolesystem-object exemptionsys_objectThe last row is the one the placement had to earn: the visit sits inside the
isSystemObjectguard, so a platform object whose fields are exempt cannot have a gated heading above them.Corpus — how many existing declarations redden in this repository
Zero. Measured on
objectstack-ai/objectstackatd510921fc8, with the same harness on either side of the edit:examples/app-showcase(22 objects)examples/app-crm(6 objects)examples/app-todo(1 object)examples/app-multi-package(2 objects)Cross-checked two further ways: every file in the tree that declares a
fieldGroups:array (20 of them, fixtures included) was scanned for a reserved word in akeyorlabel— no hits; andcheck:doc-security-postureis green over 27ObjectSchema.createexamples in 227 marked blocks across 239 prose files. So no declaration data is touched by this PR, and none needs to be.Changeset level, and why
minor, on@objectstack/lint, carrying a BREAKING banner.It is breaking in the accept-set sense — a declaration that passes today can fail tomorrow — and
majoris refused outright bycheck-changeset-no-majorduring the launch window, where breaking-ness is carried by the banner plus the ADR-0087 disposition rather than by the bump.patchis wrong for the same reason it would be wrong for any accept-set narrowing: the level would say a consumer can upgrade without reading anything.skip-changesetis wrong because@objectstack/lintis published (17.4.0,files: ["dist", …]) and its shipped behaviour moves.ADR-0087 disposition:
not-required (no-migration-prescription). No key, symbol, enum member or stored value moves — a stored metadata row is structurally identical before and after — soobjectstack migrate metahas nothing to rewrite, and there is no FROM-TO mapping to state because there is no single replacement: the author picks a domain word, and the refusal names the platform vocabulary at the exact path.pnpm check:adr-0087-registrationreads the disposition and the clause-② arm and passes.Verification
Run at
d510921fc8(after the last commit, which is a clean merge oforigin/main).pnpm --filter @objectstack/lint test— 104 files, 3886 passed, 5 skipped.pnpm --filter @objectstack/lint typecheck— green;check:test-typecheckholds its existing ledger (2 files / 6 errors / 2 pinned signatures), unchanged.pnpm lint(repo-wideeslint . --no-inline-config) — exit 0 over the whole repository, no narrowing claimed.scripts/pm/dispatch-gates.mjs --commands, 58 commands, re-derived after the merge): 55 green, includingcheck:adr-0087-registration,check:changeset-no-major,check:empty-changeset,check:nul-bytes,check:cross-package-test-inputs,check:test-source-alias,check:type-check-coverage,check:docs-transcript-drift,check:doc-security-posture,check:doc-authoring,check:published-files.PREREQUISITE NOT MET, which is neither a pass nor a finding) because they read a whole-repo build:check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt.Build CoreandTypeScript Type Checkown those runs.packages/cli's spawn-based tier: the CLI cannot load its own command table without a repo build (probed directly: exit 2,MODULE_NOT_FOUNDon@objectstack/types/dist/index.mjs, on a config carrying neither a flow nor a field group).validate-build-gate-parity.test.tsdid run in-process: 21 passed. This tier is CI's; the diff touches no integration-tier file and no spawn entry point.New pins
The rule's own test surface grew rather than the behaviour being left to prose:
keyspelling in the fix-it, asserted positively and negatively;Payrollnear-miss);#5017meta-guard gains agroupreceiver row bound toObjectSchema.fieldGroups[], so readinggroup.name— the alias the schema rejects — fails before review rather than after; andfieldGroupsjoinsobj's declared-key list.Acceptance notes
Noted, not filed:
packages/lint/src/authoring-rules.tsenumerates this rule's collections twice, in a comment and insurfaceReason, as "six collections (objects, fields, actions, permission sets, positions, apps — plus books)". Field groups are a sub-surface ofobjects, which is already carried and mapped, so the [finding] The fourviews[]visibility-predicate rules are CLI-only — a Studio/REST/MCPviewwrite bypasses all of them; if they move to runtime-publish, they must move together #7220 wall argument those two passages make is unchanged and still correct. The enumeration is now incomplete, though. It is outside this PR's declared file surface and is a prose accuracy matter, not a defect: no gate reads it and no verdict depends on it. Whoever next moves that registry entry — thepositions/appscrossing it describes — is the natural carrier.Generated by Claude Code