fix(objectql,lint): a declarative operation: update action is no longer reported as a button wired to nothing - #16039
Conversation
…longer reported as a button wired to nothing The boot action-governance inventory (ADR-0110 D5) built `unboundDeclarations` from a `type`-only test. The declarative single-record field write (#14092) is that shape by construction — `ActionSchema` refuses `target` and `body` beside `operation: 'update'` and keeps `type` at its default `script`, because the platform action route performs the write — so every one of them was named at every boot and every `metadata:reloaded`, with a prescription parse refuses. Both remaining `type`-only readers now read `operation` first, matching the runtime's `isDeclarativeUpdateAction` precedence: - `packages/objectql/src/action-governance.ts` — the inventory finding. - `packages/lint/src/validate-ai-tool-references.ts` — the authoring-time mirror of the runtime's headless-invocability door, which had diverged from it and reported a resolvable `action_<name>` reference as fictional. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…widening The `dist/index.d.ts` probe (build at head → swap the changed file to the base sha → rebuild → diff → restore byte-exact) reports the only delta in `@objectstack/objectql` as JSDoc prose on `reconcileActionRegistrations`, and `@objectstack/lint`'s barrel byte-identical. No exported symbol or signature moves; the removed lint finding is `severity: 'warning'`, and `os lint` gates on `severity === 'error'` alone, so nothing is newly accepted or rejected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…gth tuple `mock.calls` is `any[][]`, so a destructured `[message]: [string]` parameter is not assignable to `Array.prototype.filter`'s predicate (TS2769). Caught by `packages/objectql`'s `check:test-typecheck`, which does compile `*.test.ts`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
📓 Docs Drift Check3 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 21 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 82c65296a143513f0e439408306ef8f42b55f63d && git checkout 82c65296a143513f0e439408306ef8f42b55f63d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8e0b2975835a0f1930f782a8e38a82f338eabad5 150373751d93088ba16763e5bf8ed544dbfcc725 && git checkout -B drift-repro 8e0b2975835a0f1930f782a8e38a82f338eabad5 && git merge --no-ff 150373751d93088ba16763e5bf8ed544dbfcc725
node scripts/docs-audit/affected-docs.mjs --json 8e0b2975835a0f1930f782a8e38a82f338eabad5 |
|
Fixes #15444
The boot action-governance inventory (ADR-0110 D5) built its
unboundDeclarationsfinding from a
type-only test, so every declarativeoperation: 'update'actionwas reported as "a button wired to nothing". Both readers that still asked
typefirst now ask
operationfirst, matching the precedence the runtime doors alreadyuse. Nothing about dispatch changes — the inventory is warn-only and
exception-proof, and the route dispatched these actions correctly before and after.
Reproduced before it was fixed
The new pin, run against unmodified source at the base sha, reports the defect
directly — the update action and the control are both in the finding:
After the guard:
count: 1,actions: ['todo_task:ghost_button']— the real deadbutton alone.
The prescription is refused at parse time — measured, both limbs
The warning tells the operator to "add a
body, or register a handler under thedeclared
target". Both are refused byActionSchema. Measured withsafeParse(via
@objectstack/spec/ui), not read off the source:successoperation: 'update'+patch+targetfalse[target]operation: 'update'+patch+bodyfalse[body]operation: 'update'+patch(the legal form)trueThe third row is the control: a full green parse, so the first two are refusals of
those keys and not of the fixture.
The card's headline is already false too — #15079's executor HAS landed
Triage recorded the headline as "accidentally accurate right now" because the
runtime executor had not landed. Verified by content on
origin/main, not by anAPI
mergedfield:packages/runtime/src/action-execution.tscarriesisDeclarativeUpdateActionand the[#15079]guards, introduced by8a12067bd(2026-09-04, "execute the declarative row-level
operation: 'update'action").So both limbs of the finding are wrong today, not one: the button does execute,
and the prescription is refused. That raises the defect's grade; the deliverable is
unchanged.
The sweep: the answer is SIX, not five — and the sixth is fixed here
Re-run mechanically over non-test
packages/**+apps/**sources, two axes, eachwith a firing positive control on the same command and scope.
type: 41 hits after narrowing tothe action vocabulary (310 before). Controls fired:
action-governance.ts:222(the fifth) 1 hit, the four known runtime: execute the declarative row-level
operation: 'update'action — the platform action route performs one data-plane update of the current record as the caller (runtime half of #14092) #15079 runtime readers 4 hits.target/bodypresence deciding averdict): exactly 3 hits, both controls firing.
Triaged, all 41 resolve to one of: a different vocabulary (
endpoint.type,flow.type, ASTnode.type), a branch on a non-scripttype a declarative updatenever carries (so the
typetest gives the right answer), alreadyoperation-first(the #15079 doors,
action.zod.ts:1731,stack.zod.ts:1647) — or these two:packages/objectql/src/action-governance.ts— this card's reader.packages/lint/src/validate-ai-tool-references.ts— the sixth, previouslyunenumerated.
materialisesAsToolsays in its own doc that it mirrors theruntime's headless-invocability door. That door learned
operationbeforetypein runtime: execute the declarative row-level
operation: 'update'action — the platform action route performs one data-plane update of the current record as the caller (runtime half of #14092) #15079; this mirror did not, so itsscriptarm answeredfalsefor an actionwith neither
targetnorbody— exactly the shape the spec forces. Effect: anai.exposeddeclarative update action referenced from a skill asaction_NAMEwas reported as a fictional tool reference, and the action wasoffered in the near-miss hint as one that "never materialises" — for metadata the
runtime lists and runs. This is the divergence runtime: execute the declarative row-level
operation: 'update'action — the platform action route performs one data-plane update of the current record as the caller (runtime half of #14092) #15079's own comment names as thefailure these predicates exist to prevent.
Per the card's instruction that a sixth belongs here rather than on a seventh card,
it is fixed in this PR and pinned.
Precedence, verified rather than copied
isDeclarativeUpdateActionis a bare equality on the declared key with notypeclause, and its contract is explicit that
operationis read beforetype"everywhere, without exception" — because data at rest that never went through
ActionSchemais where the two keys can contradict. Both guards here use that sameshape.
@objectstack/objectqlis upstream of@objectstack/runtime, and@objectstack/lintdoes not depend on the runtime at all, so neither can import theconstant; each spells it locally, and the pins are what hold them together.
Pins, and the ablation that proves they measure something
Both pins carry a positive control in the same run, so neither can pass against a
reader that reports nothing at all.
Each guard was ablated in one shell — mutation proved on disk before measuring
(anchored marker count 1 → 0 and
git hash-object!= the HEAD blob), thenrestored and the restore proved (blob == HEAD blob and
git diff HEADempty):action-governance.ts7839435571…!=752ed5414d…validate-ai-tool-references.tsd5ce54b4b5…!=4509e751ff…No rebuild step is involved: both tests import their subject by relative path
inside its own package, so vitest resolves source, not
dist/.Bump level:
patch, and why it is notminorThe
dist/index.d.tsprobe (build at head → swap the changed file to the base sha →rebuild → diff → restore byte-exact and prove it) reports:
@objectstack/objectql— the only delta is JSDoc prose onreconcileActionRegistrations. No exported symbol or signature moved.@objectstack/lint—dist/index.d.tsbyte-identical (245107 bytes both sides).And separately, on whether any request is newly accepted or rejected: no. The
inventory is warn-only, and the removed lint finding is
severity: 'warning'whileos lintgates onseverity === 'error'alone (packages/cli/src/commands/lint.ts).Nothing widens, so the "additive widening ⇒ at least
minor" floor does not apply.Flagging it explicitly since the dispatch anticipated
minor.Verification — all at
150373751pnpm --filter @objectstack/objectql exec vitest run …(3 governance suites) —37 passed (37);
pnpm --filter @objectstack/lint exec vitest run …—13 passed (13).
pnpm --filter @objectstack/objectql typecheck && pnpm --filter @objectstack/lint typecheck— exit 0. Both edited test files confirmed present in their typecheck programs via
--listFiles(1 hit each), so this is a measurement and not a green over unread files.npx eslint . --no-inline-config— the full repo scan, exit 0, 6037 files,0 errors, 0 warnings (count read from
--format json, not estimated).check:nul-bytes(
OK (scanned 7721 text file(s) … no raw ASCII control bytes)),check:startup-registry-verdict(✓ 42 startup/open-registry seam(s) … none recording a verdict the boot can contradict),check:durability-log-level,check:cross-package-test-inputs(OK: 27 package(s) read outside themselves, all declared),check:empty-changeset(✓ … 1 declaring changeset(s) added) andcheck:changeset-no-major(✓ This diff introduces no major bump.).node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack; itsprovenance line confirms the tree and the 5-path change set.
Scope note:
objectui'sActionRunnertypes its dispatch table onRunnableActionType, which the spec documents as a deliberate coupling and is nottouched here.
Generated by Claude Code