You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The three 'Audit that…' per-type rows are now shipped decisions:
sys_sharing_rule = record-authoritative with provenance + seed-not-clobber;
sys_position = seed-only identity/display, locked by test; sys_capability =
seed-not-clobber with scope seed-once. Record why sharing rules got no write
gate (admin tuning surface), the customized-stamp boundaries, and the scope
migration cost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe
Copy file name to clipboardExpand all lines: docs/adr/0094-sys-permission-set-pure-projection.md
+29-5Lines changed: 29 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -301,14 +301,38 @@ only a seed-not-clobber discipline.
301
301
| Type | Enforcement reads | Class | Decision |
302
302
| :-- | :-- | :-- | :-- |
303
303
|`sys_permission_set`| metadata (`PermissionEvaluator.resolvePermissionSets` → `metadata.list('permission')`, DB row only as fallback) | metadata-authoritative |**Record is a projection — done** (this ADR). |
304
-
|`sys_sharing_rule`| the record, live (`sharing-plugin.ts` "rule evaluation reads `sys_sharing_rule` live"; `sharing-rule-service``engine.find`) |**record-authoritative**|Declared rules are a **boot seed**; the record is the authority. Do **not** project. Audit that `bootstrapDeclaredSharingRules` preserves env-edited rows (seed-not-clobber) and that the metadata overlay is not read as a live override. |
305
-
|`sys_position`| mixed — position→permission-set resolution is metadata-first for the *sets*, but the `sys_position`**record** (incl. its `permissions` field, bindings, `delegatable`, `admin` gating) is read live by the anchor gate and `DelegatedAdminGate`|**needs the seed-vs-authority audit**against the criterion above; likely record-authoritative for bindings with metadata seeding identity | Classify precisely, then either project (if the position *definition* is enforced from metadata) or make declared positions seed-only. |
306
-
|`sys_capability`| the record (curated registry read for capability existence) | record-authoritative (registry) |Seed-only; low authoring surface. Audit seed-not-clobber. |
304
+
|`sys_sharing_rule`| the record, live (`sharing-plugin.ts` "rule evaluation reads `sys_sharing_rule` live"; `sharing-rule-service``engine.find`) |**record-authoritative**|**Resolved (#2909 P0/T1)**: declared rules are a **boot seed**; the record is the authority; not projected. Rows carry readonly `managed_by` (unified A4 tri-state) + `customized` provenance; the seeder (`defineRule` in seed mode, `managedBy:'package'`) adopts pristine/legacy rows and keeps updating them, but **never overwrites admin-authored or `customized` rows** — an admin's `active:false` on an over-sharing rule survives redeploys. A `beforeUpdate` hook stamps `customized` on any non-system edit of a seeded row. |
305
+
|`sys_position`| mixed — position→permission-set resolution is metadata-first for the *sets*, but the `sys_position`**record** (incl. its `permissions` field, bindings, `delegatable`, `admin` gating) is read live by the anchor gate and `DelegatedAdminGate`|**record-authoritative**(bindings and lifecycle), with metadata seeding **identity + display only**|**Resolved (#2909 T2)**: declared positions are seed-only — `bootstrapDeclaredPositions` refreshes `label`/`description` and nothing else; bindings/`active`/`is_default`/`delegatable`/`managed_by` belong to the runtime/admin. Locked by `bootstrap-declared-positions.test.ts`. |
306
+
|`sys_capability`| the record (curated registry read for capability existence) | record-authoritative (registry) |**Resolved (#2909 T3)**: seed-not-clobber holds — `label`/`description` are platform-owned and refresh each boot; `managed_by`/`active` were already preserved; `scope` is an admin-editable classification face and is now **seed-once** (insert only). Locked by `bootstrap-system-capabilities.test.ts`. |
307
307
308
308
Only `sys_permission_set` was both metadata-authoritative **and** carried a
309
309
harmful, actively-drifting split-brain, which is why it was fixed first and in
310
-
full. The others are recorded here with their class so the follow-up work is
311
-
scoped, not rediscovered — tracked in framework#2909.
310
+
full. The follow-up work above landed via framework#2909.
311
+
312
+
#### Recorded tradeoffs (#2909)
313
+
314
+
-**Why `sys_sharing_rule` chose seed-not-clobber over a write gate
315
+
(projection-lite).** A self-consistent alternative existed: gate
316
+
package-managed rows against admin writes (as #2918 does for
317
+
`sys_position`/`sys_capability`), making the boot overwrite harmless. It was
318
+
rejected because sharing rules are a first-class admin authoring *and
319
+
tuning* surface in Setup — including narrowing or deactivating a seeded
320
+
rule that turned out to over-share. Locking package rows would amputate
321
+
that product capability, and the classification criterion (enforcement
322
+
reads the record) already points at record authority. Admin edits are
323
+
therefore *remembered* (`customized`), not blocked. Only the two provenance
324
+
columns themselves are `readonly` (engine-stripped from non-system
325
+
payloads), so provenance cannot be forged or cleared through the data door.
326
+
-**Known boundaries of the `customized` stamp**: multi-row updates (no
327
+
single `input.id`) are not stamped — every rule-editing UI path updates by
328
+
id; an admin who *deletes* a package rule gets it back pristine on the next
329
+
boot (delete ≠ customize; deactivation is the supported way to retire a
330
+
seeded rule); a "reset to package default" affordance (system-context clear
331
+
of `customized`) is future work.
332
+
-**`sys_capability.scope` is seed-once**: a curated scope change shipped in
333
+
a new platform version no longer propagates to existing rows and needs a
334
+
data migration. Accepted — silently reverting an admin's reclassification
0 commit comments