Skip to content

Commit a87163c

Browse files
baozhoutaoclaude
andauthored
feat(lint): report a declared field with zero consumers across the registered metadata roots (#15922) (#16323)
* feat(lint): report a declared field with zero consumers across the registered metadata roots New advisory rule `field-no-consumers` (`validateFieldConsumers`): an object field that nothing in the stack reads or displays is a warning on validate, build and lint. Object-aware (the same name on two objects gets two verdicts), carriers (translations, seeds, mappings, permission grants, flow writes, prose) never count, and the finding carries the verdict, the carrier paths a removal must clean, and the roots scanned. Exemptions are derived from the spec: injected system columns, the ADR-0079 title field, master_detail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8 * docs: the CLI transcripts print 43 author-time rules now that field-no-consumers is registered `check:docs-transcript-drift` holds the four hand-written `os validate` / `os build` transcripts equal to what `authoringRulesFor()` derives; the new registry entry moves that count from 42 to 43. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8 --------- Co-authored-by: Claude Code <noreply@anthropic.com>
1 parent b9a14dd commit a87163c

9 files changed

Lines changed: 1036 additions & 4 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
New advisory rule `field-no-consumers` (`validateFieldConsumers`): a field declared on an object that nothing in the stack reads or displays is reported as a `warning` by `os validate`, `os build` and `os lint`.
6+
7+
Until now such a field was schema-valid and passed every platform check — the declaration was inert and nothing in the toolchain said so. The rule is object-aware (the same field name on two objects gets two verdicts, resolved against the object whose declaration encloses each reference), and it distinguishes consumers from carriers: a view column, form section, page binding, flow node, dataset dimension, widget filter, formula, validation, hook or action is a consumer; a translation label, a seed value, an import-mapping column, a field-level permission grant or a flow that only writes the field is a carrier and never counts. The finding carries the verdict (`carrier-only` with the carrier paths a removal must clean, or `inert`), the roots scanned, and — when the name is also declared elsewhere — the other objects, so a per-object verdict is never mistaken for a name-level one.
8+
9+
Exempt, each derived from the spec rather than listed by hand: the registry-injected system columns an author re-declared, the record's title field (ADR-0079 `nameField` ladder), and `master_detail` fields (ADR-0035 — cascade delete, `controlled_by_parent` sharing and roll-ups read the relationship by declaration). A stack that declares no consumer root at all (objects only, or objects plus carriers) is not judged: its consumers live in another package. Test fixtures are never scanned.
10+
11+
Public surface: `validateFieldConsumers`, `FIELD_NO_CONSUMERS`, `FIELD_CONSUMER_ROOTS`, `FIELD_CARRIER_ROOTS`, and the `FieldConsumerFinding` / `FieldConsumerVerdict` / `FieldConsumerSeverity` types.

content/docs/deployment/cli.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ os compile --json # JSON output for CI pipelines
477477
→ Normalizing stack definition...
478478
→ Lowering inline handlers...
479479
→ Validating protocol compliance...
480-
→ Running author-time rules (42)...
480+
→ Running author-time rules (43)...
481481
→ Checking capability providers (#3366)...
482482
→ Collecting package docs (ADR-0046)...
483483
→ Writing artifact...

content/docs/deployment/validating-metadata.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ A clean run walks the registry and reports timing:
555555
Config: /path/to/support-desk/objectstack.config.ts
556556
Load time: 21ms
557557
→ Validating against ObjectStack Protocol...
558-
→ Running author-time rules (42)...
558+
→ Running author-time rules (43)...
559559
→ Checking capability providers (#3366)...
560560
→ Checking package docs (ADR-0046)...
561561

content/docs/getting-started/build-with-claude-code.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ visible: 'status != "resolved"'
263263
◆ Validate
264264
────────────────────────────────────────
265265
→ Validating against ObjectStack Protocol...
266-
→ Running author-time rules (42)...
266+
→ Running author-time rules (43)...
267267
268268
✗ Author-time rules failed (1 issue)
269269
• stack · action 'resolve_ticket' visible: bare reference `status` — a

content/docs/ui/react-pages.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ objectstack validate
380380
────────────────────────────────────────
381381
→ Loading configuration...
382382
→ Validating against ObjectStack Protocol...
383-
→ Running author-time rules (42)...
383+
→ Running author-time rules (43)...
384384
→ Checking capability providers (#3366)...
385385
→ Checking package docs (ADR-0046)...
386386

packages/lint/src/authoring-rules.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ import { validateCapabilityReferences } from './validate-capability-references.j
118118
import { validateFlowTriggerReadiness } from './validate-flow-trigger-readiness.js';
119119
import { validateApprovalApprovers } from './validate-approval-approvers.js';
120120
import { validateRecordTitle } from './validate-record-title.js';
121+
import { validateFieldConsumers } from './validate-field-consumers.js';
121122
import { validateSemanticRoles } from './validate-semantic-roles.js';
122123
import { validateFormLayout } from './validate-form-layout.js';
123124
import { validateSeedReplaySafety } from './validate-seed-replay-safety.js';
@@ -928,6 +929,31 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
928929
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
929930
run: (stack) => validateRecordTitle(stack),
930931
},
932+
// [#15922] A declared field that nothing in the stack reads or displays —
933+
// the field-level remainder of #4698's "declared but never read" class,
934+
// landed here under the hotcrm#1543 ruling. Object-aware (the same name on
935+
// two objects gets two verdicts) and advisory: a consumer can live outside
936+
// the stack (an API client, another package's hook, a Studio-authored view),
937+
// so the ceiling for a static check is a warning — a refusal would narrow
938+
// the authorable surface and is the maintainer's call. `normalized` because
939+
// it needs no parsed stack (it resolves names, not shapes), so `os lint`
940+
// runs it too.
941+
//
942+
// CLI-only for the FULL-SNAPSHOT reason, and it is the sharpest instance of
943+
// that reason in the table: the rule's whole verdict is the ABSENCE of a
944+
// reference across views / pages / flows / datasets, none of which the
945+
// per-write snapshot carries, so on an object write it would report every
946+
// field of the written object as inert.
947+
{
948+
name: 'validateFieldConsumers',
949+
tier: 'advisory',
950+
input: 'normalized',
951+
commands: ALL,
952+
source: 'packages/lint/src/validate-field-consumers.ts',
953+
surfaces: CLI_ONLY,
954+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
955+
run: (stack) => validateFieldConsumers(stack),
956+
},
931957
// ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
932958
// into the object's field map; a dangling one is Zod-valid and silently inert
933959
// at render. Advisory: every consumer degrades gracefully.

packages/lint/src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,20 @@ export type {
628628

629629
export { validateNavAccess, NAV_OBJECT_UNGRANTED } from './validate-nav-access.js';
630630
export type { NavAccessFinding, NavAccessSeverity } from './validate-nav-access.js';
631+
// [#15922] A declared field with zero consumers across the registered
632+
// metadata roots — advisory, object-aware, one rule id with the verdict
633+
// carried on the finding (see the module note for the taxonomy decision).
634+
export {
635+
validateFieldConsumers,
636+
FIELD_NO_CONSUMERS,
637+
CONSUMER_ROOTS as FIELD_CONSUMER_ROOTS,
638+
CARRIER_ROOTS as FIELD_CARRIER_ROOTS,
639+
} from './validate-field-consumers.js';
640+
export type {
641+
FieldConsumerFinding,
642+
FieldConsumerSeverity,
643+
FieldConsumerVerdict,
644+
} from './validate-field-consumers.js';
631645

632646
export {
633647
validateTranslationReferences,

0 commit comments

Comments
 (0)