Commit fc3a36a
* fix(spec,objectql,sharing,storage): state per-row vs record dispatch on the hook contract (#6966)
A predicate (`multi: true`) write dispatches its lifecycle hooks once per
matched row — `after*` since #5038, `before*` since #5574 — on a context
deliberately indistinguishable from a single-id write's. That is the feature,
and it erased the only signal several handlers had: before #5574 a bulk
`before*` fired once with `input.id` present-but-`undefined`, so "no id" meant
"this call stands for N rows". Every guard written on it silently inverted
rather than failing.
Adds `HookContext.dispatch` — `{ mode: 'record' | 'per-row', index, scope }` —
bound by the engine at every write dispatch site (insert, update, delete, both
phases), at the point the dispatch ladder is decided. Optional, and an absent
marker reads as "not per-row", so existing handlers keep their behaviour.
`scope` is one object shared by every dispatch of one write across both phases:
the seam handlers used to get by stashing on the context, which only ever
worked because a single-id write reuses one context across its pair.
Deliberately not the `isPredicateBulkWrite` discriminator #5574 retired under
ADR-0049: that one inferred "bulk" at the consumer from `input.id` and
`options.multi` and ended with no producer and no reachable consumer. This one
is engine-produced and has readers.
Behaviour fixed:
- plugin-sharing — the `before*` stash of a write's affected row set was landing
on a per-row context the `after*` phase never saw, so every bulk update or
delete on a ruled object revoked all of that object's rule grants and queued a
full asynchronous re-grant, once per matched row, with the repeats racing each
other. Access was never widened; a bounded write now takes the bounded path
again, the cap still applies to the union, and the `after*` work runs once per
write instead of N times (the bounded branch was quadratic in batch size).
- service-storage — the `beforeDelete` id pre-resolution was dead on every path
and `afterDelete` was doing one `sys_file` lookup per row where the batch fits
one `$in`. The pre-resolution query is gone entirely: the engine has already
matched the rows. `beforeUpdate` copy-on-claim no longer runs per row against
a batch-scoped payload, removing a row-conditioned rewrite of a shared SET
clause (out of contract under ADR-0058 Addendum II D3).
Stale comments in both packages asserting that predicate writes never populate
`input.id`, and that the engine reuses one `HookContext` across a write's
before/after pair, are corrected where they sit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EcLMsZRoR3daV3QzPXgwxK
* docs(service-storage): name the follow-up issue at the afterUpdate ownership note (#6966)
The comment said the bulk-update file-ownership hole was "filed separately";
it is #7102. A pointer a reader can follow beats a promise they cannot check.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EcLMsZRoR3daV3QzPXgwxK
* test(objectql): pin the marker as an invariant, not just path by path (#6966)
`HookContext.dispatch`'s JSDoc claims every dispatched context carries it —
which is what makes `ctx.dispatch?.mode` safe to read with no "what if the
engine did not bind it" branch. The per-path cases prove the four write paths;
this proves the claim itself, and covers the one context a reader might worry
about: update()/delete() keep a batch-scoped `hookContext` on the predicate
path, and no handler ever sees it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EcLMsZRoR3daV3QzPXgwxK
* docs(spec): regenerate the hook reference page for HookContext.dispatch (#6966)
`content/docs/references/data/hook.mdx` is generated from the Zod schema, so
the new key has to land there too — `check:docs` is what caught it. The row
renders all three members inline (three keys, under the renderer's four-key
limit), so it carries no `…` elision and none of the `any` trap the `roles`
tombstone note warns about.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EcLMsZRoR3daV3QzPXgwxK
* fix(plugin-sharing): an empty per-row union reads as unbounded, not as "no rows" (#6966)
The accumulator only exists because a `before*` dispatch created it, so an
empty one means every id it was handed was null — "we do not know", not
"nothing changed". Reading it as an empty row set would silently skip the
cleanup entirely, which is the direction #4757 was filed for and the rule this
module states for its resolve path.
Adds direct unit cover for the accumulator: the union across rows without
re-querying the predicate, dedup across the two subscribers that both stash on
every row, the empty-union verdict, and the cap applied to the union.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EcLMsZRoR3daV3QzPXgwxK
* chore(spec): record HookDispatch in the api-surface snapshot (#6966)
`HookDispatch` is a new public export, so the surface snapshot moves with it —
0 breaking, 1 added. Caught by CI's `check:api-surface`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EcLMsZRoR3daV3QzPXgwxK
* chore(spec): regenerate api-surface and export-origins after merging main (#6966)
Two generated artifacts move with the merge:
- `export-origins/` is new on main (#4796) and had never seen `HookDispatch`.
- `api-surface/` needed a real regeneration, not just my own entry. The merge
of main into this branch produced a file byte-identical to THIS branch's
side, silently dropping the five exports #7123 added on main
(`SEARCH_VIRTUAL_TYPES`, `foldAsciiCase`, `asciiCaseInsensitiveContains`,
`asciiCaseInsensitiveRegexSource`, `isVirtualSearchField`). These paths carry
`merge=os-regen` in .gitattributes precisely so a merge regenerates rather
than picks a side; it did not here, and `check:api-surface` is what caught
it. Regenerating yields the union — both main's five and this branch's
`HookDispatch`.
Read the export-origins diff as its gate asks: one line, `HookDispatch` under
`src/data/hook.zod.ts`, the same origin as `HookContext`. Not a re-home, not a
new dual-source.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EcLMsZRoR3daV3QzPXgwxK
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0fd8556 commit fc3a36a
15 files changed
Lines changed: 873 additions & 70 deletions
File tree
- .changeset
- content/docs/references/data
- docs/audits
- packages
- objectql/src
- plugins/plugin-sharing/src
- services/service-storage/src
- spec
- api-surface
- authorable-surface
- export-origins
- src/data
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
39 | 40 | | |
40 | 41 | | |
41 | 42 | | |
| |||
Lines changed: 11 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
25 | | - | |
| 24 | + | |
| 25 | + | |
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
| 34 | + | |
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
| 48 | + | |
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
52 | | - | |
| 52 | + | |
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
105 | | - | |
| 105 | + | |
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
111 | 111 | | |
112 | | - | |
| 112 | + | |
113 | 113 | | |
114 | 114 | | |
115 | 115 | | |
| |||
179 | 179 | | |
180 | 180 | | |
181 | 181 | | |
182 | | - | |
| 182 | + | |
183 | 183 | | |
184 | 184 | | |
185 | 185 | | |
| |||
195 | 195 | | |
196 | 196 | | |
197 | 197 | | |
198 | | - | |
| 198 | + | |
199 | 199 | | |
200 | 200 | | |
201 | 201 | | |
202 | | - | |
| 202 | + | |
203 | 203 | | |
204 | 204 | | |
205 | 205 | | |
206 | 206 | | |
207 | 207 | | |
208 | | - | |
| 208 | + | |
209 | 209 | | |
210 | 210 | | |
211 | 211 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
921 | 921 | | |
922 | 922 | | |
923 | 923 | | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
| 966 | + | |
| 967 | + | |
| 968 | + | |
| 969 | + | |
| 970 | + | |
| 971 | + | |
| 972 | + | |
| 973 | + | |
| 974 | + | |
| 975 | + | |
| 976 | + | |
| 977 | + | |
| 978 | + | |
| 979 | + | |
| 980 | + | |
| 981 | + | |
| 982 | + | |
| 983 | + | |
| 984 | + | |
| 985 | + | |
| 986 | + | |
| 987 | + | |
| 988 | + | |
| 989 | + | |
| 990 | + | |
| 991 | + | |
| 992 | + | |
| 993 | + | |
| 994 | + | |
| 995 | + | |
| 996 | + | |
| 997 | + | |
| 998 | + | |
| 999 | + | |
| 1000 | + | |
| 1001 | + | |
| 1002 | + | |
| 1003 | + | |
| 1004 | + | |
| 1005 | + | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
| 1011 | + | |
| 1012 | + | |
| 1013 | + | |
| 1014 | + | |
| 1015 | + | |
| 1016 | + | |
| 1017 | + | |
| 1018 | + | |
| 1019 | + | |
| 1020 | + | |
| 1021 | + | |
| 1022 | + | |
| 1023 | + | |
| 1024 | + | |
| 1025 | + | |
| 1026 | + | |
| 1027 | + | |
| 1028 | + | |
| 1029 | + | |
| 1030 | + | |
| 1031 | + | |
| 1032 | + | |
| 1033 | + | |
| 1034 | + | |
| 1035 | + | |
| 1036 | + | |
| 1037 | + | |
| 1038 | + | |
| 1039 | + | |
| 1040 | + | |
| 1041 | + | |
| 1042 | + | |
| 1043 | + | |
| 1044 | + | |
| 1045 | + | |
| 1046 | + | |
| 1047 | + | |
| 1048 | + | |
| 1049 | + | |
| 1050 | + | |
| 1051 | + | |
| 1052 | + | |
| 1053 | + | |
| 1054 | + | |
| 1055 | + | |
| 1056 | + | |
| 1057 | + | |
| 1058 | + | |
| 1059 | + | |
| 1060 | + | |
| 1061 | + | |
| 1062 | + | |
| 1063 | + | |
| 1064 | + | |
| 1065 | + | |
| 1066 | + | |
| 1067 | + | |
| 1068 | + | |
| 1069 | + | |
| 1070 | + | |
| 1071 | + | |
| 1072 | + | |
| 1073 | + | |
| 1074 | + | |
| 1075 | + | |
| 1076 | + | |
| 1077 | + | |
| 1078 | + | |
| 1079 | + | |
| 1080 | + | |
| 1081 | + | |
| 1082 | + | |
| 1083 | + | |
| 1084 | + | |
| 1085 | + | |
| 1086 | + | |
| 1087 | + | |
| 1088 | + | |
| 1089 | + | |
| 1090 | + | |
| 1091 | + | |
| 1092 | + | |
| 1093 | + | |
| 1094 | + | |
| 1095 | + | |
| 1096 | + | |
| 1097 | + | |
| 1098 | + | |
| 1099 | + | |
| 1100 | + | |
924 | 1101 | | |
925 | 1102 | | |
926 | 1103 | | |
| |||
0 commit comments