docs(skills): data-hooks.md speaks ADR-0090 D3 vocabulary - #15186
Merged
Conversation
The published catalog page stated the `session.roles` retirement three times and used the reserved word "role" as live vocabulary six more times. D3 makes "role" reserved-forbidden (capability = permission_set, distribution = position, hierarchy = business_unit); this page ships verbatim to third-party projects via `npx skills add`, so every live use teaches an AI reader the reserved word. Kept: the one retirement note (the `ctx.session` table row) and the one useful warning (never write `ctx.session?.roles?.includes`). Rewritten: the duplicate retirement note in the `HookContext` comment folds into "privilege is judged by the security service, never by a session claim"; the masking callout says "for a permission set or position"; the masking comment spells the rest in D3 vocabulary. The cross-object example no longer queries a field named `role` -- `sys_user.role` does exist as a legacy better-auth admin scalar, but ADR-0068 D2 stopped synthesizing it and its only writer was retired, so an example looking up "the admin" by it teaches authorization off a dead field; it now resolves a related user by id, which is what a hook actually does. Residue: 3 occurrences, all quoted history, which is what the follow-up baseline entry covers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
This was referenced Sep 4, 2026
os-zhuang
approved these changes
Sep 4, 2026
os-zhuang
marked this pull request as ready for review
September 4, 2026 05:33
os-zhuang
enabled auto-merge
September 4, 2026 05:33
os-steve
added a commit
that referenced
this pull request
Sep 4, 2026
Brings in PR #15186 (#15177), which rewrote skills/objectstack-data/references/data-hooks.md into ADR-0090 D3 vocabulary, leaving 3 quoted-history occurrences of the reserved word where this branch's widened scan first met 12. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
os-steve
added a commit
that referenced
this pull request
Sep 4, 2026
The maintainer authorised the MAINTAINER-ONLY `--update` for this page's residue and for this PR only, after PR #15186 rewrote the page into ADR-0090 D3 vocabulary and left 3 occurrences that cannot be reworded without falsifying what they document. Both are quoted pre-rename history — D3's own named legitimate KIND: line 350 **No role list** - `session.roles` was retired in 17.0.0: it was declared but never produced, so every read was undefined line 837 Never gate this on a session claim: ctx.session?.roles?.… Naming the retired identifier is the sentence's entire content; renaming it makes the sentence false. `--update` rewrites the whole baseline from the current tree, so the result was inspected key by key rather than skimmed. Exactly one entry was added and nothing else moved: entries 43 -> 44, occurrences 120 -> 123 (delta 3) ADDED skills/objectstack-data/references/data-hooks.md: 3 REMOVED (none) CHANGED (none) check-role-word: green, 236 files across 2 roots, ledger 44 file(s) / 123 occurrence(s). check-corpus-claim-drift: green, ledger unchanged at 2 file(s). Both population pins hold: 236 walked, 12 published reference pages, 0 under the generated content/docs/references/ tree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #15177
What this is
skills/objectstack-data/references/data-hooks.mdships verbatim into customerprojects via
npx skills add. ADR-0090 D3 makes "role" a reserved-forbidden word —capability is
permission_set, distribution isposition, hierarchy isbusiness_unit, collaboration isteam— with one documented carve-out, thebetter-auth
sys_member.rolefield. This page used the reserved word twelve times:five of them quoting the
session.rolesretirement, which it stated three separatetimes; six using the word as live vocabulary; one as an example query.
Maintainer ruling of 2026-09-04 on #15075 (comment 5535828546, corrected by
5535830096) chose option B: rewrite the page first, and admit only the residue to
the shrink-only baseline. That baseline entry is PR #15073's to make, not this PR's.
Occurrence list, before and after
Re-measured on the merge base
efb35131withgrep -nowiE 'roles?'— the samewhole-word set the gate's own pattern (
\brole(?:s)?\b, case-insensitive) matches,so this list and the gate's count are the same measurement.
Before — 12 occurrences across 8 lines:
**No role list**…session.roleswas retired in 17.0.0No `roles` here (retired in 17.0.0)where: { role: 'admin' }`session.roles` was declared for yearsctx.session?.roles?.includes(…)After — 3 occurrences across 2 lines:
**No role list**…session.roleswas retired in 17.0.0 — the retirement note, quoted historyctx.session?.roles?.includes(…)— the warning, quoting the API that never existedThe page now states the retirement once and warns once, which is the target the card
set. Both survivors are quoted history: they name a thing that was declared and
retired, which is the only way to warn against it.
Each rewrite
1. Line 350 — the
ctx.sessiontable row: unchangedKept verbatim. This is the one legitimate history statement, and the two rewrites
below both point back to it rather than restating it.
2. Lines 574-575 — the
HookContextinterface commentThe retirement note's second copy. Folded into one sentence, no reserved word;
the fact itself stays on the table row above.
3. Line 733 — the cross-object example query
A correction to the card's stated reason. The card says "no
sys_userfield isnamed
role". That is not so:packages/platform-objects/src/identity/sys-user.object.tsdoes declare one (
role: Field.text({ label: 'Platform Role', readonly: true, … }),the better-auth
adminplugin scalar). The disposition is unchanged, and the realfact argues for it harder than the stated one did: ADR-0068 D2 stopped synthesizing
that scalar, and its only writer — the
set_user_roleaction — was retired(the removal note sits at
sys-user.object.ts:282, and points atsys_user_permission_set/admin_full_accessas where platform-admin membershipactually lives). So the field exists but is a dead legacy column, and an example
that looks up "the admin" by it teaches authorization off a field the platform no
longer maintains. It is also not the D3 carve-out: the carve-out is
sys_member.role, and this query is not that field.Choice of replacement, on the four axes:
to demonstrate that
whereis the canonical key. What a hook actually does withctx.api.object(...).findOneis resolve a related record (an owner, a manager)to read its fields. Looking up "the admin" is not a measured usage; resolving a
related user by id is.
idlookup is contract-stable. A lookup by a vendorscalar the platform stopped synthesizing is a workaround fossilized into docs.
axis.
where: { role: 'admin' }in a hooks reference is the seed ofif (admin) { ... }authorization code in a customer project. Anidlookupcannot be misread as an authorization decision.
other offered option, and it is rejected on this axis: the page documents no
permission-set API on
ctx, so writing one into an example would advertise acapability this page cannot back — declared but not delivered. The minimal real
field is the smaller, honest change.
The object name
useris left as it is: this page uses tutorial-generic objectnames throughout (
account,contact,task,audit_log), and changing it isoutside this card's file-and-line surface.
4. Line 822 — the static-masking callout
D3 vocabulary. The paragraph re-wraps because the replacement phrase is longer than
what it replaces; the line count is unchanged at 5 and no content is bought by the
re-wrap.
5. Lines 837-842 — the masking-handler comment
The retirement note's third copy plus four live uses. The one useful warning is
kept — do not write
ctx.session?.roles?.includes— and the rest is spelled in D3vocabulary. The retirement fact is not restated; the comment points at the table row
instead. Six lines become five.
"never exempts anyone" replaces "looked role-aware and was not" because it states
the actual runtime consequence: the optional chain yields
undefined, which isfalsy, so the exemption never fires and everyone is masked.
Residue, measured with the widened walker
PR #15073 widens this gate to walk the published catalog's
references/pages —referencesleavesSKIP_DIRS, andcontent/docs/referencesis excluded by pathinstead. That change is not in this PR. It was applied in a throwaway detached
worktree at this branch's head, measured, and the worktree destroyed; the mutation
was confirmed on disk before each run (the removed literal at 0 hits, both injected
fragments at 1 hit each,
git diff --statshowing the file).data-hooks.mdcountefb35131(page before this PR)7add396fBoth runs exit 1, which is correct and expected: with the walker widened the page is
in the walk for the first time and is not yet in
scripts/role-word-baseline.json.Adding that entry is PR #15073's authorized act, for the residue only, and
scripts/role-word-baseline.jsonis untouched here.The residual three, quoted from the branch:
Without the walker change,
pnpm check:role-wordat this head is green and does notsee the page at all —
Scanned: 224 .md/.mdx file(s) read across 2 root(s) — content/docs 190, skills 34, with thereferences/trees skipped by name.Ratchet arithmetic
scripts/check-skills-token-ratchet.mjsis the per-file budget for the publishedcatalog. There is no per-file line ceiling over
skills/**:scripts/pm/check-skill-line-ratchet.mjsprices.claude/skills/**only. It was runanyway and is green.
data-hooks.mdlinesdata-hooks.mdtokensdata-hooks.mdceilingdata-hooks.mdheadroomobjectstack-datapackage linesobjectstack-datapackage tokensNet shrink in both currencies, so nothing is bought and no ceiling moves.
Gate verdict after:
✓ check-skills-token-ratchet: skills/objectstack-data/references/data-hooks.md is 9674 tokens (ceiling 12611; headroom 2937).One deferral, flagged rather than taken. The ratchet's shrink-only discipline
says a ceiling "may be LOWERED by any PR that shrinks its file" and calls lowering
"always legitimate and encouraged". Banking this shrink would mean lowering the row
to 9674 — but that row lives in
scripts/check-skills-token-ratchet.mjs, and thiscard's file surface is the one page, no script. Left for whoever owns that call; the
headroom is printed on every gate run, so it is not hidden.
Gates
Derived after the commit with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths — the tool reads its own change setfrom the merge base), which returned 20 commands, plus the three the dispatch named
by hand. Every exit code was captured by redirecting to a file first and reading
$?before any pipe. All run at head7add396f, the commit this PR pushes.check-ci-filter-paritycheck-closing-keyword-paritycheck-closing-keyword-parity --self-testcheck-comment-mask-corpuscheck-skills-token-ratchetcheck-skills-token-ratchet --self-testcheck:doc-formula-expressionscheck:skill-refspackages/speccheck:agent-test-spellingcheck:corpus-claim-driftcheck:cross-package-test-inputscheck:doc-authoringcheck:nul-bytescheck:pm-governed-mergescheck:refd-timer-probecheck:role-wordcheck:skill-compatibilitycheck:skill-frame-synccheck:skill-identifier-livenesscheck:watch-hint-literalcheck:pm-skill-id-lintcheck:pm-skill-ratchet.claude/skills/**, not this page)pnpm linteslint . --no-inline-config, no narrowing, cleancheck:doc-formula-expressionsfirst returned exit 3, which is that gate's ownPREREQUISITE NOT METcode and explicitly not a finding —@objectstack/formulaand
@objectstack/lintwere unbuilt in a fresh worktree. Both were built and thegate re-run to the real verdict above.
Also carried out:
grep -naPcontrol-character sweep over the edited file, clean.Scope
One file, one commit.
scripts/role-word-baseline.jsonuntouched; no scripttouched; no other page touched. Governed surface, so this stays a draft for the
dispatching seat to review at the contract tier and request the approvers.
skip-changesetapplies — nothing underskills/**is published by a changeset.Related cards, none of them acted on here: PR #15073 lands after this page and
remains open until then; #15061 remains open behind it; the ruling on #15075 is
already recorded there.
Generated by Claude Code