Skip to content

Commit efa8eb7

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15072-field-sharing-recipient
# Conflicts: # packages/plugins/plugin-sharing/src/sharing-rule-service.ts
2 parents fc1a4cf + 1bc3c09 commit efa8eb7

62 files changed

Lines changed: 4200 additions & 1073 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/console-00d3f09c500c.md

Lines changed: 146 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): retire `MetadataChangedEventPayloadSchema` — the `metadata:changed` payload nothing ever emitted or consumed (#14180, ADR-0049)
6+
7+
<!-- adr-0087: registered metadata-changed-event-payload-retired -->
8+
9+
**BREAKING** export removal, landing after the v17.0.0 cut (the lockstep
10+
launch-window convention ships it as `minor`; the prescription is registered
11+
under protocol major 18 — `RETIRED_DEFS_BY_MAJOR[18]` + the D3 semantic entry
12+
`metadata-changed-event-payload-retired` — where `os migrate meta` users will
13+
look).
14+
15+
`kernel/cluster.zod.ts` declared a "canonical payload for the
16+
`metadata:changed` event" and said every metadata persistence layer MUST emit
17+
it after any successful write and every reader MUST subscribe and compare its
18+
`version` before invalidating. Nothing ever did either: zero runtime emitters,
19+
zero subscribers, zero imports outside `packages/spec` (its own unit test, the
20+
isomorphic alias pin and the generated artifacts), in objectstack and in
21+
objectui at the pinned sha. It could not have been honoured as declared — the
22+
`version` field is `z.bigint()`, which the standard JSON serializer refuses, so
23+
the payload could not cross any pubsub transport without a codec no driver
24+
ships. The three cluster channels that do run (`metadata.changed`,
25+
`metadata.mutated`, `datasource.mutated`) all carry an address-only signal
26+
whose receiver re-reads its own store — the opposite of the declared
27+
version-compare receipt — so the one plausible future consumer was decided
28+
against (2026-09-01 ruling), and the triage ruling (2026-09-02) chose removal
29+
over "make a consumer".
30+
31+
FROM → TO:
32+
33+
- `MetadataChangedEventPayloadSchema` / `MetadataChangedEventPayload`
34+
*(removed)* — no replacement type is declared. Subscribe to one of the lanes
35+
documented in `content/docs/kernel/cluster.mdx` §6.2 instead:
36+
`metadata.changed` (`ClusterMetadataChangedPayload`, `@objectstack/metadata`),
37+
`metadata.mutated` (`ClusterMetadataMutationPayload`,
38+
`@objectstack/metadata-protocol`) or `datasource.mutated`
39+
(`ClusterDatasourceMutationPayload`, `@objectstack/service-datasource`).
40+
- `MetadataChangeOperationSchema` / `MetadataChangeOperation`*(removed)*
41+
the orphan value schema of the payload's `operation` field; it had no other
42+
consumer.
43+
44+
One-line fix: delete the import — every one of the four names is TS2305 after
45+
upgrade, and no runtime path ever produced or read a value of these types. A
46+
host that used the retired type for a transport of its own keeps a local type.
47+
48+
The retirement kit:
49+
50+
- **whole-def deletion** (route 3 — not an authorable surface: no metadata-type
51+
binding, stack collection or manifest embed ever carried it, and nothing
52+
parsed it outside its own unit test, so there is no authored document to
53+
rewrite and nobody who could receive a parse-time tombstone):
54+
`kernel/MetadataChangedEventPayload` and `kernel/MetadataChangeOperation` in
55+
`RETIRED_DEFS_BY_MAJOR[18]` plus the D3 semantic entry. The payload def was
56+
never in `json-schema.manifest/` (the JSON Schema build skips `bigint`); the
57+
enum was, so the manifest deletion gate adjudicates it against the entry.
58+
- **retirement pins** in `kernel/cluster.test.ts`: runtime namespace probes
59+
assert both names are absent from `kernel/cluster.zod` and from the
60+
`@objectstack/spec/kernel` entry, with `ClusterCapabilityConfigSchema` as
61+
the positive control; the two isomorphic alias pins left with the schemas.
62+
- **docs**: `content/docs/kernel/cluster.mdx` no longer describes a planned
63+
version-stamped payload or a version-compare reader contract — the shipped
64+
address-only lanes and their re-read receipt are the contract; the
65+
`ClusterMetadataChangedPayload` doc-comment in `@objectstack/metadata` no
66+
longer claims to "align with" the retired schema (it never did).
67+
- zero in-tree consumers, so no in-repo source changes ride along; runtime
68+
behaviour is unchanged.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
"@objectstack/plugin-sharing": minor
3+
---
4+
5+
fix(plugin-sharing): one refused grant no longer aborts a sharing rule's reconcile pass — its stale-row revocations still run (#14754)
6+
7+
After #14484 `sys_record_share` is `tenant-scoped` in the #13491 ledger, so on a
8+
walled install an organization-less system insert on it is refused loudly with
9+
`ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` (#8844). `SharingService.grant`
10+
resolves the organization on every path that can; a platform-global sharing
11+
rule (`organization_id = null`, its sweep unscoped) materialising a grant onto
12+
an organization-LESS record resolves none, and meets that refusal.
13+
14+
`SharingRuleService.reconcile` / `reconcileForRecord` had no per-grant catch, so
15+
the refusal propagated and **that rule's pass aborted mid-loop**. Two things
16+
were lost, and they are not equally serious:
17+
18+
- the remaining grants — recoverable, the next pass writes them;
19+
- **the stale-row revocations of that pass** — not recoverable by waiting,
20+
because every subsequent pass meets the same organization-less record and
21+
dies in the same place. A stale over-grant of that rule therefore persisted
22+
indefinitely, and the record kept aborting the pass until it was repaired by
23+
hand. That is the security-relevant half.
24+
25+
Measured while pinning this, and it sharpens the point: the engine returns
26+
organization-less rows **last** in a rule's criteria sweep (the driver's
27+
NULL-org compatibility arm is appended to the scoped arm). So a refused grant
28+
is nearly always one of the final attempts of a pass, and what an abort
29+
destroyed was hardly ever "the remaining grants" — it was almost entirely the
30+
revoke loop that runs after the whole upsert loop.
31+
32+
Both loops now attempt each grant individually. A refusal is logged with the
33+
rule, object, record, recipient and the engine's own code, counted, and the
34+
pass **continues** — the remaining grants and, above all, the stale-row
35+
revocations still run.
36+
37+
**The catch is deliberately narrow.** Only
38+
`ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` is absorbed; every other error
39+
rethrows unchanged. A catch-all would swallow real defects and report a pass
40+
that "completed" having written nothing. It would also silently retire a
41+
reviewed decision: `record-share-organization-stamp.test.ts` deliberately pins
42+
the abort on the OTHER error a reconcile pass can meet here — the scoped update
43+
half answering `RECORD_NOT_FOUND` for a row stamped with a different
44+
organization — which the 2026-09-02 contract review left standing on "loud
45+
beats a wrong count". Those three pins are unchanged and still green.
46+
47+
**Why `minor` rather than `patch`.** The repair is a bug fix, but it reports
48+
through a new key. `reconcile` / `reconcileForRecord` / `evaluateRule` /
49+
`evaluateAllForRecord` now return `SharingRuleReconcilePassResult` — the spec's
50+
`SharingRuleEvaluationResult` plus `grantsRefused: number` — and that type is
51+
newly exported from the package index. Purely additive: the contract in
52+
`@objectstack/spec` is untouched, its six declared fields are unchanged, and a
53+
consumer typed against `ISharingRuleService` keeps compiling as it did. Same
54+
shape as `fix(runtime): tell an action handler when its caller-scope record load
55+
was refused` (#14143), which shipped `minor` for the same reason.
56+
57+
`grantsRefused > 0` does **not** mean the pass failed. It means the pass met a
58+
record it cannot grant on and carried on — which is the whole point.
59+
60+
**Wire surface — declared, not lifted.** `grantsRefused` reaches the wire.
61+
`POST /api/v1/sharing/rules/:idOrName/evaluate` is a ledgered **SDK** route —
62+
`packages/rest/src/rest-route-ledger.ts:390`, the row carrying
63+
`disposition: 'sdk'` and `client: 'shares.rules.evaluate'` — and its REST handler
64+
passes the service return value through **unfiltered**
65+
(`packages/rest/src/rest-server.ts:11108``:11109`:
66+
`const result = await svc.evaluateRule(req.params.idOrName, context ?? {})`
67+
followed by `res.json(result)`). So the seventh key is on the response body every
68+
caller of that route already receives. The SDK method declares
69+
`SharingRuleEvaluationResult` as its resolved type
70+
(`packages/client/src/index.ts:4766`, unwrapped at `:4771` through
71+
`unwrapResponse` parameterised on that same type), and that type is the spec's
72+
six-field contract — so the **declared client type cannot name the seventh key**.
73+
That is a client-type **lag**, not a contract break: the key is additive on the
74+
wire, every declared field is unchanged, and a consumer typed against
75+
`SharingRuleEvaluationResult` keeps compiling exactly as before. Lifting the type
76+
is not this PR's to do — `SharingRuleEvaluationResult` lives in
77+
`@objectstack/spec`, a `domain:spec` single-owner file — so the lag is declared
78+
here and tracked as the follow-up #14969, which lifts `grantsRefused?: number`
79+
(optional) into `SharingRuleEvaluationResult`.

.claude/skills/pm-dispatch/references/dispatch-runbook.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ monitor 按自己的 deadline 触发;身份 = 三元组 `(issue, 分支, PR head
240240
点名单当天现取仍会漏(实测漏的门恰被改动打红过)—— dev 对实际改动路径重取补跑。
241241
**去重/看板读数**:取一次(查询式 + 时间戳)整批共用,
242242
派发令写「当既有事实用,只复核其后的增量,⛔ 不重跑」。dev 侧对价住 os-dev 定义。
243+
- **四轴框架整段现取现粘、逐单随派发词下发**(维护者 2026-09-03 裁):内部 PM 副本 =
244+
`.claude/skills/pm-dispatch/SKILL.md` `## 升级与决策` 段的四轴框架块(黑体句起、推荐约束句止),
245+
整段逐字粘贴,⛔ 不重打、⛔ 不摘句;派发那刻现取,把该副本的 commit 与 md5 记进派发词。
246+
⛔ 仓内任何文件不得另立第三份(`check:skill-frame-sync` 全树扫);os-dev 定义没带就停下索取。
243247
- **文件面两句原文**:「预期落点是 X;若实测表明真正的生产者在别包,报备后按生产者侧修
244248
(落点与理由写进报告和 PR 正文),⛔ 不在消费者侧打补丁」—— 只写一个路径名,
245249
是要求 dev 在守约与修对之间二选一;跨包常等于跨车道,PM 事后补跨座位声明。

0 commit comments

Comments
 (0)