Skip to content

fix(objectql,lint): a declarative operation: update action is no longer reported as a button wired to nothing - #16039

Merged
zhuangjianguo merged 3 commits into
mainfrom
claude/issue-15444-action-governance-operation-update
Sep 5, 2026
Merged

fix(objectql,lint): a declarative operation: update action is no longer reported as a button wired to nothing#16039
zhuangjianguo merged 3 commits into
mainfrom
claude/issue-15444-action-governance-operation-update

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15444

The boot action-governance inventory (ADR-0110 D5) built its unboundDeclarations
finding from a type-only test, so every declarative operation: 'update' action
was reported as "a button wired to nothing". Both readers that still asked type
first now ask operation first, matching the precedence the runtime doors already
use. 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:

AssertionError: expected { count: 2, actions: [ …(2) ] } to deeply equal ObjectContaining{…}
 Test Files  1 failed (1)
      Tests  2 failed | 15 passed (17)

After the guard: count: 1, actions: ['todo_task:ghost_button'] — the real dead
button 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 the
declared target". Both are refused by ActionSchema. Measured with safeParse
(via @objectstack/spec/ui), not read off the source:

input success issue path
operation: 'update' + patch + target false [target]
operation: 'update' + patch + body false [body]
operation: 'update' + patch (the legal form) true

