Repository navigation
feat(spec): userActions.edit/delete accept per-record CEL predicates (objectui#2614) - #3076
Merged
Merged
Conversation
…(objectui#2614)
Extend the object-level userActions.edit / userActions.delete flags from
plain booleans to a union with an object form
{ enabled?, visibleWhen?, disabledWhen? } (RowCrudActionOverrideSchema),
so the built-in row Edit/Delete affordances can be hidden or disabled per
record via CEL predicates — the same evaluation contract custom actions
already use. Semantics: visibleWhen false → not rendered (fail-closed);
disabledWhen true → rendered disabled (fail-soft). The predicates are
advisory UI gating; server enforcement stays with permissions/hooks.
resolveCrudAffordances() keeps returning the resolved booleans (enabled
falls back to the managedBy bucket default) and now surfaces the
predicates as editPredicates / deletePredicates, pass-through as
authored. Boolean-only inputs produce byte-identical output — zero
behavior change for existing schemas.
clampManagedObjectWrites (ADR-0092 D2 hint clamp) treats the object form
by its explicit enabled flag only: per-record predicates are not a write
grant, so a managed object stays fail-closed unless enabled === true.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnCZhYpQjRyg2E44RHBiNE
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 100 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnCZhYpQjRyg2E44RHBiNE
…e exports Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnCZhYpQjRyg2E44RHBiNE
…58 expression ledger The new data/object.zod.ts:visibleWhen / :disabledWhen surfaces (objectui#2614) tripped the conformance ratchet. Two rows, matching their actual fail policies: visibleWhen is fail-closed (a faulting predicate hides the row button), disabledWhen is fail-soft-log (a faulting predicate leaves it enabled; server hooks are the boundary). Both interpret on the canonical celEngine via objectui useRowPredicate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnCZhYpQjRyg2E44RHBiNE
… (objectui#2614) A PAID invoice's fields already freeze via readonlyWhen; the built-in row actions now follow the same truth: Edit renders DISABLED on paid rows (disabledWhen), Delete is HIDDEN outright (visibleWhen). Draft and sent invoices keep the untouched menu. Browser-verified against the objectui HMR console: paid row menu = greyed Edit only; sent row menu = enabled Edit + Delete. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnCZhYpQjRyg2E44RHBiNE
os-zhuang
marked this pull request as ready for review
July 16, 2026 16:44
This was referenced Jul 17, 2026
os-zhuang
added a commit
that referenced
this pull request
Jul 17, 2026
content/docs/references/** is generated from packages/spec and committed, but no CI job ever regenerated and diffed it, so the public reference docs drifted silently while main stayed green. #3076 added RowCrudActionOverride to the spec and the docs never learned the type existed. Regenerate: 7 files, every change traced to a spec change that shipped without re-running the generator — RowCrudActionOverride and ServiceSelfInfo missing outright, dashboard filterBindings/name missing, readonly (#2948/#3003) and allowTransfer (#3004) stale, and connector ADR-0096 → ADR-0097 (both ADRs exist and are distinct, so the published docs were pointing readers at the wrong one). Verified deterministic: two consecutive runs produce byte-identical output. Gate: build-docs.ts --check, following the sibling convention. Every write goes through emit() and every wiped folder through manageDir(), so check and write run identical generation logic and differ only in the final disposition — it cannot pass on output a real run would not produce. Verified output-identical to the previous generator across all 258 files, and proven to fail on stale content, a missing page, a stale leftover page, and a vacuous no-schema run. Not `git diff --exit-code`: that misses untracked files. Placement: lint.yml's "TypeScript Type Check" — no paths filter and a required status check, so the gate can neither go dormant nor be merged past. ci.yml's "Build Docs" is gated on a `docs` filter excluding packages/spec/**, so it skips exactly the spec-only PRs that cause this drift. Also un-dormants the sibling gates: check:spec-changes / check:upgrade-guide read the ADR-0087 registries but ran under a filter listing only skills/**, and that filter watched content/docs/guides/skills.mdx, a path #2584 moved. Job renamed check-skill-docs → check-generated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 17, 2026
…icate union (objectstack-ai#7692) (objectstack-ai#7758) objectstack-ai#3076 (objectui#2614) gave `userActions.edit`/`delete` a boolean-or-predicates union so the built-in row affordances could be gated on record state. `create`/`import` were left as bare booleans with no other lever, so a child object's related-list [+ New] button could not be gated on the parent record's state while the row Edit/Delete beside it could. On a frozen parent the row actions grey out and [+ New] still renders; the server guard 409s the insert, so it is an affordance leak an app has no way to close. Both keys now take the SAME union — the same RowCrudActionOverrideSchema, not a new dialect. resolveCrudAffordances carries the predicates through as createPredicates/importPredicates alongside the existing edit/delete pair, via the same normalizeRowCrudOverride collapse. What `record.*` binds to differs between the two positions, and the schema says so rather than implying a symmetry it does not have: edit/delete evaluate per row against that row's own record; create/import gate a record that does not exist yet, so they evaluate once per toolbar against the record in scope — the host (parent) record on a related list, and nothing on a standalone object list, where a `record.*` predicate therefore hides the button under the fail-closed rule. plugin-hono-server tracks the widened producer: the /me/permissions managed-write clamp tested `create` with a bare `!== true`, which would have clamped away a legitimate `create: { enabled: true, visibleWhen: … }` opt-in. It now reads `create` through the same opt-in helper as edit/delete. The renderer half (related-list toolbar honouring create.visibleWhen) is objectui's downstream card and is not part of this change. Claude-Session: https://claude.ai/code/session_017aiuit7rFUsQWropgqtfnR Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…ned and URL-spelled citations, pinned in one spelling table (objectstack-ai#20989) Fixes objectstack-ai#20636 Clause-②: no The family closeout for the citation extractor in `scripts/check-issue-citations.mjs`: every spelling a seat measured as invisible to the diff gate and the census is now read, every exclusion keeps only the shapes it was measured protecting, and the self-test carries the one enumeration table the triage asked for (48 spellings, each "extracted as" or "not a citation, because"). Landing site: `scripts/check-issue-citations.mjs` only. `scripts/check-doc-authoring.mjs` is untouched; H2 below says why. ## What changed - **Hyphen after the number.** The lookahead no longer refuses a `-`, so `objectstack-ai#13398-class`, `objectstack-ai#5347-A` and `ui#6206-B` read as citations of their number. It still refuses a word character, so a hex colour stays out. - **Slash before the `#`.** A `/` is now valid context before a bare `#`. It stays refused only before a qualifier candidate, so a URL path fragment such as `https://example.com/docs/page#12` never reads `page` as prose. A `/` right after a digit is the exception, so `objectstack-ai#3076/objectui#2614` still reads its own qualifier. - **Slash-joined continuation.** In `#A/#B`, the second number takes the chain head's reading: bare after a bare or prose head, the head's repository after a qualified head, and an ordinal after an ordinal. Only a joined `/` continues a chain. `objectui#1 / objectstack-ai#2` and `objectui#1 + objectstack-ai#2` stay two separate readings. - **URL spelling.** `https://github.com/OWNER/REPO/issues/N` and `.../pull/N` are now citations, qualified by their own `OWNER/REPO`. That puts `objectstack-ai/framework` in this repository and makes every other repository's URL cross-repo, never a finding. Inside a markdown link `[#N](URL)`, the citation counts once. - **Head rows.** I retired `re-charter`, `clause` and `option` (named in the thread), plus `acceptance` and the section mark (found by the same measurement). Each protects 0 sites repo-wide at two or more digits and hides board citations. The grammar's two-digit floor already keeps one-digit ordinals out. I added a `](` row, the narrower guard the hyphen exclusion leaves behind for markdown in-page heading anchors. - **Self-test.** - A new `spellings` battery (56 cases) reads the table row by row. - Every head row must excuse at least one table row, and every required spelling (the card's list plus the thread's) must be present. - The `live-corpus` battery gains three floors, one per new arm, each counting only a number spelled once on its line. - The roster floor rises from 6 to 9 batteries. - Total: 114 cases in 8 batteries became 173 in 9. - I edited the header in place and kept its line count, because `scripts/pm/dispatch-gates.mjs`'s self-test pins `scripts/check-issue-citations.mjs:204 local-env`. The marker is still on line 204, and that pin passes (see Gates). ## H1: what each exclusion protected and hid Measured on `3693a1b50` over the declared surfaces. Every arm was judged against one enumerated board: 188 pages, frontier 20959, 2026-09-30T22:55Z. The instrument mirrors the gate's extractor and matched it file for file on all 2,640 files (0 mismatches). | exclusion | hid (sites, dead) | protected in the declared surfaces | disposition | |---|---|---|---| | hyphen after the number | 58, 1 dead (8 cross-repo) | 0: no numeric range, slug or hex-like token. Repo-wide: in-page heading anchors, 16 lines in `docs/design/**` and `skills/**`, plus one range, `docs/audits/...md`, whose first number is a citation | dropped; the `](` row keeps the anchor out | | `/` before the `#` | 528, 10 dead: 523 `#A/#B` second numbers and 5 `TOKEN/#N` such as `ADR-0049/objectstack-ai#1888` | 0 paths and 0 URL fragments | narrowed to the candidate arm; continuations read as their chain | | head `re-charter` | 0 left on this tree (the 26 dead `re-charter objectstack-ai#13135` were rewritten by PR objectstack-ai#20750) | 0; only the gate's own fixtures used it | retired | | head `clause` | 0 in the surfaces; 4 in deferred test files, all board citations | 0 | retired | | head `option` | 1 (`option objectstack-ai#14088`, live); 1 more under `scripts/**` | 0 | retired | | head `acceptance` | 1 (`the silent acceptance objectstack-ai#6132 closed`, live) | 0 at two or more digits | retired (in-place, below) | | head section mark | 0 in the surfaces; 4 in test files (`§6 objectstack-ai#11176's decisions`) | 0 at two or more digits | retired (in-place, below) | | heads kept | none measured hiding a citation | directive 203 (max 13), `PD` 85 (max 13), batch 276 (69 distinct, 11 to 227), `OQ` 10, `PKCS` 1 | kept | The 8 slash chains headed by another repository are not a case where the two populations cannot be told apart. The 5 on objectui's public board each name objectui's record, the issue and then the pull request that fixed it, read one by one against both boards: - `objectui#2715/objectstack-ai#2717` - `objectstack-ai#2711/objectstack-ai#2722` - `objectstack-ai#2725/objectstack-ai#2732` - `objectstack-ai#2967/objectstack-ai#2904` - `objectstack-ai#4648/objectstack-ai#4901` This repository's records with the same numbers are unrelated. The other 3 (`cloud`, `hotcrm-heimao`) are boards one credential cannot read, so they stay unjudged, as they were before. ## H2: where the URL spelling belongs The extractor. At `3693a1b50`, 57 URL sites sit in the gate's projection: 56 in package comments and 1 link on a release page. 4 of them are dead. Only 2 URL sites in package sources are inside string literals, both internal `note:` strings in `packages/runtime/src/route-ledger.ts`. `check:doc-authoring` asks a different question: may a runtime string carry a tracker reference at all? It reads string literals, skills and spec refusal messages. The two projections are disjoint, so adding the URL to the extractor double-counts nothing there. Inside the extractor, the one double-spelled site (`[objectstack-ai#15325](...objectstack-ai/issues/15325)` on `v17/17-3.mdx`) counts once. `check-doc-authoring.mjs` is not touched. ## H3: open PRs' added lines All 13 open PRs at 2026-09-30T23:2xZ: their heads were fetched into a private ref namespace (deleted afterwards). For each, the BASE extractor and this one were run over the lines it adds, against its merge base. Result: 85 added-line citations under both extractors, 0 newly extracted, 0 lost. No PR's verdict changes. Lines a PR does not add are never judged, which is unchanged and pinned in the `diff-scope` battery. ## Census, before and after The gate's own `--census --json`, once with the `3693a1b50` script and once with this one, over the same tree: | | judged | resolves | resolves as PR | cross-repo | allocated-but-absent | |---|---|---|---|---|---| | before (frontier 20965) | 37,152 | 33,403 | 1,985 | 1,024 | 740 | | after (frontier 20966) | 37,796 | 33,917 | 2,083 | 1,041 | 755 | That is 644 more judged sites and 15 more dead ones, with 0 findings lost. By arm: hyphen 1 dead, slash 10 dead, URL 4 dead. Newly visible dead sites per lane. I rewrote none of them; they belong to the lane cards: - **objectstack-ai#20594 (`domain:cli`): 1.** `packages/rest/src/rest-server.ts:7456`, objectstack-ai#11006. - **objectstack-ai#20595 (`domain:engine`): 6.** - `driver-sql`: `sql-driver.ts:3933` (URL, objectstack-ai#17590), `:12110` (objectstack-ai#10629), `:16106` (objectstack-ai#17343). - `metadata`: `loaders/ambiguous-metadata-stem.ts:40` (objectstack-ai#14423). - `metadata-protocol`: `migrations/partial-index-probe.ts:395` (objectstack-ai#16657). - `objectql`: `plugin.ts:1496` (objectstack-ai#10629). - **objectstack-ai#20596 (`domain:services`): 0.** - **objectstack-ai#20597 (`domain:spec`, `packages/lint`): 0.** - **objectstack-ai#20234 (`packages/spec/src`): 7.** - `data/datasource.zod.ts:701` (objectstack-ai#9040). - `data/filter.zod.ts:1040` (URL, objectstack-ai#17590) and `:1042` (URL, objectstack-ai#17286). - `data/value-roundtrip-conformance.ts:100` (URL, objectstack-ai#12380). - `ui/component.zod.ts:593` and `:669` (objectstack-ai#6276), and `:3771` (objectstack-ai#9972). - **Release pages (`domain:devx`, no lane card): 1.** `content/docs/releases/v17/index.mdx:168` (objectstack-ai#6075). This census was run on the PR head's tree, not after landing. A re-run after landing reads the same corpus plus whatever `main` has gained by then. ## In-place fixes beyond the three named rows I retired the `acceptance` and section-mark rows here rather than filing them. All four conditions hold: 1. They are the same defect class as `option` and `clause`: a head row hiding a board citation. 2. The fix is mechanical, and the shape is pinned by the table. 3. The file is this claim's own surface (`NON_CITATION_HEADS`). 4. The same gate's self-test covers them, so no new verification surface is added. Evidence: `acceptance objectstack-ai#6132` (live) in `packages/formula/src/cel-pushdown-limits.ts:82`, and the section-mark sites in deferred test files. Neither row has any two-or-more-digit ordinal anywhere in the repository. ## Gates (final head `25d96fcc0`) I re-derived the list with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, which gave 32 commands. The same derivation on a throwaway tree at `origin/main` (`05be35259`) with this diff applied gave an identical list. I ran all 32, plus `pnpm check:doc-authoring` and the self-test, and every one exited 0. Reconciliation verdict line: `✓ dispatch-gates --ran: 32 derived famil(ies) accounted for — 32 run, 0 NOT-MEASURED (a DERIVED zero — all 32 recorded an exit code and none of them is 3).` Verdict lines worth quoting: - `node scripts/check-issue-citations.mjs --self-test`: `✅ ... every spelling enumerated ... (173 cases, 9 batteries)`. It also passes on `origin/main` `05be35259` with this diff applied. - `node scripts/check-issue-citations.mjs` (diff mode): `✅ check-issue-citations: no issue citations added against 3693a1b (0 file(s) read).` The script lives in the deferred `scripts/**` surface. - `pnpm check:pm-dispatch-gates`: `✓ dispatch-gates self-test: 1976 cases pass.` (1237.9s). Its pin `scripts/check-issue-citations.mjs:204 local-env` holds. - `pnpm check:doc-authoring`: `✓ doc authoring guard: ... hold the baseline — 549 pinned site(s)`. - `pnpm check:nul-bytes`: `check-nul-bytes: OK (scanned 9582 text file(s) ...)`. - `node scripts/check-scripts-symbol-anchors.mjs`: `✅ ... 3706 anchors across 282 scripts resolve`. ## Ablations (one-shot, from committed `25d96fcc0`, via `scripts/ablation-replace.mjs`) Every leg was expected to go red, and every leg did. Each one restored to blob `c732ce2e21c7` (equal to HEAD) with an empty `git diff HEAD`. No permanent ablation file is left. | mutation | first red | |---|---| | hyphen refused again after the number | `the live corpus must yield a #N-word citation` | | `/` refused again before the `#` | `the live corpus must yield a slash-joined #A/#B second number` | | URL arm disabled | `the live corpus must yield a URL-spelled citation` | | `option` head row restored | `spelling option #N in "the option objectstack-ai#14088 gave" must read objectstack-ai#14088; got nothing` | | continuation disabled | `spelling repo#A/#B ... must read objectstack-ai/objectui#2711, objectstack-ai/objectui#2722` | | `](` row disabled | `a markdown link's in-page heading anchor is not a citation` | | link de-duplication disabled | `spelling [#N](URL) ... must read objectstack-ai#15325` | The first slash ablation, run before the floors were tightened, went red in the table but left the live-corpus continuation floor green. A line citing the same number twice let a plain citation stand in for the slash-joined one. Commit `25d96fcc0` makes each new floor count only a number spelled once on its line. The re-run above is red at that floor. ## Acceptance notes - **URL spelling in runtime strings.** `check:doc-authoring` does not read it. At `3693a1b50` the population is 2 internal route-ledger `note:` strings (`packages/runtime/src/route-ledger.ts:459`, `:465`), and no author-facing door shows them. Noted, not filed; carrier: none. - **Range second numbers.** In a range such as `objectstack-ai#712-714`, the second number carries no `#` and is not read. There is 1 site repo-wide, in `docs/audits/**`, outside the declared surfaces. Pinned in the table with its reason. - **Dormant test-file citations.** The 8 test-file citations the retired `clause` and section-mark rows hid are in the deferred test surface. They become visible only when that surface is swept. - **Line-number pin.** `dispatch-gates.mjs` pins this file's `local-env` marker by line number (`:204`), so any future header growth above it has to move that pin in the same PR. No changeset: a root `scripts/` file publishes nothing (the root `package.json` is private, and no package's `files` ships `scripts/`), so this PR takes `skip-changeset`. --- _Generated by [Claude Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This branch was successfully deployed
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.
Summary
Spec slice of objectstack-ai/objectui#2614 — the built-in row CRUD actions (Edit/Delete) could only be gated by object-level booleans, so an author could not hide/disable the Edit button per record (e.g. frozen snapshot rows).
userActions.edit/userActions.deletenow accept, in addition to the plain boolean, an object form (RowCrudActionOverrideSchema):ExpressionInputSchema(CEL; string shorthand normalizes to the{ dialect: 'cel', source }envelope). Unknown keys are rejected (.strict()), so e.g. ahideWhentypo fails at parse time.visibleWhenfalse → button not rendered (fail-closed);disabledWhentrue → rendered disabled (fail-soft). Advisory UI gating only — server enforcement stays with permissions/hooks.resolveCrudAffordances()keeps returning resolved booleans (enabledfalls back to themanagedBybucket default) and now surfaces the predicates aseditPredicates/deletePredicates, passed through as authored. Boolean-only inputs produce byte-identical output — zero behavior change for existing schemas.clampManagedObjectWrites(ADR-0092 D2/me/permissionshint clamp) treats the object form by its explicitenabledflag only: per-record predicates are not a write grant, so managed objects stay fail-closed unlessenabled === true.The UI consumption (grid row menu + related-list data-table) lands in the companion objectui PR on the same branch name.
Tests
packages/spec/src/data/object.test.ts— boolean back-compat, string-shorthand normalization, predicate pass-through with bucket-defaultenabled, boolean-equivalent object form, unknown-key rejection.packages/plugins/plugin-hono-server/src/fold-wildcard-superuser.test.ts— object form counts as opt-in only via explicitenabled: true.@objectstack/spec+@objectstack/plugin-hono-serverbuild clean (dts included).🤖 Generated with Claude Code
https://claude.ai/code/session_01HnCZhYpQjRyg2E44RHBiNE
Generated by Claude Code