fix(lint)!: action-name-undefined resolves record:related_list action ids against the child object - #21626
Conversation
…s against the child object Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…fined Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 4 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 e4be8b61164bddc24c0c5e6cf37b56b65578ed9c && git checkout e4be8b61164bddc24c0c5e6cf37b56b65578ed9c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 045b946256d988653fdca185c7fd33d6d86bd78d b37812bed9f9a02ca545c7a236e3d72173991894 && git checkout -B drift-repro 045b946256d988653fdca185c7fd33d6d86bd78d && git merge --no-ff b37812bed9f9a02ca545c7a236e3d72173991894
node scripts/docs-audit/affected-docs.mjs --json 045b946256d988653fdca185c7fd33d6d86bd78d
|
Fixes #20936
Clause-②: no (narrowing)
What
action-name-undefined(packages/lint/src/validate-action-name-refs.ts) now walksrecord:related_list→properties.actions[], scoped to that component type, and asks the two questions the console's renderer asks:stack.actionsentry bound to it byobjectName(the setdefineStackmerges into the object'sactions, so the set the object's metadata serves). An id defined only on the page's object, or only as a global action, is refused like a typo. The message names where it is defined. The did-you-mean and the hint's action list come from the child object.ACTION_LOCATIONS(imported from@objectstack/spec/ui), classified per member in aRecordkeyed by the spec'sActionLocationtype. That giveslist_toolbar,list_itemandrecord_related. A location the spec adds fails@objectstack/lint's typecheck until it is classified, so the set cannot go stale the way a hard-coded pair would.Both findings use the same rule id,
action-name-undefined, at severityerror. Each is reported at the id's authored index. Only string elements are ids (inline objects are skipped, as onpage:header). The child object is the component's bounddataSource.objectwhen one is set, otherwiseproperties.objectName. The walk says nothing about a child object this stack does not define.Measurements against the PM's mechanism assumptions
Read at
origin/mainf97660cdd6(this branch's base) and at objectui89cad75d55702cc4f267bead5bf267de575d5842(.objectui-shaon that base).record:quick_actions.actionNames[],record:alert'saction.actionNameandpage:header'sactions[]. The docblock at:27–:30saidactions"is declared separately onrecord:related_list", and no walk read it. That sentence is replaced by a bullet for the new walk.packages/plugin-detail/src/renderers/record-related-list.tsx:287readsschema.actions.:290looks upuseMetadataItem('object', …)for the relatedobjectName.:301takesrelatedObjectMeta.actionsas the registry, and:303hands both toplaceAuthoredRelatedListActions. InrelatedListActions.ts,:110resolves the ids withresolveDeclaredActionIdsagainst that registry.:141–:143place byactionRendersAtatlist_toolbar/list_item/record_related. An id that resolves but is placed at none of the three is refused asunplaced. Control: at the previous pindb11afd4967c,git grep -c "schema.actions"on the same file answers 0 (exit 1), whileschema.relationshipFieldanswers 3, so the grep reaches the file.git merge-base --is-ancestor f4ed2387e9 89cad75d5570exits 0.collectActionNames, the union ofstack.actionsand every object'sactions. The related list's child isproperties.objectName, the same keyvalidate-page-field-bindings.ts'srelatedListFieldRefsalready reads. objectui's data-source gate (packages/react/src/element-data-source/ElementDataSourceGate.tsx:398) writes a bounddataSource.objectover it. How the scope note and the direction fit: the note keeps the other walks stack-wide because ownership and location checks there would cost the ADR-0072 D1 zero-false-positive posture for coverage nobody asked for. Neither reason holds for this walk. The renderer asks both questions itself and refuses on either miss, so a finding is the runtime's own verdict moved to authoring time ("resolve at runtime for the surface being authored", ADR-0072 D1). This card's ruling also asks for that coverage. One false-positive source stays: a child object this stack does not define has its actions in another package, so the walk is silent there rather than guessing. The docblock's scope note now says this, and every other walk is unchanged. I see no real conflict, so this is not raised as a question.ACTION_LOCATIONS(packages/spec/src/ui/action.zod.ts:663) is the full six-location vocabulary and listsrecord_related. It does not declare which locations a related list draws: the list draws three of the six. Using the whole vocabulary as the set would accept arecord_header-only action that the renderer refuses. So the rule imports the vocabulary and theActionLocationtype and classifies every member exhaustively. No location literal is copied outside a compiler-checked key. Whether the spec should export that subset itself is in the report'sopen_questions. This PR does not decide it.packages/lint. There is no producer-side change: the spec already types the key asz.array(z.string()), and the renderer already refuses.Pins (beside #20105's)
validate-action-name-refs.test.ts,describe('validateActionNameRefs — record:related_list actions'):record_related,list_toolbar,list_item, plus one bound throughstack.actionsobjectName: silent;record_headeronly, at[], and with nolocations: three findings;dataSource.objectwins overobjectName;sys_member): silent.Tests (all at
b37812bed9unless noted)pnpm --filter @objectstack/lint test:Test Files 119 passed (119),Tests 5621 passed | 5 skipped (5626).pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-action-name-refs.test.ts:Tests 36 passed (36)(30 before, plus 6).pnpm --filter @objectstack/lint typecheck(at6a73a0ad67, samesrcas the head): exit 0. The test layer is clean against its ledger: "2 file(s) / 6 error(s) / 2 pinned signature(s) held", unchanged.distprobe:require('packages/lint/dist/index.cjs').validateReferenceIntegrity(stack)on a related list naming one defined and one undefined child id returns exactly oneaction-name-undefinederrorat…properties.actions[1]. The ESM entry gives the same count. This is the suiteos validate/os lint/os buildcall.sys-user,sys-organization,sys-position, walked withwalkPageComponents) hold 13 related lists, and 0 of them authoractions.git grep -c related_list -- examples(CHANGELOGs excluded) answers 0 files, exit 1. Control:record:quick_actions|page:headerhits 3 files in the same tree.Reverse verification (one-off, from the committed head, restore proven by blob equality and an empty
git diff HEAD)record_relatedfrom the classification (node scripts/ablation-replace.mjs … --delete -- pnpm --filter @objectstack/lint exec tsc --noEmit): red withTS2741: Property 'record_related' is missing in type …. Restored to blobe0deed9272da, which equals HEAD.record_related: nulland running the rule's test file:Tests 4 failed | 32 passed (36). All four new tests that use arecord_relatedaction go red; the stack-wide walks stay green. Restored to blobe0deed9272da, which equals HEAD.Gates
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackatb37812bed9, and reconciled with--ran: "62 derived famil(ies) accounted for — 60 run, 2 NOT-MEASURED". Every run family exited 0, includingcheck-adr-0087-registration(the changeset carriesnot-required (no-migration-prescription)),check-changeset-no-major,check-empty-changeset,check:nul-bytes,check:doc-authoring,check:engine-double-contractandcheck:type-check-debt(re-run after a first attempt hit my own 300s timeout under box contention; the re-run reported "none above its recorded number").check:dual-build-cjs-loadsandcheck:lean-entry-closure, reason: PREREQUISITE NOT MET, because they need the whole workspace built (85 packages have nodist/). Narrowed instead: the diff adds no new import specifier to@objectstack/lint(@objectstack/spec/uiwas already imported 17 times). Afterpnpm --filter @objectstack/lint build, the CJSrequireofdist/index.cjsand the ESM import both load and run the rule. CI runs both gates over the full build.check:docs-transcript-driftfirst exited 3 (lint unbuilt). It exited 0 after the lint build.eslint.config.mjs: both files match**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}andpackages/**/*.{ts,tsx,mts,cts}, and neither is underNEVER_LINTED. ②npx eslint --no-inline-config --format jsonon the two changed source files: 2 files, 0 errors, 0 warnings. ③ The config never enables type-aware linting (eslint.config.mjs:327–:328: noparserOptions.project, no typed rules), and every block is per-file syntactic, so this diff cannot move any untouched file's verdict.Acceptance notes
action-name-undefined, with a message that says it IS defined and names itslocations. A new rule id would be a new public export from@objectstack/lint(a widening), and the claim declaresClause-②: no (narrowing).content/docs/ui/actions.mdx:276still saysrecord_relatedis "Declared, not yet placed: the console does not draw it on those rows yet". At the pin, the related list does draw it (relatedListActions.ts:143; objectui#11270 merged asa8b9889332, behind the pin by 0). The same page's "Surfaces can also reference actions by name" list (:279) does not listrecord:related_list.actions. Neither is in this card's file surface. Carrier: none named.pagewrites: its suite member keeps the defaultflowruntime type.Generated by Claude Code