Skip to content

fix(lint)!: action-name-undefined resolves record:related_list action ids against the child object - #21626

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20936-related-list-action-refs
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20936-related-list-action-refs

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20936
Clause-②: no (narrowing)

What

action-name-undefined (packages/lint/src/validate-action-name-refs.ts) now walks record:related_list → properties.actions[], scoped to that component type, and asks the two questions the console's renderer asks:

  1. Is the id an action of the related list's child object? The child's actions are the ones written on that object plus every stack.actions entry bound to it by objectName (the set defineStack merges into the object's actions, 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.
  2. Does that action declare a location a related list draws? The location set is read from the spec's ACTION_LOCATIONS (imported from @objectstack/spec/ui), classified per member in a Record keyed by the spec's ActionLocation type. That gives list_toolbar, list_item and record_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 severity error. Each is reported at the id's authored index. Only string elements are ids (inline objects are skipped, as on page:header). The child object is the component's bound dataSource.object when one is set, otherwise properties.objectName. The walk says nothing about a child object this stack does not define.

Measurements against the PM's mechanism assumptions

Read at origin/main f97660cdd6 (this branch's base) and at objectui 89cad75d55702cc4f267bead5bf267de575d5842 (.objectui-sha on that base).

  1. Holds. The rule walked record:quick_actions.actionNames[], record:alert's action.actionName and page:header's actions[]. The docblock at :27–:30 said actions "is declared separately on record:related_list", and no walk read it. That sentence is replaced by a bullet for the new walk.
  2. Holds. At the pin, packages/plugin-detail/src/renderers/record-related-list.tsx:287 reads schema.actions. :290 looks up useMetadataItem('object', …) for the related objectName. :301 takes relatedObjectMeta.actions as the registry, and :303 hands both to placeAuthoredRelatedListActions. In relatedListActions.ts, :110 resolves the ids with resolveDeclaredActionIds against that registry. :141–:143 place by actionRendersAt at list_toolbar / list_item / record_related. An id that resolves but is placed at none of the three is refused as unplaced. Control: at the previous pin db11afd4967c, git grep -c "schema.actions" on the same file answers 0 (exit 1), while schema.relationshipField answers 3, so the grep reaches the file. git merge-base --is-ancestor f4ed2387e9 89cad75d5570 exits 0.
  3. No existing walk scopes to an object. Every walk resolves through collectActionNames, the union of stack.actions and every object's actions. The related list's child is properties.objectName, the same key validate-page-field-bindings.ts's relatedListFieldRefs already reads. objectui's data-source gate (packages/react/src/element-data-source/ElementDataSourceGate.tsx:398) writes a bound dataSource.object over 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.
  4. Partly disproved. ACTION_LOCATIONS (packages/spec/src/ui/action.zod.ts:663) is the full six-location vocabulary and lists record_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 a record_header-only action that the renderer refuses. So the rule imports the vocabulary and the ActionLocation type 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's open_questions. This PR does not decide it.
  5. Holds. The landing site is packages/lint. There is no producer-side change: the spec already types the key as z.array(z.string()), and the renderer already refuses.

Pins (beside #20105's)

validate-action-name-refs.test.ts, describe('validateActionNameRefs — record:related_list actions'):

  • ids that resolve on the child at record_related, list_toolbar, list_item, plus one bound through stack.actions objectName: silent;
  • an id that resolves nowhere: one finding at its authored index, naming the id and the child object;
  • decision pinned: an id defined only on the page's object, and one defined only as a global action: both are findings, because the list never reads either;
  • child actions placed at record_header only, at [], and with no locations: three findings;
  • a bound dataSource.object wins over objectName;
  • a child object this stack does not define (sys_member): silent.

Tests (all at b37812bed9 unless 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 (at 6a73a0ad67, same src as the head): exit 0. The test layer is clean against its ledger: "2 file(s) / 6 error(s) / 2 pinned signature(s) held", unchanged.
  • Built-dist probe: require('packages/lint/dist/index.cjs').validateReferenceIntegrity(stack) on a related list naming one defined and one undefined child id returns exactly one action-name-undefined error at …properties.actions[1]. The ESM entry gives the same count. This is the suite os validate / os lint / os build call.
  • Corpus: there was no fixture to re-read. The platform's pages (sys-user, sys-organization, sys-position, walked with walkPageComponents) hold 13 related lists, and 0 of them author actions. git grep -c related_list -- examples (CHANGELOGs excluded) answers 0 files, exit 1. Control: record:quick_actions|page:header hits 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)

  • Deleting record_related from the classification (node scripts/ablation-replace.mjs … --delete -- pnpm --filter @objectstack/lint exec tsc --noEmit): red with TS2741: Property 'record_related' is missing in type …. Restored to blob e0deed9272da, which equals HEAD.
  • Setting record_related: null and running the rule's test file: Tests 4 failed | 32 passed (36). All four new tests that use a record_related action go red; the stack-wide walks stay green. Restored to blob e0deed9272da, which equals HEAD.

Gates

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at b37812bed9, and reconciled with --ran: "62 derived famil(ies) accounted for — 60 run, 2 NOT-MEASURED". Every run family exited 0, including check-adr-0087-registration (the changeset carries not-required (no-migration-prescription)), check-changeset-no-major, check-empty-changeset, check:nul-bytes, check:doc-authoring, check:engine-double-contract and check: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").

  • NOT MEASURED: check:dual-build-cjs-loads and check:lean-entry-closure, reason: PREREQUISITE NOT MET, because they need the whole workspace built (85 packages have no dist/). Narrowed instead: the diff adds no new import specifier to @objectstack/lint (@objectstack/spec/ui was already imported 17 times). After pnpm --filter @objectstack/lint build, the CJS require of dist/index.cjs and the ESM import both load and run the rule. CI runs both gates over the full build.
  • check:docs-transcript-drift first exited 3 (lint unbuilt). It exited 0 after the lint build.
  • ESLint, narrowed and proven. ① Population, from eslint.config.mjs: both files match **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} and packages/**/*.{ts,tsx,mts,cts}, and neither is under NEVER_LINTED. ② npx eslint --no-inline-config --format json on the two changed source files: 2 files, 0 errors, 0 warnings. ③ The config never enables type-aware linting (eslint.config.mjs:327–:328: no parserOptions.project, no typed rules), and every block is per-file syntactic, so this diff cannot move any untouched file's verdict.

Acceptance notes

  • One rule id for both refusals. An id that resolves to a child action placed nowhere the list draws is reported as action-name-undefined, with a message that says it IS defined and names its locations. A new rule id would be a new public export from @objectstack/lint (a widening), and the claim declares Clause-②: no (narrowing).
  • Docs drift, not fixed here. content/docs/ui/actions.mdx:276 still says record_related is "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 as a8b9889332, behind the pin by 0). The same page's "Surfaces can also reference actions by name" list (:279) does not list record:related_list.actions. Neither is in this card's file surface. Carrier: none named.
  • The rule still does not run at the runtime publish door for page writes: its suite member keeps the default flow runtime type.

Generated by Claude Code

@github-actions github-actions Bot added the size/m label Oct 3, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 12 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/getting-started/build-with-claude-code.mdx (via list_item (symbol, a field of const object RELATED_LIST_DRAWS), record_header (symbol, a field of const object RELATED_LIST_DRAWS))
  • content/docs/protocol/objectui/actions.mdx (via list_item (symbol, a field of const object RELATED_LIST_DRAWS), list_toolbar (symbol, a field of const object RELATED_LIST_DRAWS), record_header (symbol, a field of const object RELATED_LIST_DRAWS), record_more (symbol, a field of const object RELATED_LIST_DRAWS), record_related (symbol, a field of const object RELATED_LIST_DRAWS), record_section (symbol, a field of const object RELATED_LIST_DRAWS))
  • content/docs/protocol/objectui/concept.mdx (via list_item (symbol, a field of const object RELATED_LIST_DRAWS))
  • content/docs/protocol/objectui/layout-dsl.mdx (via record_header (symbol, a field of const object RELATED_LIST_DRAWS))
  • content/docs/ui/actions.mdx (via list_item (symbol, a field of const object RELATED_LIST_DRAWS), list_toolbar (symbol, a field of const object RELATED_LIST_DRAWS), record_header (symbol, a field of const object RELATED_LIST_DRAWS), record_more (symbol, a field of const object RELATED_LIST_DRAWS), record_related (symbol, a field of const object RELATED_LIST_DRAWS), record_section (symbol, a field of const object RELATED_LIST_DRAWS))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via record_more (symbol, a field of const object RELATED_LIST_DRAWS))
  • content/docs/releases/v17/17-6.mdx (via record_related (symbol, a field of const object RELATED_LIST_DRAWS))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 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 045b946256d988653fdca185c7fd33d6d86bd78d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e4be8b61164bddc24c0c5e6cf37b56b65578ed9c — the merge of head b37812bed9f9a02ca545c7a236e3d72173991894 into base 045b946256d988653fdca185c7fd33d6d86bd78d, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 045b946256d988653fdca185c7fd33d6d86bd78d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 20:23
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 20:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit 0fc8087 Oct 3, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20936-related-list-action-refs branch October 3, 2026 20:57
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

2 participants