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.

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