Skip to content

lint: every hook-body-* / hook-api-update-readonly-* rule is unreachable for a hook authored as a handler function — the whole family is gated on body.language === 'js', and the reference CRM has 39 hooks and 0 bodies #16095

Description

@os-steve

Measured on the pinned @objectstack/lint@17.3.0 artifact as installed by objectstack-ai/hotcrm, ⛔ not on the platform source tree. Filed unassigned by the hotcrm step-3 rule survey (hotcrm#1613); the local assertions currently standing in for these stay in place there.

The gap

validateReadonlyHookWrites — and the sibling hook-body write validator — open with:

const body = hook.body;
if (!isRec5(body) || body.language !== "js") return;
const source = body.source;
if (typeof source !== "string" || source.trim() === "") return;

A hook authored as a TypeScript handler function never reaches that door. It carries no body, so every rule behind it returns before looking at anything:

  • hook-api-update-readonly-field
  • hook-api-update-readonly-when-field
  • hook-body-write-unknown-field
  • hook-body-write-unprovisioned-anchor
  • hook-body-source-unparseable

This is not hypothetical authoring style. Normalizing objectstack-ai/hotcrm's stack through normalizeStackInput and counting:

hooks: 39
{ withBody: 0, withHandler: 39, withSource: 0 }
sample hook keys: name, object, events, handler, priority, async, description, onError, runAs

Thirty-nine of thirty-nine. Actions in the same stack are the other way round — 24 of 31 carry body.source — so the action-side rules DO reach that app, and the hook-side rules reach none of it.

Measurement: the same defect, twice, one shape apart

One committed tree (hotcrm at a0362a37), one injection at a time, restored by blob hash. Clean-tree baseline: 0 error(s), 17 warning(s), 12 suggestion(s), exit 0. Both injections write the same readonly field (crm_case.is_escalated, readonly: true) through the same call, into the same file, in the same run design.

hook shape body of the injected hook pnpm lint
handler: async (ctx) => { … } (the repo's own shape, all 39) await ctx.api.object('crm_case').update({ id: ctx.input.id, is_escalated: true }) nothing fires — 17 warnings, exit 0, byte-identical to baseline
body: { language: 'js', source: "…" } the same statement, verbatim hook-api-update-readonly-field error, exit 1

The second row is the control: the rule is live, correct and gating on this exact stack. The first row is therefore a reading about the door, not about the rule.

For contrast, the flow-side and action-side counterparts of the same family both fire on this repo as authored:

rule injected where result
flow-update-readonly-field case_actions.escalate writes is_escalated error, exit 1
flow-update-readonly-when-field readonlyWhen added to crm_case.escalation_reason warning x4, exit 0
action-api-update-readonly-when-field readonlyWhen added to crm_contact.is_primary (written by mark_primary) warning, exit 0

So the hook surface is the only one of the three with no author-time signal at all, and it is the surface the reference app uses exclusively.

Why it matters more than a missing warning

The strip these rules describe is real at run time: on a non-system context the engine removes readonly keys from the UPDATE payload and the call still returns success. An author writing that from a handler gets no signal at any layer — not from lint, and not from the call, which succeeds. The rule exists precisely because that failure is silent, and the shape it is attached to is not the shape people write.

Suggested direction, not a prescription

The lowering path already exists and os lint already reasons about it: checkHookBodyLowering(config) reads FUNCTION values before any Zod parse, and #13651 made lowering's refusal loud rather than fail-open. If the write-set extraction can be run over a lowered body — or over the same extraction os build performs — the family reaches handler-authored hooks without changing what os build accepts. Failing that, the honest alternative is to say so on each rule: a rule that structurally cannot see the dominant authoring shape should not read as coverage.

⚠️ ⛔ This card asks for nothing about os build's acceptance. It is entirely about which hooks the author-time rules can see.

Related, and why this is not a duplicate

Refs: hotcrm#1613 · hotcrm#1582 (F1) · hotcrm#1587 (F6, whose test/hook-write-shape.test.ts row is credited entirely to the three unreachable hook-body-* ids)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions