lint: reach handler-authored hooks with the hook write-set rules, and record which intakes reach them - #16564
Conversation
… rules reach handler-authored hooks `os lint` judged the un-lowered normalized stack, so every rule in the `hook-body-*` / `hook-api-update-readonly-*` family returned before reading a hook authored as an inline `handler` function; `os build` lowers first and never had the gap. `lintConfig` now runs `lowerCallables` on the same input and hands the lowered view to the `parsed` tier only, leaving the function-reading rules and the caller's stack untouched. Doors measured with a control per leg (lint / build / validate), the two validators carry the reach ledger in their headers, docs and changeset added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
…n the scaffold door The reach annotation landed naming three `os *` commands. The doors are the call sites of `runAuthoringRules`, and there are more of them than there are commands: `os build` enters twice (the union run and the per-package run) and `runScaffoldAuthoringRules` — which `os init` / `dev` drive over a rendered template — is a fourth intake that lowers before it parses and has therefore reached this family all along. A ledger that omits a reached door mis-states coverage in the same direction the card is about. Both rule headers now enumerate every measured intake, each recorded with the body-authored control that makes its verdict readable, and the scaffold door gains a pin with that control beside it. The pin asserts `schemaError` is null before reading the verdict, so a stack that stops parsing cannot read as "no finding". No rule logic changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
…ok-rules-reach-handler-authored-hooks
📓 Docs Drift CheckThis PR changes 2 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 25 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 7f687078cb6143025a12c82c1c02d88eb53b406a && git checkout 7f687078cb6143025a12c82c1c02d88eb53b406a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 554a16037eedb448a9ddc6d70434dc45208bcfca e4e0a8ef488d106a79d73aeaa9a064d922ece9a3 && git checkout -B drift-repro 554a16037eedb448a9ddc6d70434dc45208bcfca && git merge --no-ff e4e0a8ef488d106a79d73aeaa9a064d922ece9a3
node scripts/docs-audit/affected-docs.mjs --json 554a16037eedb448a9ddc6d70434dc45208bcfca
|
…claration (#16095) The Check Changeset gate refuses patch on a package whose src moved under a clause-② declaration (maintainer ruling 2026-09-04, batch #35, #15294); ruled by the contract review on #16095 (comment 5571527379). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XesLUWmuhjuRwmU618AZ1M
|
Contract review PASS at Generated by Claude Code |
Fixes #16095
Clause-②: yes
Every rule in the
hook-body-*/hook-api-update-readonly-*family opens onbody.language === 'js'. A hook authored as an inlinehandlerfunction carries nobody, so on the stackos linthanded the rule registry, the whole family returned before reading anything.os buildnever had that gap: it runslowerCallablesbefore its Zod parse and judges the lowered stack.lintConfignow hands the registry'sparsedtier that same lowered view, so the pre-flight refuses what the build would refuse.Nothing here changes what
os buildaccepts, andvalidate-action-body-writes.tsis untouched.Read this first: the review tier this PR is missing
The mandated contract-review tier was measurably unavailable for this card, so it ran one step down under the documented quota-exhaustion exemption. The
needs:contract-reviewlabel is the compensation, not a formality: this PR changes whatos lintrefuses, and the severity/rollout decision behind that belongs to a reviewer running at the proper tier. It should not be flipped ready, queued or merged while the label stands. Everything below is written for a reviewer who was not here.Premise check against
origin/mainThe card's premise holds, with two corrections a reviewer should not re-derive:
origin/mainthe three doors arevalidate-hook-body-writes.ts:729,validate-readonly-hook-writes.ts:291,validate-action-body-writes.ts:283. Re-read and confirmed.@objectstack/lint@17.3.0, which is outside this repository. Re-derived locally instead, and the local shape is different — see the numbers below. The reference app's ratio is not verified by this PR and should not be read as if it were.Which intakes reach the family, enumerated with a control on every leg
The doors are the call sites of
runAuthoringRules, and there are more of them than there are commands. A reachability claim measured through one entry point is a claim about that entry point, so every leg below carries the body-authored control — the identical statement written as an explicitbody— which must fire wherever the handler leg is silent for the silence to mean "door", not "rule".os build, union runos build, per-package runos lintrunScaffoldAuthoringRules(os init/dev)os validateTwo of those rows are the reason this PR has a second commit. The reach annotation first landed naming three
os *commands;os buildactually enters twice, andrunScaffoldAuthoringRulesis a fourth intake that lowers before it parses and has therefore reached this family all along. A ledger that omits a reached door mis-states coverage in the same direction the card is about, so both rule headers now enumerate every measured intake and the scaffold door has a pin with its control beside it.The
os validaterow is a measurement, not a decision. Closing it would change whatos validaterefuses, which is its own call and is not made here.Finding delta — the blast-radius measurement
Measured over this repository's own four example apps (
examples/app-crm,app-showcase,app-todo,app-multi-package) by loading each real config through the realos lintpath, once with this change and once with the call site reverted to its pre-change form:Row for row identical. No finding added, none removed, no severity moved. No configuration that passes today starts failing, so the
error/exit-1 escalation that would have made severity a maintainer decision does not occur on this corpus.Local population, re-derived: 6 hooks, of which 2 are handler-authored (
app-crm1,app-todo1) and 4 are body-authored (app-showcase). The parsed tier previously saw 4 of those 6 carrying ajsbody; it now sees 6. So the reach genuinely moved by 2 hooks and still produced 0 new findings. Separately, 125 of 157 actions are body-authored, consistent with the triage note that the action-body door reaches its intake.The zero is a real zero, not a dead instrument. Positive control, run on the real
app-crmconfiguration with only the hook's handler body swapped for a write to that app's realreadonly: truefieldcrm_opportunity.approval_status:hook-api-update-readonly-field, aterrorThe two real handler-authored hooks mutate
ctx.input.datadirectly and never callctx.api, which is the honest reason the family has nothing to say about them.Scope limit a reviewer should weigh. This delta is zero on this repository. The reference CRM is out of this session's scope and its 39-hook population could not be measured here, so the real-world blast radius of the
error-severity member is not established by this PR. If the severity or a warning-first rollout is in question, that decision needs the reference app's numbers, which this PR does not supply.Evidence
HEADby blob hash, not by an exit code.pnpm --filter @objectstack/cli exec vitest run --project unit— 182 files, 2477 passed, 6 expected-fail.pnpm --filter @objectstack/cli exec vitest run --project integration— 32 files, 284 passed. Run locally because this PR adds an integration-tier file (the e2e door suite spawns the CLI).pnpm --filter @objectstack/lint test— 100 files, 3486 passed.pnpm --filter @objectstack/lint --filter @objectstack/cli run typecheck— clean, test layer included, no new ledger debt.pnpm lint(whole repo,eslint . --no-inline-config) — clean. Run in full, so no narrowing argument is needed.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, then--ran, which reports86 derived famil(ies) accounted for — 86 run, 0 NOT-MEASURED. One family,check:type-check-debt, first returned exit 3PREREQUISITE NOT METafter tsc hit an OOM under a 4096 MB ceiling; that is the not-measured class rather than a finding, and it is green on re-run with a raised heap.33ade701be, after the final merge oforigin/main.Deliberately not done
os buildacceptance is untouched. Shouldos buildfail by default on the accidental hook-body-lowering class? — deferred until the new lint rule has produced a real population number #13838 is not addressed here and stays open; it is waiting on a population number, and the local figures above are offered to it rather than merged into it.validate-action-body-writes.tsis untouched. Its identical-looking door reaches its intake, so it is not a defect and was not "fixed" in passing.os validateis left not reaching the family. Recorded as a measurement in both rule headers; changing it is a separate decision.Changeset
.changeset/lint-hook-rules-reach-handler-authored-hooks.md—@objectstack/climinor (rule reachability changes),@objectstack/lintpatch (header ledger only, behaviour unchanged). Noskip-changeset: this publishes from a package.🤖 Generated with Claude Code
https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
Generated by Claude Code