You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(lint,spec): L2 action body 写不存在字段从盲区变为作者时 lint 告警 (#4271)
The write-set lint #4305 gave L2 hook bodies now covers the other surface that
carries one. An action body is the same artefact — same `HookBodySchema`, same
`HookBodySchema.safeParse` in `actionBodyRunnerFactory`, same QuickJS sandbox —
so it fails the same way: `ctx.api.object('crm_deal').update({ stag: 'won' })`
succeeds, returns success to the caller, and the unknown column never lands.
New rule `action-body-write-unknown-field`, advisory, wired into
REFERENCE_INTEGRITY_RULES so `os validate` / `os lint` / `os compile` all report
it. Both places the runtime reads actions from are walked (top-level `actions`
and `objects[].actions`); a defineStack-merged action, which lives in both, is
reported once at its authored path.
Only the `ctx.api` write family carries over. An action's `ctx.input` is its
PARAMS bag, not a record, so resolving those names against object fields would
flag every correctly-named parameter. `ctx.record` is not a write surface
either: the runner hands the body a plain snapshot and never writes it back, so
`ctx.record.x = …` is discarded for declared and undeclared fields alike — a
different defect, and flagging only its undeclared half would imply the
declared half persists.
So the rule ships a declared PARTITION of the shared HOOK_BODY_WRITE_PATTERNS
— ACTION_BODY_WRITE_PATTERN_IDS plus ACTION_BODY_WRITE_EXCLUSIONS, each
exclusion carrying its reason — tested to cover the shared ledger exactly, so a
fourth pattern landing on the hook side fails this rule's test until someone
classifies it. Every applicable pattern is proved end-to-end through the full
validator; every exclusion is proved extractable-but-inapplicable. One
extractor, one field index, one system-column set, shared rather than copied.
The dedupe is by value (bound object + name + body source), not by object
identity the way collectBundleActions can afford: the suite runs on the parsed
stack and parsing rebuilds every node, so a merged action's two copies arrive
distinct-but-equal. An identity check passed the unit fixture and reported the
showcase app's one warning twice — caught by running `os validate` on it.
Lands one notch tighter than the hook side on the boot path: the only
applicable pattern is rooted at `ctx.api`, so an action body that never
mentions it does not parse at all, let alone load the ~9 MB TypeScript
compiler. Guarded by lazy-deps.test.ts.
spec: ScriptBodySchema and ActionSchema.body point at the action-side rule and
spell out that ctx.input (params) and ctx.record (a discarded snapshot) are not
record-write surfaces. Doc comments only — all 8 generated artifacts verified
unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0 commit comments