Commit f2044ef
fix(lint): security-role-word visits objects[].fieldGroups[] — the heading above the fields it already judged (#18850)
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 Record` produces `error security-role-word @
objects[0].fields.duty.label`. `object.label` and `action.label` are
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 `role`
in identifiers/labels → error"*. A field group declares both halves:
`key` is an identifier (`Field.group` assigns membership by it, and a
layout section's `group` inherits the whole group by it, ADR-0085 §5),
and `label` is 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: `role` there 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 Record` was refused while the group heading directly
above it, `Account & Role`, walked through: the author renames the field
and the heading keeps the word.
`key` is visited beside `label` for the same reason the other six
surfaces visit name beside label. Refusing `fields: { role_data }` while
admitting `fieldGroups: [{ 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`, `recordTypes` and 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.
| fixture | before | after |
|:--|:--|:--|
| `fieldGroups: [{ key: 'assignment', label: 'Account & Role' }]` | 0
findings | 1 — `error security-role-word @
objects[0].fieldGroups[0].label` |
| `fieldGroups: [{ key: 'role_info', label: 'Assignment' }]` | 0
findings | 1 — `error security-role-word @
objects[0].fieldGroups[0].key` |
After, verbatim:
```
error security-role-word @ objects[0].fieldGroups[0].label (field group "showcase_contact.assignment")
field group label "Account & Role" uses the reserved word "role" (ADR-0090 D3).
error security-role-word @ objects[0].fieldGroups[0].key (field group "showcase_contact.role_info")
field group key "role_info" uses the reserved word "role" — the platform vocabulary is
permission_set (capability), position (distribution), business_unit (hierarchy) (ADR-0090 D3).
```
The fix-it says `key`, not `name`, because `ObjectFieldGroupSchema`
spells the identifier `key` and declares `name` as a rejected alias — a
message naming `name` would 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:
| dark fixture | before | after |
|:--|:--|:--|
| the seven surfaces visited today (object name, field name, field
label, action name, action label, permission set, position label, app
name, book label) | 9 findings | 9 findings, byte-identical — `diff`
over that block reports 0 lines |
| a field group with no reserved word (`contact` / `work` / `status` /
`notes` / `Payroll — Controlled Rollout`) | 0 | 0 |
| the `sys_member.role` system-object exemption | 0 | 0 |
| a field group carrying the word on a `sys_` object | 0 | 0 |
The last row is the one the placement had to earn: the visit sits inside
the `isSystemObject` guard, 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/objectstack` at `d510921fc8`, with
the same harness on either side of the edit:
| corpus | field-group entries | findings before | findings after |
|:--|--:|--:|--:|
| `examples/app-showcase` (22 objects) | 6 | 0 | 0 |
| `examples/app-crm` (6 objects) | 0 | 0 | 0 |
| `examples/app-todo` (1 object) | 0 | 0 | 0 |
| `examples/app-multi-package` (2 objects) | 0 | 0 | 0 |
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 a `key` or `label` — no hits; and
`check:doc-security-posture` is green over 27 `ObjectSchema.create`
examples 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 `major` is refused outright by
`check-changeset-no-major` during the launch window, where breaking-ness
is carried by the banner plus the ADR-0087 disposition rather than by
the bump. `patch` is 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-changeset` is wrong because `@objectstack/lint`
is 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 — so `objectstack migrate
meta` has 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-registration` reads the disposition and the clause-② arm
and passes.
## Verification
Run at `d510921fc8` (after the last commit, which is a clean merge of
`origin/main`).
- `pnpm --filter @objectstack/lint test` — **104 files, 3886 passed, 5
skipped**.
- `pnpm --filter @objectstack/lint typecheck` — green;
`check:test-typecheck` holds its existing ledger (2 files / 6 errors / 2
pinned signatures), unchanged.
- `pnpm lint` (repo-wide `eslint . --no-inline-config`) — **exit 0 over
the whole repository**, no narrowing claimed.
- Derived gate families (`scripts/pm/dispatch-gates.mjs --commands`, 58
commands, re-derived after the merge): **55 green**, including
`check: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`.
- **NOT MEASURED, declared to CI** — three families exit 3
(`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 Core` and
`TypeScript Type Check` own those runs.
- **NOT MEASURED** — `packages/cli`'s spawn-based tier: the CLI cannot
load its own command table without a repo build (probed directly: exit
2, `MODULE_NOT_FOUND` on `@objectstack/types/dist/index.mjs`, on a
config carrying neither a flow nor a field group).
`validate-build-gate-parity.test.ts` did 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:
- the heading and the field it heads are asserted **in one stack**, in
path order — apart, they are two passing tests that say nothing about
what was wrong;
- the `key` spelling in the fix-it, asserted positively and negatively;
- the silent shape (the showcase's four real groups plus a `Payroll`
near-miss);
- the system-object exemption on a field group;
- the `#5017` meta-guard gains a `group` receiver row bound to
`ObjectSchema.fieldGroups[]`, so reading `group.name` — the alias the
schema rejects — fails before review rather than after; and
`fieldGroups` joins `obj`'s declared-key list.
## Acceptance notes
Noted, not filed:
- `packages/lint/src/authoring-rules.ts` enumerates this rule's
collections twice, in a comment and in `surfaceReason`, as *"six
collections (objects, fields, actions, permission sets, positions, apps
— plus books)"*. Field groups are a sub-surface of `objects`, which is
already carried and mapped, so the #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 — the
`positions`/`apps` crossing it describes — is the natural carrier.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a2cf0f1 commit f2044ef
3 files changed
Lines changed: 218 additions & 5 deletions
File tree
- .changeset
- packages/lint/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
546 | 546 | | |
547 | 547 | | |
548 | 548 | | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
549 | 632 | | |
550 | 633 | | |
551 | 634 | | |
| |||
1262 | 1345 | | |
1263 | 1346 | | |
1264 | 1347 | | |
1265 | | - | |
| 1348 | + | |
| 1349 | + | |
| 1350 | + | |
| 1351 | + | |
1266 | 1352 | | |
1267 | 1353 | | |
1268 | 1354 | | |
| |||
1296 | 1382 | | |
1297 | 1383 | | |
1298 | 1384 | | |
| 1385 | + | |
| 1386 | + | |
| 1387 | + | |
| 1388 | + | |
| 1389 | + | |
| 1390 | + | |
| 1391 | + | |
| 1392 | + | |
| 1393 | + | |
| 1394 | + | |
| 1395 | + | |
1299 | 1396 | | |
1300 | 1397 | | |
1301 | 1398 | | |
| |||
1528 | 1625 | | |
1529 | 1626 | | |
1530 | 1627 | | |
| 1628 | + | |
| 1629 | + | |
| 1630 | + | |
| 1631 | + | |
| 1632 | + | |
| 1633 | + | |
| 1634 | + | |
| 1635 | + | |
| 1636 | + | |
| 1637 | + | |
| 1638 | + | |
| 1639 | + | |
| 1640 | + | |
1531 | 1641 | | |
1532 | 1642 | | |
1533 | 1643 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1045 | 1045 | | |
1046 | 1046 | | |
1047 | 1047 | | |
1048 | | - | |
| 1048 | + | |
| 1049 | + | |
1049 | 1050 | | |
1050 | 1051 | | |
1051 | 1052 | | |
1052 | 1053 | | |
1053 | 1054 | | |
1054 | 1055 | | |
| 1056 | + | |
| 1057 | + | |
| 1058 | + | |
| 1059 | + | |
| 1060 | + | |
| 1061 | + | |
| 1062 | + | |
| 1063 | + | |
| 1064 | + | |
| 1065 | + | |
| 1066 | + | |
| 1067 | + | |
| 1068 | + | |
| 1069 | + | |
| 1070 | + | |
| 1071 | + | |
| 1072 | + | |
| 1073 | + | |
| 1074 | + | |
| 1075 | + | |
| 1076 | + | |
| 1077 | + | |
| 1078 | + | |
| 1079 | + | |
| 1080 | + | |
| 1081 | + | |
| 1082 | + | |
| 1083 | + | |
1055 | 1084 | | |
1056 | 1085 | | |
1057 | 1086 | | |
| |||
1076 | 1105 | | |
1077 | 1106 | | |
1078 | 1107 | | |
1079 | | - | |
| 1108 | + | |
| 1109 | + | |
| 1110 | + | |
| 1111 | + | |
| 1112 | + | |
| 1113 | + | |
| 1114 | + | |
| 1115 | + | |
| 1116 | + | |
| 1117 | + | |
| 1118 | + | |
| 1119 | + | |
| 1120 | + | |
| 1121 | + | |
| 1122 | + | |
| 1123 | + | |
1080 | 1124 | | |
1081 | 1125 | | |
1082 | 1126 | | |
1083 | 1127 | | |
1084 | 1128 | | |
1085 | 1129 | | |
1086 | 1130 | | |
1087 | | - | |
| 1131 | + | |
1088 | 1132 | | |
1089 | 1133 | | |
1090 | 1134 | | |
| |||
1093 | 1137 | | |
1094 | 1138 | | |
1095 | 1139 | | |
1096 | | - | |
| 1140 | + | |
1097 | 1141 | | |
1098 | 1142 | | |
1099 | 1143 | | |
| |||
1111 | 1155 | | |
1112 | 1156 | | |
1113 | 1157 | | |
| 1158 | + | |
| 1159 | + | |
| 1160 | + | |
| 1161 | + | |
| 1162 | + | |
| 1163 | + | |
| 1164 | + | |
| 1165 | + | |
| 1166 | + | |
| 1167 | + | |
| 1168 | + | |
| 1169 | + | |
| 1170 | + | |
1114 | 1171 | | |
1115 | 1172 | | |
1116 | 1173 | | |
| |||
0 commit comments