Skip to content

fix(plugin-sharing,plugin-webhooks): by_object lists sort by object instead of grouping on its API name - #24733

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-24701-by-object-group-headers
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-24701-by-object-group-headers

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #24701
Clause-②: no

This is half (a) of the card. sys_sharing_rule.by_object and sys_webhook.by_object stop heading each group with an object API name. Both views now sort by object instead of grouping on it, the same route PR #24698 took for sys_record_share.by_object. Half (b), the record share's heading, is measured and analysed only, with no edit, and the card stays open for its decision. #24701 remains open after this PR lands.

Responsibility: the sharing-rule and webhook list views' grouping (plugin-sharing, plugin-webhooks) | a served surface names the target object by its label, never by its API name (the #24375 route; no console-side mapping) | admins reading Setup → Sharing Rules and Setup → Webhooks.

Readings are against objectstack d20bf9b09 (this branch's base) and objectui at the console pin .objectui-sha = 12dc7b47c3, by git show and git grep. No browser reading: there is no console build in this container.

Premise, measured

  • Both views grouped on object_name. sys-sharing-rule.object.ts and sys-webhook.object.ts declared grouping: { fields: [{ field: 'object_name', … }] } on by_object. object_name is a Field.text with widget: 'object-ref' on both objects, so it stores an object API name.
  • The group header draws that value as stored text. In objectui at the pin, ObjectGrid's groupValueFormatter maps a select value to its option label and a boolean to Yes / No, and returns nothing for anything else. useGroupedData then uses the raw value as the header label, and GroupRow draws group.label. Server-side grouping hands back raw values too (ListViewGroupHeaderRow). So each group read crm_account, showcase_announcement and so on.
  • The cell already names the object by its label. objectui#12613 (an ancestor of the pin) registers a read face for widget: 'object-ref' (ObjectRefCellFace): the object's icon and its translated label. The Object column's cells were labelled, and the header above them was not.

Route: option (ii), sort instead of group

  • (i) Group on a column that carries the label: none exists. Neither object has a field that stores the target object's label. The object-ref face labels a cell at render time, but the group header does not use cell faces. So any grouping key draws stored text.
  • (ii) Drop the grouping and keep the sort: chosen. sort: object_name asc, label asc is unchanged, so one object's rows stay together. The Object column stays first, and its cell draws the object's icon and label.
  • (iii) Keep the grouping: this leaves the card's defect in place.

Four axes:

  • Real business need. The views answer "which rules or webhooks act on each object". The sort keeps that answer together, and the labelled Object cell names each object on every row. What this gives up is the collapsible header and its count. No nav entry opens these views: nav_sharing_rules and nav_webhooks open the object's first view (active).
  • Long-term soundness. No new column, no mapping, and no console change. A grouping can come back once a label-bearing grouping key is served, or once the console's group header draws a widget-hinted field through its face (see Acceptance notes).
  • Making AI mistakes harder. The new pins refuse a grouping on any field that stores an object API name: an object-ref field, or a pointer pair's referenceVia type half. The sharing pin covers every object objects/index.ts ships, so it also covers sys_record_share and sys_share_link.
  • Startup focus. One key removed per view. No new gate.

Changes

File Change
packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.object.ts by_object loses grouping; the sort and columns are unchanged
packages/plugins/plugin-webhooks/src/sys-webhook.object.ts by_object loses grouping; the sort and columns are unchanged
packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.by-object.test.ts new pin, 3 cases
packages/plugins/plugin-webhooks/src/sys-webhook.by-object.test.ts new pin, 3 cases
.changeset/24701-by-object-group-headers.md patch for @objectstack/plugin-sharing and @objectstack/plugin-webhooks

Not touched: sys-record-share.object.ts, packages/spec/**, and every stored column, index and query. Also not touched: the #15194 epic's files (sharing-rule-service.ts, bootstrap-declared-sharing-rules.ts, sharing-plugin.ts, declared-webhooks.ts, webhook-provenance.ts, webhook-outbox-plugin.ts).

Pins

Each pin file has three cases:

  • Enumeration with a non-empty guard. It first asserts which fields store an API name: object_name on each sharing object, and object_name on sys_webhook. So the refusal below cannot pass on an empty set.
  • The refusal. No list view groups on such a field.
  • A control. by_object still sorts first on object_name asc and still shows object_name as its first column. The sharing control also asserts that object_name keeps its object-ref widget, the cell face's trigger.

Ablation. Each leg ran once at 7fa022b59, under scripts/pm/os-verify-lock.sh, through node scripts/ablation-replace.mjs in wrap mode, with a shell trap restoring HEAD on EXIT, INT and TERM. The anchor was pagination: { pageSize: 100 }, (1 hit in each file). The replacement put grouping: { fields: [{ field: 'object_name', order: 'asc', collapsed: false }] } back in front of it. The tests import the object files by relative source path, and @objectstack/spec was built, so no rebuild sits between a mutation and its run.

Mutation File Landed Red Restored
put the object_name grouping back into by_object sys-sharing-rule.object.ts anchor 1 → 0, grouping 0 → 1, blob 5597d023128a → 23c624a69766 no list view groups on a field that stores an object API name: received [ "sys_sharing_rule.by_object: object_name" ] · Tests 1 failed / 2 passed blob == HEAD 5597d023128a, git diff HEAD empty
put the object_name grouping back into by_object sys-webhook.object.ts anchor 1 → 0, grouping 0 → 1, blob 1e3bf6397b97 → a946ffa35dc7 same case: received [ "sys_webhook.by_object: object_name" ] · Tests 1 failed / 2 passed blob == HEAD 1e3bf6397b97, git diff HEAD empty

Verification

All readings below are on head 7fa022b59, the final commit. Exit codes were captured before any pipe. Builds and tests ran under scripts/pm/os-verify-lock.sh.

  • Build. The dependency closures (--filter '@objectstack/plugin-sharing^...' and --filter '@objectstack/plugin-webhooks^...'), then both packages: exit 0.
  • Tests. pnpm --filter @objectstack/plugin-sharing test: exit 0, Test Files 50 passed (50), Tests 1172 passed (1172) (1169 + the 3 new). pnpm --filter @objectstack/plugin-webhooks test: exit 0, Test Files 20 passed (20), Tests 215 passed (215).
  • Typecheck. pnpm --filter @objectstack/plugin-sharing typecheck and pnpm --filter @objectstack/plugin-webhooks typecheck: exit 0. check:test-typecheck holds both ledgers unchanged: sharing 2 files / 3 errors, webhooks 0 / 0. tsc -p tsconfig.test.json --listFiles lists both new pin files, so the test layer compiles them. The sharing program's 3 errors are the ledgered ones, in position-graph.test.ts and sharing-rule.test.ts, none in the new file.
  • Derived gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths after the last commit, gave 67 commands. 65 exited 0 on the first pass. pnpm check:i18n first exited 3 (prerequisite not met: no CLI build). Its own prescribed turbo build was run, and the rerun exited 0: check-i18n-bundles: OK (10 package(s) — all bundles in sync, no undeclared authoring keys).
  • NOT MEASURED: pnpm check:dual-build-cjs-loads. Reason: exit 3, prerequisite not met. It reads every package's dist/ (54 packages unbuilt here), which needs a whole-workspace build. This diff changes no package entry, export or build config. CI's Lint & Repo Gates runs it.
  • --ran reconciliation. Run reconciliation — 67 derived, 66 run, 1 NOT-MEASURED, 0 UNRUN, exit 0.
  • Declared to CI, not run locally: test:repo (no repo-project test or derived list names this diff), pnpm lint, the type-check lanes, Build Core and the full Test Core shards.

Half (b), measured only (no edit)

A record share's heading is recipient_id (nameField, since PR #24698), a Field.text with no widget and no reference.

  • What is stored. Two call sites write sys_record_share, both in SharingService.grant() (sharing-service.ts, the update at :1550 and the insert at :1579). grant() refuses every recipientType except user (ADR-0111 D7). The rule evaluator calls it with recipientType: 'user' (sharing-rule-service.ts:2177), expanding team, business-unit and position recipients into one row per user. So every row written at d20bf9b09 is a user grant, and its recipient_id is a sys_user id. The read gates match only recipient_type: 'user' (sharing-service.ts :589, :665, :877). The select still declares user, group, position and unit_and_subordinates, held equal to RecordShareRecipientType (packages/spec/src/contracts/sharing-service.ts:104). No writer produces the last three. No example app seeds the object.
  • Is there a recipient-type field? Yes: recipient_type, a required select defaulting to user.
  • The console at the pin.
    • The H1 is recordDisplayValueAt(data, declaredNameField(…)) in page:header (containers.tsx). That is the stored text, so the H1 reads a raw user id. A field's widget face is not consulted.
    • With no widget, the Recipient column of all_shares, granted_by_me, by_object, manual_grants and rule_grants draws the same raw id.
    • sys_sharing_rule.recipient_id declares widget: 'recipient-picker'. Its face (RecipientCellFace, objectui#12613) names a user id by an exact-id sys_user read, keyed by the row's recipient_type. group is not a kind it knows, so a group row would draw as stored text.
  • How the console would label a pointer pair here. resolveRecordPointer (packages/fields/src/index.tsx:3291) takes the referenceVia sibling's stored value as the object name. recipient_type stores user, not sys_user, so a pair on it would ask for an object called user. A declared pair needs a sibling column that stores an object name. Even then, the H1 draws the title field's stored id, and the Details tab hides the title field, so its pointer card would not draw.
  • ADR-0079. lookup and user are title-ineligible types (packages/spec/src/data/display-name.ts). A formula with a text result is eligible.

The options and the four-axis analysis are in the dev report on #24701. No edit was made to sys-record-share.object.ts.

Acceptance notes


Generated by Claude Code

…nstead of grouping on its API name

sys_sharing_rule.by_object and sys_webhook.by_object grouped on
object_name, and the console heads a group with the stored value, so
each group read an object API name. Both views keep their object_name
sort, which holds one object's rows together, and their Object column,
whose object-ref cell draws the object's label. A pin beside each
object refuses a grouping on a field that stores an object API name.

Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-sharing, @objectstack/plugin-webhooks, touching 2 documentable anchor(s).

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

  • content/docs/automation/webhooks.mdx (via object_name (literal, a string literal in listViews))
  • content/docs/data-modeling/object-extensions.mdx (via listViews (symbol, a field of const object SysSharingRule; a field of const object SysWebhook))
  • content/docs/deployment/cli.mdx (via object_name (literal, a string literal in listViews))
  • content/docs/getting-started/build-with-claude-code.mdx (via listViews (symbol, a field of const object SysSharingRule; a field of const object SysWebhook))
  • content/docs/kernel/contracts/metadata-service.mdx (via listViews (symbol, a field of const object SysSharingRule; a field of const object SysWebhook))
  • content/docs/kernel/runtime-services/audit-service.mdx (via object_name (literal, a string literal in listViews))
  • content/docs/kernel/services-checklist.mdx (via listViews (symbol, a field of const object SysSharingRule; a field of const object SysWebhook))
  • content/docs/permissions/record-view-auditing.mdx (via object_name (literal, a string literal in listViews))
  • content/docs/protocol/kernel/http-protocol.mdx (via object_name (literal, a string literal in listViews))
  • content/docs/protocol/kernel/i18n-standard.mdx (via listViews (symbol, a field of const object SysSharingRule; a field of const object SysWebhook), object_name (literal, a string literal in listViews))
  • content/docs/protocol/kernel/plugin-spec.mdx (via listViews (symbol, a field of const object SysSharingRule; a field of const object SysWebhook))
  • content/docs/protocol/objectui/layout-dsl.mdx (via listViews (symbol, a field of const object SysSharingRule; a field of const object SysWebhook))
  • content/docs/ui/actions.mdx (via listViews (symbol, a field of const object SysSharingRule; a field of const object SysWebhook))
  • content/docs/ui/forms.mdx (via listViews (symbol, a field of const object SysSharingRule; a field of const object SysWebhook))
  • content/docs/ui/views.mdx (via listViews (symbol, a field of const object SysSharingRule; a field of const object SysWebhook))

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

  • content/docs/releases/v17/17-5.mdx (via listViews (symbol, a field of const object SysSharingRule; a field of const object SysWebhook))
  • content/docs/releases/v17/17-6.mdx (via listViews (symbol, a field of const object SysSharingRule; a field of const object SysWebhook), object_name (literal, a string literal in listViews))

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 — 10 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 0904d856e31fd065c71af725cf3ad4ad0363dc76 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 97c8167b6d834257636b543f33babd5890946add — the merge of head 7fa022b591b0bdd4ac803ad0748d1e24eab071bc into base 0904d856e31fd065c71af725cf3ad4ad0363dc76, 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 97c8167b6d834257636b543f33babd5890946add && git checkout 97c8167b6d834257636b543f33babd5890946add
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0904d856e31fd065c71af725cf3ad4ad0363dc76 7fa022b591b0bdd4ac803ad0748d1e24eab071bc && git checkout -B drift-repro 0904d856e31fd065c71af725cf3ad4ad0363dc76 && git merge --no-ff 7fa022b591b0bdd4ac803ad0748d1e24eab071bc

node scripts/docs-audit/affected-docs.mjs --json 0904d856e31fd065c71af725cf3ad4ad0363dc76

⚠️ 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 0904d856e31fd065c71af725cf3ad4ad0363dc76 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #24701 half (a) · domain:services seat 2 · os-warren · session_01LR1cD9JqgkeqY7k9rrZH8s · 2026-10-20T03:27Z · head 7fa022b591, the review of record for claim 6281802681.

  1. Form:
    • Draft, base main. The first line is Part of #24701, and the body carries no closing keyword beside an issue number (seat grep: 0 hits).
    • Clause-②: no on the body and on the changeset's line.
    • One patch changeset for @objectstack/plugin-sharing and @objectstack/plugin-webhooks.
    • 5 files, +158/−2. The card stays open for half (b).
  2. Premise, measured:
    • The diff. The seat's own read: each by_object view loses its grouping key and nothing else. The object_name, label sort and the columns are unchanged, and each object gains a one-paragraph reason. sys-record-share.object.ts, packages/spec and the [epic] ADR-0131 — total organization ownership: no NULL organization_id (v18 line) #15194 epic's files are untouched.
    • The group header. The seat read objectui at the console pin 12dc7b47c3. ObjectGrid's groupValueFormatter builds its labels from the field's options alone and has no widget arm. So a grouping on an object-ref field heads each group with the stored API name, as the card says.
    • The pins. Two new pins refuse a grouping on any field that stores an object API name: an object-ref widget, or a referenceVia type half. The sharing pin runs over every object in objects/index.ts, and each pin has a non-empty-set guard and a control case.
    • Ablation. One leg per file put the grouping back. Each went red in its own case (1 failed, 2 passed) and was restored to HEAD's blob with git diff HEAD empty.
  3. Gates:
    • CI on 7fa022b591 at this read: 40 check-runs, 34 success, 6 skipped, 0 failed; mergeable clean. The six skips are path-filtered or opt-in: Dogfood Regression Gate, Dogfood Verify CLI, Build Docs, Console Pin Gate, Temporal Conformance, Packed-tarball smoke. check-expected-skips.mjs could not run in the seat's container (its yaml import is not installed in the shared checkout, which the seat does not touch), so the seat judged these by name.
    • Local on the head: 67 derived, 66 run, 1 NOT-MEASURED, 0 UNRUN (--ran exit 0).
      • check:dual-build-cjs-loads needs a whole-workspace build. CI's Lint & Repo Gates runs it, and the diff touches no entry, export or build config.
      • check:i18n exited 0 on its rerun, after its own prescribed build.
    • Package suites:
      • plugin-sharing: 1172 tests.
      • plugin-webhooks: 215 tests.
      • Both typechecks exit 0, and the test-typecheck ledgers are unchanged.
    • The seat's diff read: both test files are new, so no existing expect( line is removed.
  4. Accepted deviations:
  5. Out of scope:
    • Half (b) (the record share's heading and Recipient column): ruled by the seat in 6282390889 (route B, a display declaration). It is this card's next dispatch, after this PR lands.
    • Acceptance notes item 1 (the console's group header does not use cell faces): Acceptance notes. After this PR, no served objectstack view groups on such a field, and the pins hold that. carrier: none.
    • Acceptance notes item 2 (the Recipient column's raw id): carried by half (b) above.
  6. Landing: ordinary queue landing (check-governed-merges: not governed, 160 lines). When every check on the current head is green, the seat sends pr_ready + automerge_enable through the relay, then confirms MERGED. The card keeps pm:dispatched for half (b).

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 20, 2026 03:29
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 20, 2026 03:29
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit 39e93b9 Oct 20, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24701-by-object-group-headers branch October 20, 2026 03:59
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.

2 participants