Skip to content

feat(spec): SharingRuleEvaluationResult declares grantsRefused?: number — the optional seventh key the evaluate route already answers - #15714

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-14969-sharing-rule-evaluation-result-grants-refused
Sep 5, 2026
Merged

feat(spec): SharingRuleEvaluationResult declares grantsRefused?: number — the optional seventh key the evaluate route already answers#15714
os-project-manager merged 1 commit into
mainfrom
claude/issue-14969-sharing-rule-evaluation-result-grants-refused

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #14969

SharingRuleEvaluationResult (packages/spec/src/contracts/sharing-service.ts) gains its seventh key, grantsRefused?: numberoptional, not required — with a JSDoc that carries the absent-is-not-zero rule. The wire already answered the key: POST /api/v1/sharing/rules/:idOrName/evaluate passes the service's return value to res.json unfiltered (rest-server.ts:11335-11336), the route is ledgered sdk / shares.rules.evaluate (rest-route-ledger.ts:390), and the client types evaluate as a Promise of SharingRuleEvaluationResult (client/src/index.ts:4912, 4917) — so the declared SDK type lagged the route by exactly this key. The client gains it through its @objectstack/spec/contracts import with no edit.

What changed

  • packages/spec/src/contracts/sharing-service.ts — one optional key on the interface, its JSDoc, and a two-sentence addition to the interface's own doc block.
  • packages/spec/src/contracts/sharing-rule-evaluation-result-grants-refused.pin.test.ts — the contracts-side pin (type-level, compiled by check:test-typecheck under tsconfig.test.json; membership confirmed with tsc --listFiles, 0 errors in the file): the six counts stay the exact required set, grantsRefused is the ONLY optional key and reads as number | undefined, a string is refused (@ts-expect-error), a subtype that REQUIRES the key is still a legal ISharingRuleService['evaluateRule'] return while a six-key implementation keeps compiling, and the JSDoc above the key is read and required to state the rule below.
  • .changeset/sharing-rule-evaluation-result-grants-refused.md@objectstack/spec minor (derivation in the changeset: additive public API on a published contract interface; not major because the key is optional so every implementer and consumer keeps compiling).

