Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/lint-field-no-consumers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@objectstack/lint": minor
---

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`.

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.

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.

Public surface: `validateFieldConsumers`, `FIELD_NO_CONSUMERS`, `FIELD_CONSUMER_ROOTS`, `FIELD_CARRIER_ROOTS`, and the `FieldConsumerFinding` / `FieldConsumerVerdict` / `FieldConsumerSeverity` types.
2 changes: 1 addition & 1 deletion content/docs/deployment/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ os compile --json # JSON output for CI pipelines
→ Normalizing stack definition...
→ Lowering inline handlers...
→ Validating protocol compliance...
→ Running author-time rules (42)...
→ Running author-time rules (43)...
→ Checking capability providers (#3366)...
→ Collecting package docs (ADR-0046)...
→ Writing artifact...
Expand Down
2 changes: 1 addition & 1 deletion content/docs/deployment/validating-metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ A clean run walks the registry and reports timing:
Config: /path/to/support-desk/objectstack.config.ts
Load time: 21ms
→ Validating against ObjectStack Protocol...
→ Running author-time rules (42)...
→ Running author-time rules (43)...
→ Checking capability providers (#3366)...
→ Checking package docs (ADR-0046)...

Expand Down
2 changes: 1 addition & 1 deletion content/docs/getting-started/build-with-claude-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ visible: 'status != "resolved"'
◆ Validate
────────────────────────────────────────
→ Validating against ObjectStack Protocol...
→ Running author-time rules (42)...
→ Running author-time rules (43)...

✗ Author-time rules failed (1 issue)
• stack · action 'resolve_ticket' visible: bare reference `status` — a
Expand Down
2 changes: 1 addition & 1 deletion content/docs/ui/react-pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ objectstack validate
────────────────────────────────────────
→ Loading configuration...
→ Validating against ObjectStack Protocol...
→ Running author-time rules (42)...
→ Running author-time rules (43)...
→ Checking capability providers (#3366)...
→ Checking package docs (ADR-0046)...

Expand Down
26 changes: 26 additions & 0 deletions packages/lint/src/authoring-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ import { validateCapabilityReferences } from './validate-capability-references.j
import { validateFlowTriggerReadiness } from './validate-flow-trigger-readiness.js';
import { validateApprovalApprovers } from './validate-approval-approvers.js';
import { validateRecordTitle } from './validate-record-title.js';
import { validateFieldConsumers } from './validate-field-consumers.js';
import { validateSemanticRoles } from './validate-semantic-roles.js';
import { validateFormLayout } from './validate-form-layout.js';
import { validateSeedReplaySafety } from './validate-seed-replay-safety.js';
Expand Down Expand Up @@ -928,6 +929,31 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
run: (stack) => validateRecordTitle(stack),
},
// [#15922] A declared field that nothing in the stack reads or displays —
// the field-level remainder of #4698's "declared but never read" class,
// landed here under the hotcrm#1543 ruling. Object-aware (the same name on
// two objects gets two verdicts) and advisory: a consumer can live outside
// the stack (an API client, another package's hook, a Studio-authored view),
// so the ceiling for a static check is a warning — a refusal would narrow
// the authorable surface and is the maintainer's call. `normalized` because
// it needs no parsed stack (it resolves names, not shapes), so `os lint`
// runs it too.
//
// CLI-only for the FULL-SNAPSHOT reason, and it is the sharpest instance of
// that reason in the table: the rule's whole verdict is the ABSENCE of a
// reference across views / pages / flows / datasets, none of which the
// per-write snapshot carries, so on an object write it would report every
// field of the written object as inert.
{
name: 'validateFieldConsumers',
tier: 'advisory',
input: 'normalized',
commands: ALL,
source: 'packages/lint/src/validate-field-consumers.ts',
surfaces: CLI_ONLY,
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
run: (stack) => validateFieldConsumers(stack),
},
// ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
// into the object's field map; a dangling one is Zod-valid and silently inert
// at render. Advisory: every consumer degrades gracefully.
Expand Down
14 changes: 14 additions & 0 deletions packages/lint/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,20 @@ export type {

export { validateNavAccess, NAV_OBJECT_UNGRANTED } from './validate-nav-access.js';
export type { NavAccessFinding, NavAccessSeverity } from './validate-nav-access.js';
// [#15922] A declared field with zero consumers across the registered
// metadata roots — advisory, object-aware, one rule id with the verdict
// carried on the finding (see the module note for the taxonomy decision).
export {
validateFieldConsumers,
FIELD_NO_CONSUMERS,
CONSUMER_ROOTS as FIELD_CONSUMER_ROOTS,
CARRIER_ROOTS as FIELD_CARRIER_ROOTS,
} from './validate-field-consumers.js';
export type {
FieldConsumerFinding,
FieldConsumerSeverity,
FieldConsumerVerdict,
} from './validate-field-consumers.js';

export {
validateTranslationReferences,
Expand Down
Loading
Loading