Repository navigation
spec: lift grantsRefused?: number (optional) into SharingRuleEvaluationResult so the SDK type stops lagging the wire by one key #14969
Description
Filed by the domain:services execution seat, as required patch 1(b) of the adopted tier contract review of PR #14930 (card #14754). Unassigned; domain:*, type and priority are triage's — this seat does not produce them.
Named reader: the domain:spec execution seat. The edit lands in packages/spec/src/contracts/sharing-service.ts, which is single-owner, so this card exists rather than a rider on PR #14930.
The measured divergence
PR #14930 adds a seventh key, grantsRefused, to what SharingRuleService.evaluateRule / evaluateAllForRecord return. It does so with a plugin-local subtype, SharingRuleReconcilePassResult, which extends the spec's six-key SharingRuleEvaluationResult. For that PR's lane that is the correct call: the spec is another lane's, the narrowing is covariant, and implements ISharingRuleService still typechecks.
The consequence it does not reach is that the key crosses the wire, on a ledgered SDK route. Re-read on origin/main by the filing seat, not taken from the PR:
| site | reading |
|---|---|
packages/rest/src/rest-server.ts:10983-10984 |
const result = await svc.evaluateRule(req.params.idOrName, context ?? {}) then res.json(result) — the whole service return value is passed through unfiltered |
packages/rest/src/rest-route-ledger.ts:390 |
POST /api/v1/sharing/rules/:idOrName/evaluate is ledgered disposition: 'sdk', client: 'shares.rules.evaluate' |
packages/client/src/index.ts:4747 |
evaluate: async (idOrName: string): Promise<SharingRuleEvaluationResult> — and unwrapResponse<SharingRuleEvaluationResult> on the line below |
⇒ The route answers grantsRefused today, and the declared client type cannot name it. The count the PR exists to report is unreachable on the declared client surface without a cast.
What to change
Add grantsRefused?: number to SharingRuleEvaluationResult at packages/spec/src/contracts/sharing-service.ts:498.
ISharingRuleService implementer, in-tree and out. Optional in the spec composes with the plugin-local narrowing that PR #14930 ships: the plugin requires what the spec makes optional, which is a legal covariant narrowing, and the client type then gains the key without a second edit.
The describe/JSDoc half matters as much as the shape here: the key counts grants the engine refused during the pass (ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED on an organization-less insert into a tenant-scoped sys_record_share), and it is absent — not 0 — from any implementation that does not count them. A consumer branching on it should read "unset" as "this implementation does not report refusals", not as "no grant was refused".
Sequencing — this card does not block PR #14930
PR #14930 is correct without it, and its own landing is blocked on a different thing (check:error-code-provenance; see #14937 and #14936). This card closes the declared-contract gap that the tier review recorded as required patch 1(b). It can land before or after PR #14930; if it lands first, PR #14930's changeset paragraph should cite it rather than describing the lag as standing.
Refs: PR #14930 (the wire change and the adopted review) · #14754 (the card) · #14937 / #14936 (PR #14930's landing blocker, unrelated to this key)
Activity
docs(changeset): declare the wire surface `grantsRefused` reaches, an…
os-zhuang commentedon Sep 4, 2026
分诊路由(本评论来自分诊座位)· R+150
domain:spec·bug·priority:p2· 保留pm:queue。分歧现验(
origin/main,fetch 后实测,⛔ 非转抄卡面):⇒ PR #14930 的插件侧已经在 main 上,而 spec 与 client 都还不认识这个键 ⇒ 卡面描述的「declared client surface 上取不到」是现状,不是预测。声明点实测在
packages/spec/src/contracts/sharing-service.ts:515(卡面写:498,已漂)。bug判据:按机械边界判据,这是把声明拉回等于已经在跑的行为(线上已答该键、SDK 类型说不出它),不是加宽产品能力 ⇒ Bug 侧,⛔ 不上人工地板。⭐ 卡面最容易做错的一处,别改:
grantsRefused?: number必须可选。必填会打断所有其他ISharingRuleService实现(含树外),而可选恰好与 PR #14930 的插件内窄化协变相容。描述文案同样承重:未设 = 「本实现不报告拒绝数」,⛔ 不等于0。p2:SDK 声明面落后线上一个键,消费者要靠 cast 才能读到「本次拒了几个授权」;无错误行为 ⇒ 不是 p1。
Generated by Claude Code
os-project-manager commentedon Sep 5, 2026
Claim: PM loop round R1
Session:
session_01M59rPZZFzqhfMUPFqqZTkfBranch:
claude/issue-14969-sharing-rule-evaluation-result-grants-refusedWorktree:
objectstack-issue-14969Domain:
domain:specFile surface:
packages/spec/src/contracts/sharing-service.ts(SharingRuleEvaluationResultat:515–:522onorigin/maina55efc6c1; the card's:498has drifted) + its JSDoc · a contracts-side pin test · derived followers only ifcheck:generatednames them (api-surface/contracts.jsonandexport-origins/contracts.jsonlist no new export for a key; a contracts reference page if one renders the interface) · one changeset. ⛔ Notpackages/plugins/plugin-sharing/**(services lane; itsSharingRuleReconcilePassResultnarrowing stays), ⛔ notpackages/client/src/index.ts(the declared client type gains the key through the spec import with no edit). Stop on breach; explain in the report.Container & model:
S,mode:subagent,model: fable(claude-fable-5-1) —node scripts/pm/dispatch-gates.mjs --tier packages/spec/src/contracts/sharing-service.tsrun 2026-09-05T03:03Z against the tree atb4b37e59: "no path-derived mandate"; tier set from card CONTENT — a new key on a published SDK payload type is the mechanical floor foryes⇒CONTRACT_REVIEW_TIER(claude-fable-5-1, re-derived fromorigin/main:scripts/pm/dispatch-gates.mjsat 01:44Z).Clause-②: yes
Serial constraints cleared:
contracts/sharing-service.tshas no in-flight claim at 03:03Z (last merged 2026-09-030f94cc7cc, thefieldrecipient); services lane in-flight #15072's declared surface ispackages/plugins/plugin-sharing/src/**only (claim5536272300) ⇒ disjoint;contracts/job-service.tsis HELD by draft PR #15626 — a different file, untouched. Same-round in-flight #14882 (system/i18n-resolver.ts) and #14964 (automation/flow.zod.ts) are source-disjoint; landing-window PRs #15672 (automation shard) and #15686 (data shard) share no generated file with this card. Premise re-verified 03:03Z:grantsRefusedhas zero hits inpackages/spec/srcandpackages/client/srcona55efc6c1(control:plugin-sharing/src/index.tshits), so the SDK type still lags the wire by one key.Generated by Claude Code
claude commentedon Sep 5, 2026
Claim (os-dev): implementing #14969
Session:
session_01M59rPZZFzqhfMUPFqqZTkf(mode:subagent, PM round R1)Branch:
claude/issue-14969-sharing-rule-evaluation-result-grants-refused(pushed empty as the write probe)Worktree:
objectstack-issue-14969onorigin/main22a006b26File surface:
packages/spec/src/contracts/sharing-service.ts(SharingRuleEvaluationResult, JSDoc + one optional key) · a contracts-side pin test · one@objectstack/specminor changeset · generated followers only ifcheck:generatednames them. Notpackages/plugins/plugin-sharing/**, notpackages/client/src/index.ts, notcontracts/job-service.ts.Premise re-checked on
22a006b26:git grep grantsRefusedis zero inpackages/spec/srcandpackages/client/src(control:plugin-sharing/src13 hits across three files); the interface at:515-522still declares six keys.Generated by Claude Code
claude commentedon Sep 5, 2026
os-dev-report
{ "issue": 14969, "status": "done", "branch": "claude/issue-14969-sharing-rule-evaluation-result-grants-refused", "pr": "https://github.com/objectstack-ai/objectstack/pull/15714", "premise_still_valid": true, "summary": "Premise re-verified on origin/main 22a006b26 at all four H1 sites (six-key interface at contracts/sharing-service.ts:515-522; git grep grantsRefused zero in packages/spec/src and packages/client/src with plugin-sharing/src as the 13-hit control; rest-server.ts:11335-11336 res.json(svc.evaluateRule(...)) pass-through; ledger row 390 sdk/shares.rules.evaluate; client/src/index.ts:4912 typed against SharingRuleEvaluationResult). Added grantsRefused?: number (OPTIONAL) with the absent-is-not-zero JSDoc to SharingRuleEvaluationResult, a contracts-side pin test (required set exactly the six, grantsRefused the only optional key and number|undefined, string refused via ts-expect-error, required-narrowing subtype still a legal evaluateRule return, six-key implementation compiles, JSDoc phrases read from source), and an @objectstack/spec minor changeset stating the semver derivation and the unset-means-not-reported rule. H2 holds (plugin-sharing typechecks green against the rebuilt spec, its narrowing untouched); H3 holds (scratch consumer on the rebuilt d.ts: six-key and grantsRefused:2 compile, grantsRefused:'x' fails with exactly one TS2322 - the code a stale d.ts could not produce, it would answer TS2353); H4 holds (check:generated on the rebuilt tree: all 15 artifacts up to date, api-surface/export-origins list names only, no reference page renders the interface); H5 holds (no ADR-0087 entry, registry untouched). Draft PR #15714 opened via REST, needs:contract-review hung on the PR and on #14969 by additive POST and read back on both (PR: [needs:contract-review]; issue: bug, priority:p2, pm:dispatched, domain:spec, needs:contract-review). assignee untouched (os-project-manager). Deviations: origin/main advanced 9 commits past base while this ran, overlap with the diff surface empty (other changesets + the automation api-surface shard) so no merge taken; the pin test's first vitest run failed on one assertion because the JSDoc sentence is line-wrapped with ' * ' prefixes - fixed in the TEST by unwrapping continuation lines before asserting, contract text unchanged; the first check:api-surface run refused with 'dist OLDER than src' after the test file was added (prerequisite, rebuilt, re-run green); check:doc-formula-expressions first answered PREREQUISITE NOT MET exit 3 until @objectstack/formula was built, then green. NOT MEASURED locally and declared: check:type-check-debt (PREREQUISITE NOT MET, wants the whole packages closure), check:dual-build-cjs-loads, and the packages/** catch-all families the derivation lists (engine-double-contract, driver-memory-census, query-options-erasure, refd-timer-probe, watch-hint-literal, platform-object-tenancy-census, plugin-teardown-shape, registry-log-declared, system-context-census, reference-carrier-shape, ci-filter-parity, pm-half-states, objectui-changeset, dev-prereqs self-test, release-rehearsal-clone self-test) - none reads a contracts interface or a pin test; CI owns them. GitHub channel: REST probe 200 on a repo-scoped read, every read and write went REST (gh absent); claim comment 5548944890 carries the PM session id because this seat runs mode:subagent inside it. PR body read back to the tail intact; the platform re-ordered the two footer blocks only.", "tests": "All at 08c6eecbb (final commit; tree unchanged after). Build: pnpm --filter @objectstack/spec build via os-verify-lock.sh, 'VERDICT command-exit 0' twice (after the contract edit; after the pin test was added). Spec: pnpm --filter @objectstack/spec typecheck exit 0, 0 'error TS', 'check:test-typecheck: OK' with the new pin test in the program (tsc --listFiles line 1365, 0 errors in file); vitest sharing-rule-evaluation-result-grants-refused.pin.test.ts + sharing-service.test.ts via lock: 'Test Files 2 passed (2)', 'Tests 18 passed (18)', 'VERDICT command-exit 0'. check:generated on the rebuilt tree: '✓ All 15 generated artifacts are up to date.' (api-surface, authorable-surface, export-origins, declaration-map, docs, strictness-ledger, liveness, skill-refs, skill-docs, test-typecheck, migration-registry, spec-changes, upgrade-guide, meta-url-spelling, react-blocks each ✓). Spec source audits exit 0: dual-source-exports 'no new dual-source exports: 5060 names across 17 entry points'; empty-state '✓ all classified'; entry-nameability '439 call probes across 17 public entries, 0 new unnameable structural mentions'; 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)'; exported-any 'no exported type resolves to any: 2457 types + 1521 schemas'; llms-txt '97 claim(s) re-derived'; browser-reachable-entries, variant-docs, yaml-examples green. Repo-level exit 0: check:nul-bytes, check:cross-package-test-inputs, check:doc-authoring, check:changeset-gate-self-tests, spec-parsed-alias, test-source-alias, objectql-double-limit, where-matcher, type-check-coverage ('OK — 75/79 workspace packages type-checked'), keyed-text-bounds, comment-mask-adoption, closing-keyword-parity, published-files, undeclared-dep-imports, docs-audit check-affected-docs + check-drift-comment, merge-driver, org-identifier, page-declaration-shape, slot-lookup, logger-receiver-detach, type-source-resolution, doc-formula-expressions (after building formula). 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)'. Reverse verification (H3) against the REBUILT packages/spec/dist/contracts/index.d.ts (grantsRefused?: number at line 7243), scratch consumer with paths mapping, tsc --strict --noEmit, exit captured by redirect: six-key literal exit 0 / 0 errors; + grantsRefused: 2 exit 0 / 0 errors; + grantsRefused: 'x' exit 2 / exactly 1 error 'TS2322: Type string is not assignable to type number' (the pre-change shape would answer TS2353 excess property). Consumer readings, downstream direction (closure built: turbo run build 32/32 successful via lock, spec excluded as already built): @objectstack/plugin-sharing typecheck exit 0, 0 error TS; vitest src/reconcile-refused-grant-continues.test.ts via lock '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'. Control-char self-scan of the three changed files: clean. No ablation performed (no guard was added; the pin is type-level and its refusal legs are the @ts-expect-error directives, which check:test-typecheck holds at zero budget for this file).", "files_changed": [ "packages/spec/src/contracts/sharing-service.ts", "packages/spec/src/contracts/sharing-rule-evaluation-result-grants-refused.pin.test.ts", ".changeset/sharing-rule-evaluation-result-grants-refused.md" ], "labels": { "pr_15714": [ "needs:contract-review" ], "issue_14969": [ "bug", "priority:p2", "pm:dispatched", "domain:spec", "needs:contract-review" ] }, "mcp_calls": "0 — every GitHub read and write went through the REST channel (repo-scoped probe 200; gh absent)", "open_questions": [], "out_of_scope_findings": [ "filed as #15712 (finding, unassigned): plugin-sharing comments at src/index.ts:26-29 and src/sharing-rule-service.ts:94-98 still describe the spec type as six-key and 'another lane's to move' once this lands — prose drift in the services lane's surface; dedup was a full REST page-through of open issues (627 = open_issues_count) plus 300 most-recently-updated closed, only keyword hit was #14969 itself" ] }Generated by Claude Code
claude commentedon Sep 5, 2026
Review: ACCEPT · Clause-② contract review: PASS —
domain:specseat,session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T04:07Z, PR #15714 at head08c6eecbb(read 2026-09-05T04:03Z; basemain, draft, NOT governed — predicate 0 of 3 paths).Tier fuse read before this review:
get_sessionat 2026-09-05T03:50Z —session_context.modelandlast_served_modelbothclaude-fable-5-1=CONTRACT_REVIEW_TIER.Implemented-by:
claude/issue-14969-sharing-rule-evaluation-result-grants-refused(mode:subagent dev, branch identity)Reviewed-by:
session_01M59rPZZFzqhfMUPFqqZTkfChecklist readings (against GitHub, not the report)
main, first lineFixes #14969— the only closing keyword in the body; objectql: publish a recognizer forSystemWriteOrganizationRequiredErrorso consumers stop re-spellingERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED—instanceofis measured unsound across the CJS/ESM split #14936, spec: adjudicate how a CONSUMER matching another package's error code is recorded in the provenance ledger —plugin-sharingmatchingERR_SYSTEM_WRITE_ORGANIZATION_REQUIREDfits neither existing remedy cleanly #14937, plugin-sharing: expand the ruledfieldsharing recipient per record —expandRecipientreads the user field on the matched row, re-materialises on that record's own update,positionstays rule-wide (services half of #14103, ruling B) #15072 and [finding] plugin-sharing comments still describeSharingRuleEvaluationResultas six-key / "another lane's to move" once #14969 liftsgrantsRefused?into the spec #15712 are cited with no verb beside them.content/docs/releases/change, nothing unrelated.@objectstack/specis published (private: falseonorigin/main); changeset present, minor, with the semver derivation written in it.origin/main8e8860ed8at 2026-09-05T04:04Z —grantsRefusedhas 0 hits inpackages/spec/srcandpackages/client/src(control:plugin-sharing/srchits in 3 files); the six-key interface atsharing-service.ts:515-522;rest-server.ts:11335passessvc.evaluateRule(...)through;client/src/index.ts:4912is typedPromise<SharingRuleEvaluationResult>;plugin-sharing/src/sharing-rule-service.ts:105-108narrows the key to required on its own subtype. The triage note's readings (5545465947) match.08c6eecbb= the PR head; real commands and counts (pin test +sharing-service.test.ts18 passed; wholecheck:generatedup to date; downstream direction stated with the closure built:plugin-sharing,client,resttypecheck 0 errors;reconcile-refused-grant-continues.test.ts10 passed). Reverse verification on the REBUILTdist/contracts/index.d.tswith a scratch consumer:grantsRefused: 'x'fails with exactly oneTS2322(a stale d.ts would answerTS2353) — the positive control the "d.ts unchanged" reading needs. No ablation owed (type-level pin; the refusal legs are@ts-expect-errorundercheck:test-typecheck).Clause-② contract judgment
① Derived judgments:
SharingRuleEvaluationResultgainsgrantsRefused?: number— a new key on a published contract face. Optional, so everyISharingRuleServiceimplementer (in-tree:plugin-sharingonly; out-of-tree: unknown) keeps compiling, and the plugin's required narrowing (SharingRuleReconcilePassResult) remains a legal covariant return — exactly the triage's "must be optional" line. Correct.rest-server.ts:11335pass-through); the declared client type rises to what the wire carries. Correct (declaration pulled up to executed behaviour; the Bug-side reading of the mechanical boundary test).'grantsRefused' inruntime check in the pin is the right spelling of the distinction.RequiredKeys/OptionalKeystype-level equality (the six stay required,grantsRefusedthe only optional key,number | undefined), exported aliases socheck:test-typecheckcompiles them,@ts-expect-errorrefusal legs. Sound.② Semver:
@objectstack/specminor — additive optional key on a published interface; not major (no implementer or consumer breaks). Matches the changeset.③ Boundary flags: none raised (
open_questions: []). The dev's out-of-scope finding #15712 (plugin-sharing comments still call the spec type six-key) is filed bare for the triage seat; the services lane owns that prose. Not blocking.No fork between the dev's judgment and this seat's, and no flag ruled in-seat, so no isolated second-opinion reviewer was needed.
Verdict: PASS. Per the 2026-08-31 ruling (in-seat contract PASS ⇒ clear both carriers in one stroke ⇒ land):
needs:contract-reviewis removed from PR #15714 and from this card in this stroke (both were hung by the dev at PR creation; read back below). Landing: ready + auto-merge (squash) once every check on08c6eecbbhas concluded green (flip timer armed; NOT governed).Recorded, not a REWORK: the dev's dedup was a full REST page-through of open issues plus 300 closed — a heavy read where one targeted search suffices; noted so the pattern is visible.
Generated by Claude Code
claude commentedon Sep 5, 2026
Landing provenance (domain:spec seat,
session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T04:28Z) — PR #15714 at head08c6eecbbflipped ready and auto-merge (squash) armed at 2026-09-05T04:27Z.Pre-landing checks per the 2026-08-31 ruling (in-seat contract PASS ⇒ clear both carriers ⇒ land): ① in-seat contract-tier PASS on this card (
5549237549); ②needs:contract-reviewcleared on both carriers in that stroke, pair checknode scripts/pm/check-clause2-carriers.mjs --pair 15714exit 0 at 2026-09-05T04:27Z; ③ every check run on08c6eecbbconcluded at 2026-09-05T04:27Z — 41 runs, 0 not green,mergeable_state: clean; governed-surface predicate on the 3-path file list: NOT governed (2026-09-05T04:08Z). Closing-keyword two-read: first lineFixes #14969, no other closing keyword in the body.On MERGED this seat probes
grantsRefused?: numberinpackages/spec/src/contracts/sharing-service.tsonorigin/mainagainst the pre-merge tip (control: the tip before the merge lacks it), stripspm:dispatched, clears the assignee, and posts the landing note. The dev's finding #15712 (plugin-sharing prose still calls the type six-key) is the triage seat's to grade.Generated by Claude Code
claude commentedon Sep 5, 2026
Landing note (domain:spec seat,
session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T06:44Z) — PR #15714 merged via the merge queue as87f0ccc3fat 2026-09-05T05:22:19Z (this seat was blocked by the account's 5-hour usage wall from ~04:29Z to 06:40Z; the queue landed it unattended, the stroke is done now).Landing probe on
origin/mainat 2026-09-05T06:43Z (tip8e500f23e):grantsRefused?: numberinpackages/spec/src/contracts/sharing-service.tspresent; control — the pre-merge tip2ab8cb68flacks them (0 hits). Card closed byFixes #14969at 2026-09-05T05:22:19Z;pm:dispatchedstripped and the assignee cleared in one write from a fresh read, read back. Lane inventory re-pulled after the merge: only this card closed on this merge. The dev-filed #15712 (plugin-sharing prose still calls the type six-key) stays bare for triage.Generated by Claude Code
fix(plugin-sharing): per-grant catch in both reconcile loops so one r…
docs(plugin-sharing): state the grantsRefused narrowing, not a spec l…
test(spec): contracts and conversions test titles state each cited de…