Not touched, per dispatch: packages/plugins/plugin-sharing/**, packages/client/src/index.ts, packages/spec/src/contracts/job-service.ts, packages/spec/src/migrations/registry.ts (no ADR-0087 entry — nothing retired). Generated followers: none — check:generated on the rebuilt tree reports all 15 artifacts up to date (api-surface/contracts.json and export-origins/contracts.json list export NAMES, so a new key on an existing interface leaves them byte-identical; no content/docs/references/** page renders this interface).

The JSDoc, verbatim

On the interface:

Result of a rule evaluation pass.

The six counts are what every implementation reports. grantsRefused is the one OPTIONAL key, and its absence is a statement about the implementation, not about the pass — read its doc before branching on it.

On the key:

[#14969] Grants the engine REFUSED during the pass — each one an ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED on an organization-less insert into a tenant-scoped sys_record_share. The pass continues past a refused grant (the remaining grants and the stale-row revocations still run), so grantsRefused > 0 is NOT "the pass failed": it is the pass reporting what it could not grant on.

Optional by design, and the optionality carries meaning. A required key would break every other ISharingRuleService implementer, in-tree and out, while an implementation that refuses grants may still narrow it to required on its own subtype (a legal covariant narrowing — @objectstack/plugin-sharing does exactly that). The key is ABSENT — not 0 — from any implementation that does not count refusals. A consumer branching on it must read "unset" as "this implementation does not report refusals", never as "no grant was refused"; only a present 0 says the latter.

The wire already carried it: POST /api/v1/sharing/rules/:idOrName/evaluate answers the service's return value unfiltered, so this key lifts the declared client type (shares.rules.evaluate) up to what the route sends.

Reverse verification (H3) — read from the REBUILT dist/contracts/index.d.ts

Scratch consumer outside the tree, paths-mapped to packages/spec/dist/contracts/index.d.ts (grantsRefused?: number at its line 7243), tsc --strict --noEmit, exit captured before any pipe:

literal tsc exit errors
six keys (no grantsRefused) 0 0
six keys + grantsRefused: 2 0 0
six keys + grantsRefused: 'x' 2 exactly 1 — TS2322: Type 'string' is not assignable to type 'number'

The error CODE is the proof the reading came from the rebuilt declarations and not a cache: against a .d.ts without the key the same literal answers TS2353 (excess property), not TS2322.

Gate derivation (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no paths, derived at 08c6eecbb)

Everything below ran at 08c6eecbb (the final commit; the tree has not changed since), exit codes captured by redirect before any pipe; verdict lines quoted from the gates themselves.

  • pnpm --filter @objectstack/spec build (through os-verify-lock.sh): VERDICT command-exit 0, twice — once after the contract edit, once after the pin test was added (the first check:api-surface run refused with "dist is OLDER than src", a prerequisite, not a measurement).
  • check:generated (spec, on the rebuilt tree): ✓ All 15 generated artifacts are up to date. — covers check:api-surface, check:authorable-surface, check:export-origins, check:declaration-map, check:docs, check:strictness-ledger, check:liveness, check:skill-refs, check:skill-docs, check:test-typecheck, check:migration-registry, check:spec-changes, check:upgrade-guide, check:meta-url-spelling, check:react-blocks, each .
  • Spec source audits (exit 0 each): check:dual-source-exports "no new dual-source exports: 5060 names across 17 entry points"; check:empty-state "✓ all classified"; check:entry-nameability "439 call probes across 17 public entries, 0 new unnameable structural mentions"; check:error-code-provenance "OK — every registered-code stamp site is listed under its own owner key or carries a recorded waiver (9 waiver(s), all live)"; check:exported-any "no exported type resolves to any: 2457 types + 1521 schemas"; check:llms-txt "97 claim(s) re-derived"; check:browser-reachable-entries ✅; check:variant-docs ✓; check:yaml-examples ✅ 18 examples.
  • Repo-level (exit 0 each): check:nul-bytes, check:cross-package-test-inputs, check:doc-authoring, check:changeset-gate-self-tests, check:spec-parsed-alias, check:test-source-alias, check:objectql-double-limit, check:where-matcher, check:type-check-coverage ("OK — 75/79 workspace packages type-checked"), check-keyed-text-bounds, check-comment-mask-adoption, check-closing-keyword-parity, check:published-files, check-undeclared-dep-imports, docs-audit/check-affected-docs, docs-audit/check-drift-comment, check:merge-driver, check:org-identifier, check:page-declaration-shape, check:slot-lookup, check:logger-receiver-detach, check:type-source-resolution, check:doc-formula-expressions (after building @objectstack/formula; its first run was a PREREQUISITE NOT MET exit 3, not a measurement).
  • Changeset trio: check-empty-changeset "✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)"; check-changeset-no-major "✓ This diff introduces no major bump"; check-adr-0087-registration --base origin/main "✓ this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)".
  • NOT MEASURED locally, left to CI: check:type-check-debt (PREREQUISITE NOT MET — wants the whole ./packages/* closure built), check:dual-build-cjs-loads (same class), the remaining packages/** catch-all families the derivation lists (check:engine-double-contract, check:driver-memory-census, check:query-options-erasure, check:refd-timer-probe, check:watch-hint-literal, check-platform-object-tenancy-census, check-plugin-teardown-shape, check-registry-log-declared, check-system-context-census, check-reference-carrier-shape, check-ci-filter-parity, check:pm-half-states, check:objectui-changeset, check-dev-prereqs --self-test, release-rehearsal-clone --self-test) — none reads a contracts interface or a .pin.test.ts; declared, not skipped silently.

Consumer-package readings (downstream of @objectstack/spec, closure built: turbo run build 32/32 successful through the lock), all at 08c6eecbb

  • @objectstack/spec typecheck — exit 0, 0 error TS; check:test-typecheck: OK (the new pin test is in the program: --listFiles line 1365, 0 errors); vitest sharing-rule-evaluation-result-grants-refused.pin.test.ts + sharing-service.test.ts: Test Files 2 passed (2), Tests 18 passed (18), VERDICT command-exit 0.
  • @objectstack/plugin-sharing typecheck — exit 0, 0 error TS (its SharingRuleReconcilePassResult extends SharingRuleEvaluationResult { grantsRefused: number } stays a legal covariant narrowing); vitest src/reconcile-refused-grant-continues.test.ts: Test Files 1 passed (1), Tests 10 passed (10), VERDICT command-exit 0.
  • @objectstack/client typecheck — exit 0, 0 error TS, check:test-typecheck: OK.
  • @objectstack/rest typecheck — exit 0, 0 error TS, check:test-typecheck: OK.

origin/main moved 9 commits past the branch base 22a006b26 while this ran; the overlap with this diff's surface is empty (other .changeset/*.md files and the automation api-surface / export-origins shard, textually disjoint from the contracts shard) — no merge taken; the queue rebuilds on current main.

Out of scope

Contract review

Clause-② yes (a new key on a published SDK payload type): needs:contract-review is hung on this PR and on #14969; this PR stays draft for that review.

🤖 Generated with Claude Code


Generated by Claude Code

…er, the optional seventh key the evaluate route already answers

`POST /api/v1/sharing/rules/:idOrName/evaluate` passes the service's return
value through unfiltered, and plugin-sharing counts refused grants on its own
subtype, so the wire carried `grantsRefused` while the declared client type
could not name it. The key is OPTIONAL: required would break every other
ISharingRuleService implementer; optional composes with the plugin-local
covariant narrowing. Absent means "this implementation does not report
refusals", never 0 — the JSDoc says so and the contracts pin test reads it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 129 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 8e8860ed8144feb6e5151c5a95655cb5bcb40f1epackageMentionDocs.

Which tree this was computed on

This run read content/docs from 1bb481a41ed52ac4c7dad9c0f0cf1426588e3190 — the merge of head 08c6eecbb55e526b8f2080b125c5aa140cd715dc into base 8e8860ed8144feb6e5151c5a95655cb5bcb40f1e, 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 1bb481a41ed52ac4c7dad9c0f0cf1426588e3190 && git checkout 1bb481a41ed52ac4c7dad9c0f0cf1426588e3190
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8e8860ed8144feb6e5151c5a95655cb5bcb40f1e 08c6eecbb55e526b8f2080b125c5aa140cd715dc && git checkout -B drift-repro 8e8860ed8144feb6e5151c5a95655cb5bcb40f1e && git merge --no-ff 08c6eecbb55e526b8f2080b125c5aa140cd715dc

node scripts/docs-audit/affected-docs.mjs --json 8e8860ed8144feb6e5151c5a95655cb5bcb40f1e

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 5, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review September 5, 2026 04:27
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 87f0ccc Sep 5, 2026
43 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-14969-sharing-rule-evaluation-result-grants-refused branch September 5, 2026 05:22
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.

spec: lift grantsRefused?: number (optional) into SharingRuleEvaluationResult so the SDK type stops lagging the wire by one key

2 participants