Uh oh!
There was an error while loading. Please reload this page.
refactor(runtime): spell the object-less action key as GLOBAL_ACTION_OBJECT_KEY in action-execution.ts - #14861
Conversation
…14678) `GLOBAL_ACTION_OBJECT_KEY` exists so the object-less action-registration key is written once. #14422 converged the owner-key ladder and the ObjectQL plugin's copy of it; three bare `'global'` spellings elsewhere in `packages/runtime/src/action-execution.ts` were never in that card's path, because the runtime fence it built was a re-export plus a delegating alias. All three are equal in value to the constant today — which is the defect, not a mitigation: it is what made them invisible to every test in the repo, and what would have let them part from the constant in silence. - `seedFlowActionParams`: the live comparison deciding whether an object-derived `<object>Id` param key is seeded. Substitution only — the predicate is unchanged, so `'*'` still falls through exactly as before. - `enforceActionParams`: the warn-once dedup key, also interpolated into the operator-facing `[action-params] <key>: …` line. - `collectActionDeclarations`'s docblock, which also called the key "the wildcard" against `action-governance.ts`'s "no wildcard semantics". It now reads as the sibling docblock 48 lines below already did. `action-owner-key-single-source.test.ts` gains a half C that reads `action-execution.ts` and fails if any quote spelling is written out by hand again. The forbidden spelling is derived from the constant, so the guard is not itself another copy of the literal it forbids. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
📓 Docs Drift Check2 anchor(s) derived from 1 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 — 24 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 16f90768f85e857ad0faedfb992dd97a123761c4 && git checkout 16f90768f85e857ad0faedfb992dd97a123761c4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f116b8f8d1b43ec2bfd64fbf0ebb0cf3c301b1c8 5ef23ba0760ee957d9f1c13fb6cdd97556e054cc && git checkout -B drift-repro f116b8f8d1b43ec2bfd64fbf0ebb0cf3c301b1c8 && git merge --no-ff 5ef23ba0760ee957d9f1c13fb6cdd97556e054cc
node scripts/docs-audit/affected-docs.mjs --json f116b8f8d1b43ec2bfd64fbf0ebb0cf3c301b1c8 |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33742671888 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14678
File face:
packages/runtime/src/action-execution.ts, and nothing else.packages/runtime/src/sandbox/**is untouched — PR #14826 is armed on that face with card #14760 queued behind it.packages/specis read-only here: it owns one site in this class and that site is named below rather than edited.Re-measured before editing, because the card said to
Three, at the three line numbers the card's current table gives. Neither of the two corrections that moved this card applies any more: the withdrawn fourth site at
:1539is gone, and the numbers have not shifted since75adf11.Widened past the single-quote spelling, in case the count was an artefact of the grep rather than of the tree:
adds only
:962—pass \undefined` for a global parent object schema` — the English adjective, not the token. So three is the real population, not three-of-some-quote-style.What changed
seedFlowActionParamsenforceActionParamscollectActionDeclarationsdocblockseedFlowActionParamsis the one that matters — a live comparison deciding whether an object-derivedIdparam key is seeded into an action's bag. Pure substitution:objectName !== GLOBAL_ACTION_OBJECT_KEY. The predicate is byte-for-byte the same test, so the legacy'*'key still falls through exactly as it did (see Residue).enforceActionParamsis the judgement the card asked to be made out loud. The case for leaving a literal is that a warn-once dedup key — which is also interpolated into the operator-facing[action-params] …line — must never fail to render. That argument does not survive contact with what the alternative actually is:GLOBAL_ACTION_OBJECT_KEYis a module-scopeconststring, already imported into this file, already interpolated by the sibling site. It cannot fail to render either. What a drift there really costs is an operator grepping logs by the key the engine now uses and silently missing these lines. So: converged. Note the deliberate asymmetry left in place one interpolation to the right — the?? 'action'fallback in the same template stays a bare literal, because it is a display placeholder with no constant behind it and no engine concept underneath. Same token shape, different role; that is the whole distinction this card turns on.The docblock carried a second defect independent of the literal: it called the key "the
'global'wildcard", contradictingaction-governance.ts— "engine.executeActionis an exact-stringMaplookup with no wildcard semantics". Both defects close together, and the replacement is not invented: it is the phrasing the sibling docblock 48 lines below already used, so the file now says the same thing in both places.grep -cfor all three quote spellings of the token in this file is now0,0,0.The sweep, and how true instances were told from same-token noise
The token is everywhere — 626 occurrences across 160 source files. Nearly all of them are a different vocabulary that happens to share a word, and the difference is invisible from the token:
The discriminator is not the token, it is the role: does this value become, or get compared against, the object half of the engine's
objectName:actionNameregistration key thatexecuteActionlooks up? Applying it splits the population into one true class and five unrelated vocabularies:unique: 'global' | 'organization') — by far the largest bucket, and the one that would have swallowed a token-level sweep whole.scope: 'global' | 'tenant' | 'user'), lifecycle and tenancy scope, automationlockScope, and app payload fields such as the showcase action's ownscope: 'global'return value.None of those five ever reaches an action registration key. That leaves the true class — every site where the value really is the object-less action key:
objectql/src/action-governance.ts:61objectql/src/action-governance.ts:87runtime/src/action-execution.ts×3cli/src/commands/lint.ts:321client/src/index.ts:4269spec/src/stack.zod.ts:1537runtime/src/domains/actions.ts×4cli/src/lint/hook-body-lowering.ts:279,cli/src/utils/lower-callables.ts:198What was deliberately left, and why
packages/cli/src/commands/lint.ts:321is a genuine instance — it builds the engine registration key with the sameobjectName-else-literal ladder, and its own comment says so ("it is the literal the engine really registers under"). It is not converged here for a reason that is not taste: that file is claimed.#14669 is open andpm:dispatchedonpackages/cli/src/commands/lint.ts:298— twenty-three lines above, inside the same docblock. Editing around another seat's live claim buys a conflict and nothing else. #14669's own body also states the precedent for the cross-package half: pulling@objectstack/cliin "would pull both into turbo's affected set and add a verification surface to a p3 convergence card".packages/client/src/index.ts:4269(invokeGlobaldispatching toinvoke('global', …)) is a true instance that cannot be converged as things stand.@objectstack/clientdepends on@objectstack/coreand@objectstack/specand nothing else — it has no path toGLOBAL_ACTION_OBJECT_KEYwithout giving the browser SDK an engine dependency. That is an architecture decision, not a p3 cleanup.packages/spec/src/stack.zod.ts:1537re-declares the value asGLOBAL_ACTION_SCOPEwith a docblock stating exactly why — "spelled here rather than imported because the spec sits below every runtime package". Off-limits to this seat by fence, and it is a documented second spelling rather than an accidental one. Naming it, not touching it.packages/runtime/src/domains/actions.tscarries four quoted spellings, all in comments; its live code already resolves throughactionExec.GLOBAL_ACTION_OBJECT_KEYat:431. Two of the four describe the literal URL path segment/actions/global/:action, which is a different thing that happens to share the word; the other two narrate the historical #3913 defect and are statements about a measured past. None carries the wildcard contradiction. Left, and the reason is on the record here rather than in silence.The two
packages/clilowering sites are false positives by role, which is worth showing rather than asserting.'global'there is anownerLabelthat becomes a generated handler name prefix (global_myAction) for the callable registry, and a diagnostic string. It never becomes anobjectName. If the constant moved, those labels would produce a differently-named handler ref — consistently, within the same lowering pass. No coupling, so no defect.Can the next one be made loud?
Yes, and cheaply enough that declining would have been the harder argument.
action-owner-key-single-source.test.tsalready exists in this package for exactly this weld — half A pins the behaviour, half B pins the structure. It gains a half C that readsaction-execution.tsand fails if any quote spelling of the key is written by hand again.The forbidden spelling is derived from the constant, not hard-coded:
A hard-coded
'global'there would have been a fourth copy of the very literal the file exists to forbid, going stale in the same silence. Derived, the guard follows the constant wherever it goes.This is proportionate for a p3 because it costs no new verification surface: an existing test file, in the affected package, reading a file it already reads, under gate families this card already owes.
It was verified to fail, twice, for two different reasons — a guard nobody has watched go red is a guard nobody has tested:
Both legs proved the mutation reached disk before reading any result (injected-text and removed-text counts, plus a
git hash-objectcomparison against theHEADblob), restored withgit checkout HEAD --, and proved the restore byte-identical (git diff HEADempty, hash equal to theHEADblob) before re-running green. No rebuild was needed for either leg and none is claimed: half C reads source text at runtime, and the constant it derives from comes from@objectstack/objectql's already-builtdist, which no mutation here touches.Changeset fork
patchon@objectstack/runtime, notskip-changeset.packages/runtime'sfileswhitelist is["dist", "README.md", "CHANGELOG.md"]—srcis not published raw, butdistis built from it, so the emitted output moves even though the behaviour does not. That is outside theskip-changesetcriterion, which needs a diff that publishes nothing from any package. Nothing reaches the published entry either — no export, signature or type changed, andaction-execution.tsis not re-exported frompackages/runtime/src/index.ts— which is what keeps it belowminor. This is the same fork and the same measurement PR #14667 took for this package on this file.Readings — all at
d1ec90e7, the head of this branchDependency closure built first (
pnpm --filter '@objectstack/runtime^...' build), so nothing below is a stale-distreading.pnpm --filter @objectstack/runtime typecheck— exit 0. Run by name, so it chained the second link:check:test-typecheck: OK — @objectstack/runtime's test layer compiles under packages/runtime/tsconfig.test.json; 27 file(s) / 191 error(s) / 69 pinned signature(s) held in test-typecheck-debt.json (shrink-only and identity-pinned). Nothing was added to that ledger. The edited test file is not in it, andtsc -p tsconfig.test.json --listFilesreaches it — so this is a measurement of the new code, not a green that skipped it.pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 action— 23 files, 366 tests, all passing. Half C is in the verbose transcript, so it ran rather than being collected and skipped.nul-bytes(OK … no raw ASCII control bytes),swallow-census-controls,dispatcher-error-vocabulary,engine-double-contract(OK — 759 pinned, 133 in the DEBT ledger, 3 exempt),where-matcher(332 matcher(s) discovered, 332 answer … correctly),cross-package-test-inputs(OK: 25 package(s) read outside themselves, all declared),test-source-alias,published-files,doc-authoring,objectql-double-limit,query-options-erasure,changeset-gate-self-tests,objectui-changeset,pm-half-states.The family list was re-derived from the actual change set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackrather than from the dispatch note: 36 families, 31 by path and 7 by change kind. The heavy remainder — the full lint farm, the workspace typecheck, andcheck:type-check-debt --re-measure, which needs the whole workspace closure built — is left to CI, which runs it once regardless.Residue, recorded rather than fixed
seedFlowActionParamsguards withobjectName !== GLOBAL_ACTION_OBJECT_KEY, butisObjectLessActionKey— exported from the same module and already imported into this file — also treats the legacy'*'key and the empty string as object-less. A route resolved at'*'therefore reaches the branch and seeds a*Idparam key. Inert in practice, since #3913 left nothing registering under'*', and correcting it would be a behaviour change rather than a spelling one, so it is deliberately not in this PR. Neither of the two functions edited here is named by any test in the repo, which is the other half of why the drift was invisible. Both observations are in the report to PM; the GitHub search quota was exhausted at the time, so the mandatory dedup search for a new card could not be run and filing blind would have been worse than handing it over.🤖 Generated with Claude Code
https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Generated by Claude Code