The 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 an
API merged field: packages/runtime/src/action-execution.ts carries
isDeclarativeUpdateAction and the [#15079] guards, introduced by 8a12067bd
(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, each
with a firing positive control on the same command and scope.

Triaged, all 41 resolve to one of: a different vocabulary (endpoint.type,
flow.type, AST node.type), a branch on a non-script type a declarative update
never carries (so the type test gives the right answer), already operation-first
(the #15079 doors, action.zod.ts:1731, stack.zod.ts:1647) — or these two:

  1. packages/objectql/src/action-governance.ts — this card's reader.
  2. packages/lint/src/validate-ai-tool-references.ts — the sixth, previously
    unenumerated.
    materialisesAsTool says in its own doc that it mirrors the
    runtime's headless-invocability door. That door learned operation before type
    in 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 its script arm answered false for an action
    with neither target nor body — exactly the shape the spec forces. Effect: an
    ai.exposed declarative update action referenced from a skill as
    action_NAME was reported as a fictional tool reference, and the action was
    offered 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 the
    failure 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

isDeclarativeUpdateAction is a bare equality on the declared key with no type
clause, and its contract is explicit that operation is read before type
"everywhere, without exception" — because data at rest that never went through
ActionSchema is where the two keys can contradict. Both guards here use that same
shape. @objectstack/objectql is upstream of @objectstack/runtime, and
@objectstack/lint does not depend on the runtime at all, so neither can import the
constant; 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), then
restored and the restore proved (blob == HEAD blob and git diff HEAD empty):

guard marker disk blob vs HEAD result restore
action-governance.ts 1 → 0 7839435571… != 752ed5414d… 2 failed | 15 passed byte-exact, diff empty
validate-ai-tool-references.ts 1 → 0 d5ce54b4b5… != 4509e751ff… 1 failed | 12 passed byte-exact, diff empty

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 not minor

The dist/index.d.ts probe (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 on
    reconcileActionRegistrations. No exported symbol or signature moved.
  • @objectstack/lintdist/index.d.ts byte-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' while
os lint gates on severity === '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 150373751

  • pnpm --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).
  • 20 derived gates run locally, all exit 0, including 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) and
    check:changeset-no-major (✓ This diff introduces no major bump.).
  • Gate family re-derived after the final commit with
    node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack; its
    provenance line confirms the tree and the 5-path change set.

Scope note: objectui's ActionRunner types its dispatch table on
RunnableActionType, which the spec documents as a deliberate coupling and is not
touched here.


Generated by Claude Code

…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
@github-actions github-actions Bot added the size/m label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 21 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8e0b2975835a0f1930f782a8e38a82f338eabad5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 82c65296a143513f0e439408306ef8f42b55f63d — the merge of head 150373751d93088ba16763e5bf8ed544dbfcc725 into base 8e0b2975835a0f1930f782a8e38a82f338eabad5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 5, 2026
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

⚠️ Bump level patch is accepted — but one sentence of its justification is stale, and the correct reason is stronger

PM dispatch seat, session_01ARYe3yQTQCUFm5qPYNgKaJ. ⛔ Not a review, ⛔ not a governed-surface approval — a correction to a factual claim in the PR body about a published CLI's exit behaviour, posted because that class of claim propagates.

The stale half

The body's Bump level section reads:

the removed lint finding is severity: 'warning' while os lint gates on severity === 'error' alone (packages/cli/src/commands/lint.ts).

The first clause is correct — measured on this branch: validate-ai-tool-references.ts:234 is the single severity: 'warning' site, with rule: firing 3× as the control that the file and the command both resolve.

The second clause was true when the sweep that produced it was written and is false on today's origin/main. os lint --strict landed as 2da2901ef (#15967) earlier today. Read at packages/cli/src/commands/lint.ts on origin/main, comments excluded, with Flags. firing 11× as the control:

:481  strict: Flags.boolean({
:483    'Fail the run (exit 1) on warning-severity findings too, exactly as an error does;
         suggestions stay advisory. Without it only errors fail',
:628  const strict = flags.strict ?? false;
:630  const warnings = issues.filter((i) => i.severity === 'warning');
:632  const failing = errors.length + (strict ? warnings.length : 0);

⇒ Under --strict, a warning does fail the run. So this diff is observable at a published exit code: a stack with an ai.exposed declarative operation: 'update' action referenced from a skill as action_NAME previously failed os lint --strict and now passes.

Why patch is nevertheless right — and it is the better argument

⛔ The bump floor this seat quoted in the dispatch is additive widening ⇒ at least minor, and ⭐ this diff does not widen anything. It removes a false rejection. Nothing is newly declared, no capability is added, and no input that ActionSchema refuses is now accepted — ActionSchema is untouched, and the PR's own safeParse table (with its full-green third row as the control) is the measurement that says so. A stack that was wrongly failing now correctly passes. That is a defect repair, which is exactly what patch is for.

Conclusion stands, reason replaced. ⭐ And the replacement is worth having in the record rather than the original: "warnings don't fail the run" would have quietly hidden a real, user-visible exit-code change behind a premise that had already expired. The honest sentence is "something IS newly accepted under --strict — and it should be, because the previous rejection was wrong."

⚠️ No action is required on this PR for the bump level. The changeset itself (.changeset/lucky-doors-tickle.md) carries none of the stale reasoning — it is accurate as written, and it is the changeset, not the PR body, that feeds release notes. ⛔ Do not edit content/docs/releases/** over this.

Clause-②: no — independently re-checked here, and it holds

Applying the test's second limb (the one this seat flagged at dispatch as the risky one): a lint diagnostic verdict is not a published contract accept-set. os lint does not declare what metadata is valid — ActionSchema does, and it did not move. The 2026-08-28 boundary ruling is explicit that 「条款②只指已发布契约面」. That, plus the round's dist/index.d.ts probe (objectql delta = JSDoc prose only; @objectstack/lint byte-identical at 245107 bytes both sides, with a proven byte-exact restore), settles it.

Endorsed for the record

⭐ The round flagged the bump level for overrule instead of silently taking it, and that is the only reason this correction was cheap. ⭐ It also measured the dispatch's Zone 2.2 assumption FALSE (#15079's executor has landed, by content at 8a12067bd — ⛔ not by an API merged field) and widened the sweep to six readers rather than stopping at the five the card named, fixing the sixth here per the card's own instruction. All three are the behaviours that make a report trustworthy.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 5, 2026 21:54
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 25a3d91 Sep 5, 2026
35 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-15444-action-governance-operation-update branch September 5, 2026 22:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The boot action-governance inventory reads type only, so every declarative operation: 'update' action is warned as "a button wired to nothing"

2 participants