fsanyoto commited on
Commit
9411b94
Β·
verified Β·
1 Parent(s): 1f123e1

Deploy AIOS web (React glide grid + FastAPI slice)

Browse files
RELEASES.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "current": "0b2ab48",
3
  "releases": [
4
  {
5
  "version": "v52",
 
1
  {
2
+ "current": "240caca",
3
  "releases": [
4
  {
5
  "version": "v52",
VERSION CHANGED
@@ -1 +1 @@
1
- 0b2ab48
 
1
+ 240caca
api/aios_session.py CHANGED
@@ -1,124 +1,124 @@
1
- """aios_session.py β€” X3: the signed, STATELESS session cookie (EXIT-3a, 2026-07-30).
2
-
3
- aios_session = base64url(json {t,u,e,i,x}) . base64url(HMAC-SHA256)
4
-
5
- `t` tenant slug Β· `u` username Β· `e` the user's revocation epoch Β· `i` idle deadline Β·
6
- `x` absolute expiry. Signed with `AIOS_SESSION_SECRET` using stdlib `hmac` β€” no new dependency,
7
- and nothing to store server-side, which is the whole point: EXIT-4 requires a process that holds
8
- no per-tenant state, and a session TABLE is per-tenant state. D2's Postgres sessions-MIRROR (an
9
- audit view, not the source of truth) arrives with C-2 and is deliberately deferred.
10
-
11
- WHY STATELESS STILL REVOKES. A cookie nobody can delete sounds like a cookie nobody can revoke.
12
- The answer is the `epoch` integer on the user record (`core.users.epoch` / `bump_epoch`): the
13
- cookie carries the epoch it was minted under, every verification re-reads the account's current
14
- epoch, and a password change, a deactivation, or (wave 40, R2) any LOGIN bumps it β€” so every outstanding cookie for that
15
- user dies at once, without a session table.
16
-
17
- ⚠ NO FALLBACK SECRET, EVER. With `AIOS_SESSION_SECRET` unset this module mints a RANDOM
18
- per-process key: sessions then die on restart, which is an inconvenience. A hardcoded default
19
- would instead let anyone who has read this repo forge a cookie for any user of any tenant β€” an
20
- inconvenience is the correct trade against a forgery key. `EPHEMERAL_SECRET` records which
21
- happened so the app can say so at startup and a gate can assert it.
22
- """
23
- import base64
24
- import hmac
25
- import json
26
- import os
27
- import secrets
28
- import time
29
- from hashlib import sha256
30
-
31
- COOKIE_NAME = "aios_session"
32
- IDLE_SECONDS = 8 * 60 * 60 # D2: reissued on every authenticated request
33
- ABSOLUTE_SECONDS = 30 * 24 * 60 * 60 # D2: a hard ceiling no amount of activity extends
34
-
35
- _env_secret = os.environ.get("AIOS_SESSION_SECRET") or ""
36
- EPHEMERAL_SECRET = not _env_secret
37
- _SECRET = _env_secret.encode("utf-8") if _env_secret else secrets.token_bytes(32)
38
-
39
-
40
- def _b64(raw: bytes) -> str:
41
- return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
42
-
43
-
44
- def _unb64(txt: str) -> bytes:
45
- return base64.urlsafe_b64decode(txt + "=" * (-len(txt) % 4))
46
-
47
-
48
- def _sign(payload: bytes) -> str:
49
- return _b64(hmac.new(_SECRET, payload, sha256).digest())
50
-
51
-
52
- def mint(tenant, username, epoch, now=None, absolute_expiry=None):
53
- """A fresh cookie value. `absolute_expiry` carries over on reissue so refreshing the idle
54
- window can never extend the 30-day ceiling β€” that would make the ceiling unreachable."""
55
- now = int(now if now is not None else time.time())
56
- claims = {"t": str(tenant), "u": str(username), "e": int(epoch),
57
- "i": now + IDLE_SECONDS,
58
- "x": int(absolute_expiry if absolute_expiry is not None
59
- else now + ABSOLUTE_SECONDS)}
60
- payload = json.dumps(claims, separators=(",", ":"), sort_keys=True).encode("utf-8")
61
- return f"{_b64(payload)}.{_sign(payload)}", claims
62
-
63
-
64
- def read(value, now=None):
65
- """Parse + verify a cookie value β†’ claims dict, or None.
66
-
67
- None for EVERY failure mode β€” bad shape, bad signature, expired, unparseable. The caller
68
- answers 401 and must not learn which: an error that distinguishes "signature wrong" from
69
- "expired" tells a forger whether their key is right.
70
- """
71
- if not value or not isinstance(value, str) or value.count(".") != 1:
72
- return None
73
- body, sig = value.split(".", 1)
74
- try:
75
- payload = _unb64(body)
76
- except Exception:
77
- return None
78
- # compare_digest on the SIGNATURE, always β€” a byte-wise early exit is a timing oracle.
79
- if not hmac.compare_digest(sig, _sign(payload)):
80
- return None
81
- try:
82
- claims = json.loads(payload.decode("utf-8"))
83
- except Exception:
84
- return None
85
- if not isinstance(claims, dict):
86
- return None
87
- if not isinstance(claims.get("u"), str) or not isinstance(claims.get("t"), str):
88
- return None
89
- if not claims["u"] or not claims["t"]:
90
- return None
91
- try:
92
- idle, absolute, epoch = int(claims["i"]), int(claims["x"]), int(claims["e"])
93
- except (KeyError, TypeError, ValueError):
94
- return None
95
- now = int(now if now is not None else time.time())
96
- if now >= idle or now >= absolute:
97
- return None
98
- claims["e"], claims["i"], claims["x"] = epoch, idle, absolute
99
- return claims
100
-
101
-
102
- def is_secure_request(request):
103
- """Secure flag ON unless this is plain-HTTP localhost. Set unconditionally in production;
104
- unset on a local http:// dev origin, where a Secure cookie is simply never sent back and
105
- login would appear to succeed and then not work."""
106
- host = (request.headers.get("host") or "").split(":")[0].lower()
107
- proto = (request.headers.get("x-forwarded-proto")
108
- or request.url.scheme or "http").split(",")[0].strip().lower()
109
- if proto == "https":
110
- return True
111
- return host not in ("localhost", "127.0.0.1", "[::1]", "::1")
112
-
113
-
114
- def set_cookie(response, request, value):
115
- response.set_cookie(
116
- COOKIE_NAME, value, httponly=True, samesite="lax",
117
- secure=is_secure_request(request), max_age=ABSOLUTE_SECONDS, path="/")
118
-
119
-
120
- def clear_cookie(response, request):
121
- # Same attributes as when it was set: a browser matches on path/secure/samesite when
122
- # deleting, and a mismatched delete silently leaves the cookie in place.
123
- response.delete_cookie(COOKIE_NAME, path="/", httponly=True, samesite="lax",
124
- secure=is_secure_request(request))
 
1
+ """aios_session.py β€” X3: the signed, STATELESS session cookie (EXIT-3a, 2026-07-30).
2
+
3
+ aios_session = base64url(json {t,u,e,i,x}) . base64url(HMAC-SHA256)
4
+
5
+ `t` tenant slug Β· `u` username Β· `e` the user's revocation epoch Β· `i` idle deadline Β·
6
+ `x` absolute expiry. Signed with `AIOS_SESSION_SECRET` using stdlib `hmac` β€” no new dependency,
7
+ and nothing to store server-side, which is the whole point: EXIT-4 requires a process that holds
8
+ no per-tenant state, and a session TABLE is per-tenant state. D2's Postgres sessions-MIRROR (an
9
+ audit view, not the source of truth) arrives with C-2 and is deliberately deferred.
10
+
11
+ WHY STATELESS STILL REVOKES. A cookie nobody can delete sounds like a cookie nobody can revoke.
12
+ The answer is the `epoch` integer on the user record (`core.users.epoch` / `bump_epoch`): the
13
+ cookie carries the epoch it was minted under, every verification re-reads the account's current
14
+ epoch, and a password change, a deactivation, or (wave 40, R2) any LOGIN bumps it β€” so every outstanding cookie for that
15
+ user dies at once, without a session table.
16
+
17
+ ⚠ NO FALLBACK SECRET, EVER. With `AIOS_SESSION_SECRET` unset this module mints a RANDOM
18
+ per-process key: sessions then die on restart, which is an inconvenience. A hardcoded default
19
+ would instead let anyone who has read this repo forge a cookie for any user of any tenant β€” an
20
+ inconvenience is the correct trade against a forgery key. `EPHEMERAL_SECRET` records which
21
+ happened so the app can say so at startup and a gate can assert it.
22
+ """
23
+ import base64
24
+ import hmac
25
+ import json
26
+ import os
27
+ import secrets
28
+ import time
29
+ from hashlib import sha256
30
+
31
+ COOKIE_NAME = "aios_session"
32
+ IDLE_SECONDS = 8 * 60 * 60 # D2: reissued on every authenticated request
33
+ ABSOLUTE_SECONDS = 30 * 24 * 60 * 60 # D2: a hard ceiling no amount of activity extends
34
+
35
+ _env_secret = os.environ.get("AIOS_SESSION_SECRET") or ""
36
+ EPHEMERAL_SECRET = not _env_secret
37
+ _SECRET = _env_secret.encode("utf-8") if _env_secret else secrets.token_bytes(32)
38
+
39
+
40
+ def _b64(raw: bytes) -> str:
41
+ return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
42
+
43
+
44
+ def _unb64(txt: str) -> bytes:
45
+ return base64.urlsafe_b64decode(txt + "=" * (-len(txt) % 4))
46
+
47
+
48
+ def _sign(payload: bytes) -> str:
49
+ return _b64(hmac.new(_SECRET, payload, sha256).digest())
50
+
51
+
52
+ def mint(tenant, username, epoch, now=None, absolute_expiry=None):
53
+ """A fresh cookie value. `absolute_expiry` carries over on reissue so refreshing the idle
54
+ window can never extend the 30-day ceiling β€” that would make the ceiling unreachable."""
55
+ now = int(now if now is not None else time.time())
56
+ claims = {"t": str(tenant), "u": str(username), "e": int(epoch),
57
+ "i": now + IDLE_SECONDS,
58
+ "x": int(absolute_expiry if absolute_expiry is not None
59
+ else now + ABSOLUTE_SECONDS)}
60
+ payload = json.dumps(claims, separators=(",", ":"), sort_keys=True).encode("utf-8")
61
+ return f"{_b64(payload)}.{_sign(payload)}", claims
62
+
63
+
64
+ def read(value, now=None):
65
+ """Parse + verify a cookie value β†’ claims dict, or None.
66
+
67
+ None for EVERY failure mode β€” bad shape, bad signature, expired, unparseable. The caller
68
+ answers 401 and must not learn which: an error that distinguishes "signature wrong" from
69
+ "expired" tells a forger whether their key is right.
70
+ """
71
+ if not value or not isinstance(value, str) or value.count(".") != 1:
72
+ return None
73
+ body, sig = value.split(".", 1)
74
+ try:
75
+ payload = _unb64(body)
76
+ except Exception:
77
+ return None
78
+ # compare_digest on the SIGNATURE, always β€” a byte-wise early exit is a timing oracle.
79
+ if not hmac.compare_digest(sig, _sign(payload)):
80
+ return None
81
+ try:
82
+ claims = json.loads(payload.decode("utf-8"))
83
+ except Exception:
84
+ return None
85
+ if not isinstance(claims, dict):
86
+ return None
87
+ if not isinstance(claims.get("u"), str) or not isinstance(claims.get("t"), str):
88
+ return None
89
+ if not claims["u"] or not claims["t"]:
90
+ return None
91
+ try:
92
+ idle, absolute, epoch = int(claims["i"]), int(claims["x"]), int(claims["e"])
93
+ except (KeyError, TypeError, ValueError):
94
+ return None
95
+ now = int(now if now is not None else time.time())
96
+ if now >= idle or now >= absolute:
97
+ return None
98
+ claims["e"], claims["i"], claims["x"] = epoch, idle, absolute
99
+ return claims
100
+
101
+
102
+ def is_secure_request(request):
103
+ """Secure flag ON unless this is plain-HTTP localhost. Set unconditionally in production;
104
+ unset on a local http:// dev origin, where a Secure cookie is simply never sent back and
105
+ login would appear to succeed and then not work."""
106
+ host = (request.headers.get("host") or "").split(":")[0].lower()
107
+ proto = (request.headers.get("x-forwarded-proto")
108
+ or request.url.scheme or "http").split(",")[0].strip().lower()
109
+ if proto == "https":
110
+ return True
111
+ return host not in ("localhost", "127.0.0.1", "[::1]", "::1")
112
+
113
+
114
+ def set_cookie(response, request, value):
115
+ response.set_cookie(
116
+ COOKIE_NAME, value, httponly=True, samesite="lax",
117
+ secure=is_secure_request(request), max_age=ABSOLUTE_SECONDS, path="/")
118
+
119
+
120
+ def clear_cookie(response, request):
121
+ # Same attributes as when it was set: a browser matches on path/secure/samesite when
122
+ # deleting, and a mismatched delete silently leaves the cookie in place.
123
+ response.delete_cookie(COOKIE_NAME, path="/", httponly=True, samesite="lax",
124
+ secure=is_secure_request(request))
api/deps.py CHANGED
@@ -296,7 +296,20 @@ def assistant_read_scope(session: Session, database, fields=None, filters=None):
296
  perm_scope.validate_assistant_filter((grant or {}).get("filter"), canonical_fields)
297
  except ValueError as exc:
298
  _assistant_refuse(400, "assistant_bad_filter", str(exc))
299
- permitted_rows = (perm_scope.assistant_apply_row_scope(rows, session.user, key, canonical_fields)
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  if grant is not None else list(rows or ()))
301
  visible = (perm_scope.assistant_visible_fields(canonical_fields, session.user, key)
302
  if grant is not None else list(canonical_fields))
 
296
  perm_scope.validate_assistant_filter((grant or {}).get("filter"), canonical_fields)
297
  except ValueError as exc:
298
  _assistant_refuse(400, "assistant_bad_filter", str(exc))
299
+ # ⭐ OWNER I16 β€” the tenant handle rides into the row wall here too, so one stored rule means
300
+ # the same thing in the Analyst's answer as it does on the grid (see
301
+ # `perm_scope._enrich_for_wall`).
302
+ #
303
+ # ⚠ AND IT CHANGES NOTHING ON THIS PATH TODAY, WHICH IS STATED RATHER THAN LEFT TO BE
304
+ # REDISCOVERED. `validate_assistant_filter` three lines up refuses any leaf naming a column
305
+ # outside `canonical_fields`, so a wall on a user-generated column is rejected before it can
306
+ # reach this call at all. The handle is passed anyway because the alternative is a door that
307
+ # walls these modules WITHOUT it β€” and "all six or none" is the property that keeps one wall
308
+ # from having two meanings. Widening the Assistant's own validator to admit such a column is
309
+ # a separate decision about what E may read, not a side effect of a grid fix.
310
+ permitted_rows = (perm_scope.assistant_apply_row_scope(rows, session.user, key,
311
+ canonical_fields,
312
+ st=getattr(session, "runtime", None))
313
  if grant is not None else list(rows or ()))
314
  visible = (perm_scope.assistant_visible_fields(canonical_fields, session.user, key)
315
  if grant is not None else list(canonical_fields))
api/routes_admin.py CHANGED
The diff for this file is too large to render. See raw diff
 
api/routes_customers.py CHANGED
@@ -274,7 +274,21 @@ def grid_assembly(session: Session, scope: str = "customer", storage_key: str =
274
  # reasons: the assembled list is not built yet (it needs `pids`), and a permanent filter may
275
  # only ever name a canonical field anyway β€” `routes_admin._clean_perms` validates it against
276
  # exactly this schema and 400s otherwise. `permits()` denies on anything it cannot answer.
277
- rows_src = perm_scope.apply_row_scope(rows_src, session.user, MODULE, aios_grid.FIELDS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  pids = frozenset(r["pid"] for r in rows_src if r.get("pid") is not None)
279
  # ⭐ W38-T20 β€” THE COLUMN DEFINITIONS ARE READ **ONCE** PER ASSEMBLY AND THREADED, because
280
  # `core.store.get` deep-copies whatever it hands back on every call. Three consumers want this
@@ -488,12 +502,17 @@ def allowed_pids(session: Session):
488
  paths close that gap with `apply_row_scope`; without the same call here the write wall would
489
  be the wider set, and a restricted user could PATCH a row this API will not show them.
490
  Same function, same order as `grid_assembly`, so the two walls cannot drift.
 
 
 
 
 
491
  """
492
  import core.perm_scope as perm_scope
493
  import aios_grid
494
 
495
  rows = perm_scope.apply_row_scope(_pool_rows(session), session.user, MODULE,
496
- aios_grid.FIELDS)
497
  return frozenset(r["pid"] for r in rows if r.get("pid") is not None)
498
 
499
 
 
274
  # reasons: the assembled list is not built yet (it needs `pids`), and a permanent filter may
275
  # only ever name a canonical field anyway β€” `routes_admin._clean_perms` validates it against
276
  # exactly this schema and 400s otherwise. `permits()` denies on anything it cannot answer.
277
+ #
278
+ # ⭐⭐ OWNER I16 β€” `st=rt` IS WHAT MAKES A WALL ON A USER-GENERATED COLUMN MEAN ANYTHING
279
+ # HERE. *"Permission Filters must be able to filter on user-generated Fields too."* The
280
+ # sentence above is exactly why it was needed: the canonical list has no `custom_` column in
281
+ # it and these rows are PRE-OVERLAY, so such a leaf denied every row while the editor
282
+ # reported the rule saved. With the handle, `perm_scope._enrich_for_wall` merges the
283
+ # tenant-wide value for the named column onto a COPY of each row and declares it for the
284
+ # evaluator. Nothing else about this call changes, and a caller with no handle still gets
285
+ # the wall exactly as it was.
286
+ #
287
+ # β›” THE SAME HANDLE GOES TO `allowed_pids` BELOW, AND THE PAIR IS NOT OPTIONAL. That is the
288
+ # WRITE wall to this one's READ wall; lending it here alone would make a user-generated rule
289
+ # narrow what an account SEES while leaving what it may PATCH untouched.
290
+ rows_src = perm_scope.apply_row_scope(rows_src, session.user, MODULE, aios_grid.FIELDS,
291
+ st=rt)
292
  pids = frozenset(r["pid"] for r in rows_src if r.get("pid") is not None)
293
  # ⭐ W38-T20 β€” THE COLUMN DEFINITIONS ARE READ **ONCE** PER ASSEMBLY AND THREADED, because
294
  # `core.store.get` deep-copies whatever it hands back on every call. Three consumers want this
 
502
  paths close that gap with `apply_row_scope`; without the same call here the write wall would
503
  be the wider set, and a restricted user could PATCH a row this API will not show them.
504
  Same function, same order as `grid_assembly`, so the two walls cannot drift.
505
+
506
+ β›” AND `st` IS PART OF "SAME FUNCTION, SAME ORDER" (owner I16). `grid_assembly` lends the
507
+ tenant handle so a wall naming a user-generated column can be answered at all; without the
508
+ identical argument here that rule would narrow the READ and not the WRITE β€” the drift this
509
+ docstring already refuses, wearing a new argument's clothes.
510
  """
511
  import core.perm_scope as perm_scope
512
  import aios_grid
513
 
514
  rows = perm_scope.apply_row_scope(_pool_rows(session), session.user, MODULE,
515
+ aios_grid.FIELDS, st=session.runtime)
516
  return frozenset(r["pid"] for r in rows if r.get("pid") is not None)
517
 
518
 
api/routes_products.py CHANGED
@@ -115,7 +115,14 @@ def scoped_pool(session: Session):
115
  # The SAME wall the customer assembly applies, in the same order: rows first (before pids
116
  # are taken, so an out-of-filter row never enters allowed_pids), then the field closure,
117
  # then the values stripped from the rows as well as the field list.
118
- rows_src = perm_scope.apply_row_scope(rows_src, session.user, MODULE, fields_base)
 
 
 
 
 
 
 
119
  pids = frozenset(r["pid"] for r in rows_src if r.get("pid") is not None)
120
  return pids, team_id, rows_src, fields_base
121
 
 
115
  # The SAME wall the customer assembly applies, in the same order: rows first (before pids
116
  # are taken, so an out-of-filter row never enters allowed_pids), then the field closure,
117
  # then the values stripped from the rows as well as the field list.
118
+ # ⭐⭐ OWNER I16 β€” AND THE SAME TENANT HANDLE, for the same reason and with the same
119
+ # consequence if it is left off. `fields_base` is this topic's contract plus its declared
120
+ # tenant-wide columns; a column a USER made is in neither, and these rows are pre-overlay,
121
+ # so such a leaf denied every product row while the admin was told the rule saved. See
122
+ # `perm_scope._enrich_for_wall`. This route is BOTH walls at once (it returns the pids the
123
+ # write door uses), so there is no second call here to keep in step.
124
+ rows_src = perm_scope.apply_row_scope(rows_src, session.user, MODULE, fields_base,
125
+ st=session.runtime)
126
  pids = frozenset(r["pid"] for r in rows_src if r.get("pid") is not None)
127
  return pids, team_id, rows_src, fields_base
128
 
api/routes_slack.py CHANGED
@@ -182,13 +182,30 @@ def agent_visible_fields(agent, fields, module):
182
  return perm_scope.visible_fields(fields, agent_principal(agent), module)
183
 
184
 
185
- def agent_rows(agent, rows, module, fields, ctx=None):
186
  """The rows this channel may receive β€” `permits()`, so an unanswerable permanent filter DENIES
187
- rather than being ignored."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  import core.perm_scope as perm_scope
189
  p = agent_principal(agent)
190
  hide = perm_scope.hidden_keys(p, module, fields)
191
- kept = perm_scope.apply_row_scope(rows, p, module, fields, ctx)
192
  # Both wires, never one: the field list and the row payload are separate, and stripping only
193
  # the first leaves the value sitting in the second where anyone can read it.
194
  return [perm_scope.strip_row(r, hide) for r in kept]
 
182
  return perm_scope.visible_fields(fields, agent_principal(agent), module)
183
 
184
 
185
+ def agent_rows(agent, rows, module, fields, ctx=None, st=None):
186
  """The rows this channel may receive β€” `permits()`, so an unanswerable permanent filter DENIES
187
+ rather than being ignored.
188
+
189
+ ⭐⭐ `st` IS THE TENANT HANDLE AND IT IS HERE BECAUSE OF THE SENTENCE IN `agent_principal`:
190
+ a channel agent IS a principal of the same wall, so a permanent filter naming a
191
+ user-generated column has to mean the SAME thing here as it does on the grid (owner I16).
192
+ A door that lends the handle and a door that does not are one stored rule with two meanings.
193
+
194
+ ⚠ PASS THE TENANT'S OWN RUNTIME β€” every other store read in this file takes
195
+ `session.runtime`, and a channel is not a second tenancy. It is a keyword with a `None`
196
+ default for the same reason `perm_scope.apply_row_scope`'s is: without one this is
197
+ byte-identical to the wall that shipped before the argument existed.
198
+
199
+ ⚠ `hidden_keys` DELIBERATELY DOES NOT TAKE IT IN THIS CHANGE. Its field-grant leg
200
+ under-hides without a handle (see `perm_scope.visible_overlays`), which is a real and
201
+ separate gap on this surface β€” but it is the FIELD wall, it moves what a channel receives
202
+ rather than what I16 asked for, and widening two walls in one ticket is how a permission
203
+ change stops being reviewable.
204
+ """
205
  import core.perm_scope as perm_scope
206
  p = agent_principal(agent)
207
  hide = perm_scope.hidden_keys(p, module, fields)
208
+ kept = perm_scope.apply_row_scope(rows, p, module, fields, ctx, st=st)
209
  # Both wires, never one: the field list and the row payload are separate, and stripping only
210
  # the first leaves the value sitting in the second where anyone can read it.
211
  return [perm_scope.strip_row(r, hide) for r in kept]
platform/core/perm_scope.py CHANGED
The diff for this file is too large to render. See raw diff
 
platform/core/shared_overlay.py CHANGED
@@ -1,302 +1,327 @@
1
- """The TENANT-WIDE overlay stratum β€” one value per (row, column) for the whole workspace.
2
-
3
- Wave 29, item 20 / owner ruling R11, contract C5. The sibling of `core/table_store.py`, which
4
- holds the PER-USER strata (`store.get(key)[username]`) and always has.
5
-
6
- β›” THE DEFECT THIS EXISTS FOR IS NOT "SHARING WOULD BE NICE" β€” IT IS THAT A SHARED VIEW SILENTLY
7
- WIDENS. `modules/product_data.py` already states it, in the comment above the supplier master:
8
-
9
- "A user-created field and its values live in the PER-USER strata; only VIEWS are shared. So a
10
- shared 'Buy list' view that filtered on a user-created column would, for every OTHER account,
11
- name a column that does not exist β€” and an unknown column is an INACTIVE condition in the
12
- tri-state engine, which IGNORES it and therefore WIDENS. The buy list would silently show the
13
- whole catalogue to everyone but its author."
14
-
15
- That is why the four supplier columns were frozen as read-only CONTRACT columns rather than made
16
- editable, and why the owner's ask ("turn this Excel sheet into a User created Field that we can
17
- edit") has been parked for two waves. A column whose value is the same for every reader makes the
18
- shared view mean ONE thing, which is the precondition for editing it at all.
19
-
20
- ────────────────────────────────────────────────────────────────────────────────────────────────
21
- β›”β›” THIS MODULE IS NOT A PERMISSION WALL, AND MUST NEVER BECOME ONE BY ACCIDENT.
22
-
23
- It refuses no reader and no writer. The caller has already answered "may this session open this
24
- surface" (`user_tables.may_open`, `Session.require`, the BU pool) and this module answers only
25
- "what is stored". Two different questions; one of them belongs upstream, where the session is.
26
-
27
- What it DOES enforce is the one thing a caller can get wrong silently:
28
-
29
- ⭐ `cells(table_key, pids)` β€” `pids` IS REQUIRED, POSITIONALLY, AND THERE IS NO "EVERYTHING"
30
- CALL. A default of "all rows" is the widening hazard above wearing a friendly face: the
31
- caller passes the row set it has ALREADY scoped, so a cell for a row this reader may not see
32
- cannot come back to be merged. There is deliberately no `all_cells()` to reach for; a caller
33
- that genuinely needs the lot passes the lot, in writing, where a reviewer can see it.
34
-
35
- The row wall then holds twice over, because `aios_grid.rows_from_pool` iterates the POOL and looks
36
- each pid UP in the overlay β€” never the reverse. A cell for a row outside the pool has nothing to
37
- attach to. This module is built to keep that true rather than to re-implement it.
38
- ────────────────────────────────────────────────────────────────────────────────────────────────
39
-
40
- RESIDENCY. Its own bucket, `<table_key>__shared`, beside the per-user one β€” never a `__shared__`
41
- member inside it. Two reasons, and the first is `product_data.py`'s own argument for separate
42
- topic keys ("separate store keys make that structurally impossible, which is the whole reason the
43
- table-page factory exists"): a per-user reader iterating usernames cannot encounter shared data
44
- when there is no shared data in that bucket to encounter. The second is cost β€” the per-user bucket
45
- carries every user's views, and a cell edit should not read-modify-write all of it.
46
-
47
- SHAPE, chosen to be drop-in:
48
-
49
- {"fields": {field_key: Field}, # the tenant-wide column DEFINITIONS
50
- "cells": {"<pid>": {field_key: value}}} # exactly `rows_from_pool(overlays=...)`'s shape
51
-
52
- `cells()` returns STRING pid keys for that reason β€” `rows_from_pool` does `overlays.get(str(pid))`,
53
- so a caller merges the shared stratum over the per-user one with one `dict.update` and cannot get
54
- the key type wrong. (`table_store` stores `{str(pid): {...}}` too; one shape, three readers.)
55
- """
56
- import core.store as store
57
-
58
- #: The suffix that turns a topic's per-user workspace key into its shared one. Callers pass the
59
- #: key they ALREADY hold (`product_data.TABLE_KEY`, `f'{ut_key}_table_workspace'`) β€” one
60
- #: identifier for both strata, so there is no second naming convention to get wrong.
61
- BUCKET_SUFFIX = '__shared'
62
-
63
- #: One cell value's ceiling, matching the `json` field kind's. A shared cell is read by every user
64
- #: in the tenant, so an unbounded one is an unbounded cost for all of them.
65
- MAX_VALUE_CHARS = 32_768
66
-
67
-
68
- def bucket(table_key):
69
- """The store key this topic's shared stratum lives under."""
70
- key = str(table_key or '').strip()
71
- if not key:
72
- raise ValueError('shared_overlay: a table_key is required β€” it names the bucket')
73
- return f'{key}{BUCKET_SUFFIX}'
74
-
75
-
76
- def _st(st):
77
- return st if st is not None else store
78
-
79
-
80
- def _read(table_key, st=None):
81
- """The whole stratum, always in its full two-key shape. Lenient like every other display
82
- read: an unreachable store degrades to "nothing shared yet", never to an exception on a page
83
- that would otherwise render."""
84
- try:
85
- data = _st(st).get(bucket(table_key)) or {}
86
- except Exception:
87
- data = {}
88
- return {'fields': dict(data.get('fields') or {}),
89
- 'cells': dict(data.get('cells') or {})}
90
-
91
-
92
- def _write(table_key, change, st=None, flush='async'):
93
- """Read-modify-write one shared stratum.
94
-
95
- `flush='async'` by default for the same reason `table_store._update` uses it: a typed cell
96
- lands here inside the request round-trip, and the historical synchronous hub commit cost
97
- seconds per keystroke. Structural writes (a field definition) pass `flush='sync'` β€” they are
98
- rare, and a lost column definition is a worse failure than a lost keystroke.
99
- """
100
- def _up(data):
101
- data = data if isinstance(data, dict) else {}
102
- data.setdefault('fields', {})
103
- data.setdefault('cells', {})
104
- change(data)
105
- return data
106
- return _st(st).update(bucket(table_key), _up, flush=flush)
107
-
108
-
109
- def _pid(pid):
110
- """Pids are ints everywhere in the grid and strings in JSON. ONE coercion, here, so a caller
111
- passing either cannot write a row that a reader keyed the other way never finds."""
112
- return str(int(pid))
113
-
114
-
115
- def _value(value):
116
- """A cell holds a SCALAR. The Row contract is scalar on every surface β€” the grid, the filter
117
- engine, formulas, export β€” and the `json` kind is a validated STRING rather than an object.
118
-
119
- β›” A dict or a list RAISES rather than being dropped. This is called by our own code, so a
120
- non-scalar is a programming error, and silently storing nothing would surface later as "the
121
- shared column is blank for everyone" with no failure anywhere near the cause.
122
- """
123
- if isinstance(value, (dict, list, tuple, set)):
124
- raise ValueError(f'shared_overlay: a cell holds a scalar, not {type(value).__name__}')
125
- if value is None or isinstance(value, bool) or isinstance(value, (int, float)):
126
- return value
127
- text = str(value)
128
- return text[:MAX_VALUE_CHARS]
129
-
130
-
131
- # ----------------------------------------------------------------- the column DEFINITIONS
132
- def fields(table_key, st=None):
133
- """`{field_key: Field}` β€” the columns this topic shares tenant-wide.
134
-
135
- Unscoped on purpose, and it is the one thing here that is: a shared column's EXISTENCE is
136
- tenant-wide by definition β€” that is the whole feature, and it is what stops a shared view
137
- naming a column half the workspace lacks. Its VALUES are scoped by `cells()`.
138
- """
139
- return _read(table_key, st)['fields']
140
-
141
-
142
- def is_shared(table_key, field_key, st=None):
143
- """Is this column's value tenant-wide? The predicate a caller uses to decide WHICH stratum to
144
- read and write β€” one evaluator, so the read path and the write path cannot disagree about
145
- where a given column lives ([[one-evaluator-per-question]])."""
146
- return str(field_key or '') in _read(table_key, st)['fields']
147
-
148
-
149
- def put_field(table_key, field_key, defn, st=None):
150
- """Declare (or redefine) a shared column. Returns what was stored."""
151
- key = str(field_key or '').strip()
152
- if not key:
153
- raise ValueError('shared_overlay: a field needs a key')
154
- entry = dict(defn or {})
155
- entry['key'] = key
156
-
157
- def _add(data):
158
- data['fields'][key] = entry
159
-
160
- _write(table_key, _add, st, flush='sync')
161
- return entry
162
-
163
-
164
- def drop_field(table_key, field_key, st=None):
165
- """Remove a shared column AND every stored value for it.
166
-
167
- β›” The cells go with the definition, exactly as `table_store.delete_field` scrubs its own:
168
- orphaned values would silently resurface if the key were ever reused β€” and a resurrected
169
- value in a TENANT-WIDE stratum reappears for everybody at once.
170
- """
171
- key = str(field_key or '').strip()
172
- if not key:
173
- return False
174
- hit = [False]
175
-
176
- def _drop(data):
177
- hit[0] = data['fields'].pop(key, None) is not None
178
- for row in data['cells'].values():
179
- if isinstance(row, dict):
180
- row.pop(key, None)
181
- # ⚠ AND DROP THE ROWS THAT ARE NOW EMPTY. `cells()` already skips a blank row, so this
182
- # changes nothing a reader sees β€” but this bucket is read by every user in the tenant,
183
- # and a `{}` per pid that once held a since-deleted column is dead weight that only ever
184
- # grows. Caught by `prune()` reporting rows nobody could see as dropped.
185
- for pid in [p for p, row in data['cells'].items() if not row]:
186
- data['cells'].pop(pid, None)
187
-
188
- _write(table_key, _drop, st, flush='sync')
189
- return hit[0]
190
-
191
-
192
- # ----------------------------------------------------------------------------- the VALUES
193
- def cells(table_key, pids, st=None):
194
- """`{"<pid>": {field_key: value}}` for the rows named by `pids` β€” and ONLY those.
195
-
196
- β›” `pids` IS REQUIRED. See the module header: this is the row wall expressed as a signature,
197
- so a caller cannot get "every shared cell in the tenant" by forgetting an argument. Pass the
198
- pool you have already scoped for this session.
199
-
200
- An empty `pids` legitimately returns `{}` β€” a reader with no rows sees no cells, which is the
201
- correct answer rather than a special case.
202
- """
203
- if pids is None:
204
- raise TypeError('shared_overlay.cells: pids is required β€” pass the row set you have '
205
- 'already scoped for this reader (there is deliberately no "all" call)')
206
- wanted = {_pid(p) for p in pids}
207
- if not wanted:
208
- return {}
209
- stored = _read(table_key, st)['cells']
210
- return {pid: dict(row) for pid, row in stored.items()
211
- if pid in wanted and isinstance(row, dict) and row}
212
-
213
-
214
- def put_cell(table_key, pid, field_key, value, st=None):
215
- """Write ONE shared cell. Returns the value as stored (which may be truncated)."""
216
- return put_cells(table_key, pid, {field_key: value}, st=st).get(str(field_key or ''))
217
-
218
-
219
- def put_cells(table_key, pid, values, st=None):
220
- """Write several shared cells on one row, in one store update. Returns what was stored."""
221
- row_id = _pid(pid)
222
- clean = {str(k): _value(v) for k, v in dict(values or {}).items() if str(k)}
223
- if not clean:
224
- return {}
225
-
226
- def _patch(data):
227
- data['cells'].setdefault(row_id, {}).update(clean)
228
-
229
- _write(table_key, _patch, st)
230
- return clean
231
-
232
-
233
- def put_rows(table_key, rows, st=None):
234
- """Write shared cells on MANY rows in ONE store update. Returns `{row_id: {key: value}}`.
235
-
236
- ⭐⭐ THE WHOLE POINT IS THE *ONE*, AND IT IS NOT AN OPTIMISATION. `put_cells` is one row per
237
- store write, so a 1,397-row catalog import is 1,397 download-modify-upload cycles against one
238
- JSON document. This repo has a MEASURED scar for that shape: **18 writes against one document
239
- under the store's coalescing single-flight landed ZERO while answering 200 eighteen times.**
240
- Batching is what makes a bulk import land at all, not what makes it fast.
241
-
242
- ⚠ It deliberately takes the WHOLE SET rather than accepting a stream: a caller that loops over
243
- this function has simply rebuilt `put_cells` with extra steps, and the failure it reintroduces
244
- is silent. If the set does not fit in memory it does not fit in this store either β€” that is a
245
- signal to change substrate, not to chunk.
246
-
247
- β›” NO CAP HERE, ON PURPOSE. The bound belongs at the DOOR, where a caller identity and a
248
- reportable refusal exist (`routes_grid.bulk_cells`). A silent ceiling in a store primitive is
249
- exactly the shape the standing no-cap rule forbids.
250
- """
251
- clean = {}
252
- for pid, values in dict(rows or {}).items():
253
- row_id = _pid(pid)
254
- cells_for_row = {str(k): _value(v) for k, v in dict(values or {}).items() if str(k)}
255
- if cells_for_row:
256
- clean.setdefault(row_id, {}).update(cells_for_row)
257
- if not clean:
258
- return {}
259
-
260
- def _patch(data):
261
- for row_id, values in clean.items():
262
- data['cells'].setdefault(row_id, {}).update(values)
263
-
264
- # `flush='sync'` because a bulk import must be durable when the call returns: the caller is a
265
- # script that will report "1,397 rows written" and exit, and an async flush would make that
266
- # sentence a prediction rather than a fact.
267
- _write(table_key, _patch, st, flush='sync')
268
- return clean
269
-
270
-
271
- def clear_row(table_key, pid, st=None):
272
- """Forget every shared cell on one row. True when there was something to forget."""
273
- row_id = _pid(pid)
274
- hit = [False]
275
-
276
- def _clear(data):
277
- hit[0] = data['cells'].pop(row_id, None) is not None
278
-
279
- _write(table_key, _clear, st)
280
- return hit[0]
281
-
282
-
283
- def prune(table_key, live_pids, st=None):
284
- """Drop shared cells for rows that no longer exist. Returns how many rows were dropped.
285
-
286
- β›” NOT a scoped read wearing another name, and the difference is the whole reason `cells()`
287
- refuses to serve everything: `live_pids` here means *every row this TABLE has*, which is a
288
- fact about the data, whereas `pids` in `cells()` means *every row this READER may see*, which
289
- is a fact about the session. Calling this with one session's pool would delete the shared
290
- values of every row that session cannot see. The name says which is which; so does this note.
291
- """
292
- keep = {_pid(p) for p in live_pids}
293
- dropped = [0]
294
-
295
- def _prune(data):
296
- gone = [pid for pid in data['cells'] if pid not in keep]
297
- for pid in gone:
298
- data['cells'].pop(pid, None)
299
- dropped[0] = len(gone)
300
-
301
- _write(table_key, _prune, st, flush='sync')
302
- return dropped[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The TENANT-WIDE overlay stratum β€” one value per (row, column) for the whole workspace.
2
+
3
+ Wave 29, item 20 / owner ruling R11, contract C5. The sibling of `core/table_store.py`, which
4
+ holds the PER-USER strata (`store.get(key)[username]`) and always has.
5
+
6
+ β›” THE DEFECT THIS EXISTS FOR IS NOT "SHARING WOULD BE NICE" β€” IT IS THAT A SHARED VIEW SILENTLY
7
+ WIDENS. `modules/product_data.py` already states it, in the comment above the supplier master:
8
+
9
+ "A user-created field and its values live in the PER-USER strata; only VIEWS are shared. So a
10
+ shared 'Buy list' view that filtered on a user-created column would, for every OTHER account,
11
+ name a column that does not exist β€” and an unknown column is an INACTIVE condition in the
12
+ tri-state engine, which IGNORES it and therefore WIDENS. The buy list would silently show the
13
+ whole catalogue to everyone but its author."
14
+
15
+ That is why the four supplier columns were frozen as read-only CONTRACT columns rather than made
16
+ editable, and why the owner's ask ("turn this Excel sheet into a User created Field that we can
17
+ edit") has been parked for two waves. A column whose value is the same for every reader makes the
18
+ shared view mean ONE thing, which is the precondition for editing it at all.
19
+
20
+ ────────────────────────────────────────────────────────────────────────────────────────────────
21
+ β›”β›” THIS MODULE IS NOT A PERMISSION WALL, AND MUST NEVER BECOME ONE BY ACCIDENT.
22
+
23
+ It refuses no reader and no writer. The caller has already answered "may this session open this
24
+ surface" (`user_tables.may_open`, `Session.require`, the BU pool) and this module answers only
25
+ "what is stored". Two different questions; one of them belongs upstream, where the session is.
26
+
27
+ What it DOES enforce is the one thing a caller can get wrong silently:
28
+
29
+ ⭐ `cells(table_key, pids)` β€” `pids` IS REQUIRED, POSITIONALLY, AND THERE IS NO "EVERYTHING"
30
+ CALL. A default of "all rows" is the widening hazard above wearing a friendly face: the
31
+ caller passes the row set it has ALREADY scoped, so a cell for a row this reader may not see
32
+ cannot come back to be merged. There is deliberately no `all_cells()` to reach for; a caller
33
+ that genuinely needs the lot passes the lot, in writing, where a reviewer can see it.
34
+
35
+ The row wall then holds twice over, because `aios_grid.rows_from_pool` iterates the POOL and looks
36
+ each pid UP in the overlay β€” never the reverse. A cell for a row outside the pool has nothing to
37
+ attach to. This module is built to keep that true rather than to re-implement it.
38
+ ────────────────────────────────────────────────────────────────────────────────────────────────
39
+
40
+ RESIDENCY. Its own bucket, `<table_key>__shared`, beside the per-user one β€” never a `__shared__`
41
+ member inside it. Two reasons, and the first is `product_data.py`'s own argument for separate
42
+ topic keys ("separate store keys make that structurally impossible, which is the whole reason the
43
+ table-page factory exists"): a per-user reader iterating usernames cannot encounter shared data
44
+ when there is no shared data in that bucket to encounter. The second is cost β€” the per-user bucket
45
+ carries every user's views, and a cell edit should not read-modify-write all of it.
46
+
47
+ SHAPE, chosen to be drop-in:
48
+
49
+ {"fields": {field_key: Field}, # the tenant-wide column DEFINITIONS
50
+ "cells": {"<pid>": {field_key: value}}} # exactly `rows_from_pool(overlays=...)`'s shape
51
+
52
+ `cells()` returns STRING pid keys for that reason β€” `rows_from_pool` does `overlays.get(str(pid))`,
53
+ so a caller merges the shared stratum over the per-user one with one `dict.update` and cannot get
54
+ the key type wrong. (`table_store` stores `{str(pid): {...}}` too; one shape, three readers.)
55
+ """
56
+ import core.store as store
57
+
58
+ #: The suffix that turns a topic's per-user workspace key into its shared one. Callers pass the
59
+ #: key they ALREADY hold (`product_data.TABLE_KEY`, `f'{ut_key}_table_workspace'`) β€” one
60
+ #: identifier for both strata, so there is no second naming convention to get wrong.
61
+ BUCKET_SUFFIX = '__shared'
62
+
63
+ #: One cell value's ceiling, matching the `json` field kind's. A shared cell is read by every user
64
+ #: in the tenant, so an unbounded one is an unbounded cost for all of them.
65
+ MAX_VALUE_CHARS = 32_768
66
+
67
+
68
+ def bucket(table_key):
69
+ """The store key this topic's shared stratum lives under."""
70
+ key = str(table_key or '').strip()
71
+ if not key:
72
+ raise ValueError('shared_overlay: a table_key is required β€” it names the bucket')
73
+ return f'{key}{BUCKET_SUFFIX}'
74
+
75
+
76
+ def _st(st):
77
+ return st if st is not None else store
78
+
79
+
80
+ def _read(table_key, st=None):
81
+ """The whole stratum, always in its full two-key shape. Lenient like every other display
82
+ read: an unreachable store degrades to "nothing shared yet", never to an exception on a page
83
+ that would otherwise render."""
84
+ try:
85
+ data = _st(st).get(bucket(table_key)) or {}
86
+ except Exception:
87
+ data = {}
88
+ return {'fields': dict(data.get('fields') or {}),
89
+ 'cells': dict(data.get('cells') or {})}
90
+
91
+
92
+ def _write(table_key, change, st=None, flush='async'):
93
+ """Read-modify-write one shared stratum.
94
+
95
+ `flush='async'` by default for the same reason `table_store._update` uses it: a typed cell
96
+ lands here inside the request round-trip, and the historical synchronous hub commit cost
97
+ seconds per keystroke. Structural writes (a field definition) pass `flush='sync'` β€” they are
98
+ rare, and a lost column definition is a worse failure than a lost keystroke.
99
+ """
100
+ def _up(data):
101
+ data = data if isinstance(data, dict) else {}
102
+ data.setdefault('fields', {})
103
+ data.setdefault('cells', {})
104
+ change(data)
105
+ return data
106
+ return _st(st).update(bucket(table_key), _up, flush=flush)
107
+
108
+
109
+ def _pid(pid):
110
+ """Pids are ints everywhere in the grid and strings in JSON. ONE coercion, here, so a caller
111
+ passing either cannot write a row that a reader keyed the other way never finds."""
112
+ return str(int(pid))
113
+
114
+
115
+ def _value(value):
116
+ """A cell holds a SCALAR. The Row contract is scalar on every surface β€” the grid, the filter
117
+ engine, formulas, export β€” and the `json` kind is a validated STRING rather than an object.
118
+
119
+ β›” A dict or a list RAISES rather than being dropped. This is called by our own code, so a
120
+ non-scalar is a programming error, and silently storing nothing would surface later as "the
121
+ shared column is blank for everyone" with no failure anywhere near the cause.
122
+ """
123
+ if isinstance(value, (dict, list, tuple, set)):
124
+ raise ValueError(f'shared_overlay: a cell holds a scalar, not {type(value).__name__}')
125
+ if value is None or isinstance(value, bool) or isinstance(value, (int, float)):
126
+ return value
127
+ text = str(value)
128
+ return text[:MAX_VALUE_CHARS]
129
+
130
+
131
+ # ----------------------------------------------------------------- the column DEFINITIONS
132
+ def fields(table_key, st=None):
133
+ """`{field_key: Field}` β€” the columns this topic shares tenant-wide.
134
+
135
+ Unscoped on purpose, and it is the one thing here that is: a shared column's EXISTENCE is
136
+ tenant-wide by definition β€” that is the whole feature, and it is what stops a shared view
137
+ naming a column half the workspace lacks. Its VALUES are scoped by `cells()`.
138
+ """
139
+ return _read(table_key, st)['fields']
140
+
141
+
142
+ def is_shared(table_key, field_key, st=None):
143
+ """Is this column's value tenant-wide? The predicate a caller uses to decide WHICH stratum to
144
+ read and write β€” one evaluator, so the read path and the write path cannot disagree about
145
+ where a given column lives ([[one-evaluator-per-question]])."""
146
+ return str(field_key or '') in _read(table_key, st)['fields']
147
+
148
+
149
+ def put_field(table_key, field_key, defn, st=None):
150
+ """Declare (or redefine) a shared column. Returns what was stored."""
151
+ key = str(field_key or '').strip()
152
+ if not key:
153
+ raise ValueError('shared_overlay: a field needs a key')
154
+ entry = dict(defn or {})
155
+ entry['key'] = key
156
+
157
+ def _add(data):
158
+ data['fields'][key] = entry
159
+
160
+ _write(table_key, _add, st, flush='sync')
161
+ return entry
162
+
163
+
164
+ def drop_field(table_key, field_key, st=None):
165
+ """Remove a shared column AND every stored value for it.
166
+
167
+ β›” The cells go with the definition, exactly as `table_store.delete_field` scrubs its own:
168
+ orphaned values would silently resurface if the key were ever reused β€” and a resurrected
169
+ value in a TENANT-WIDE stratum reappears for everybody at once.
170
+ """
171
+ key = str(field_key or '').strip()
172
+ if not key:
173
+ return False
174
+ hit = [False]
175
+
176
+ def _drop(data):
177
+ hit[0] = data['fields'].pop(key, None) is not None
178
+ for row in data['cells'].values():
179
+ if isinstance(row, dict):
180
+ row.pop(key, None)
181
+ # ⚠ AND DROP THE ROWS THAT ARE NOW EMPTY. `cells()` already skips a blank row, so this
182
+ # changes nothing a reader sees β€” but this bucket is read by every user in the tenant,
183
+ # and a `{}` per pid that once held a since-deleted column is dead weight that only ever
184
+ # grows. Caught by `prune()` reporting rows nobody could see as dropped.
185
+ for pid in [p for p, row in data['cells'].items() if not row]:
186
+ data['cells'].pop(pid, None)
187
+
188
+ _write(table_key, _drop, st, flush='sync')
189
+ return hit[0]
190
+
191
+
192
+ # ----------------------------------------------------------------------------- the VALUES
193
+ def cells(table_key, pids, st=None):
194
+ """`{"<pid>": {field_key: value}}` for the rows named by `pids` β€” and ONLY those.
195
+
196
+ β›” `pids` IS REQUIRED. See the module header: this is the row wall expressed as a signature,
197
+ so a caller cannot get "every shared cell in the tenant" by forgetting an argument. Pass the
198
+ pool you have already scoped for this session.
199
+
200
+ An empty `pids` legitimately returns `{}` β€” a reader with no rows sees no cells, which is the
201
+ correct answer rather than a special case.
202
+ """
203
+ if pids is None:
204
+ raise TypeError('shared_overlay.cells: pids is required β€” pass the row set you have '
205
+ 'already scoped for this reader (there is deliberately no "all" call)')
206
+ wanted = {_pid(p) for p in pids}
207
+ if not wanted:
208
+ return {}
209
+ stored = _read(table_key, st)['cells']
210
+ return {pid: dict(row) for pid, row in stored.items()
211
+ if pid in wanted and isinstance(row, dict) and row}
212
+
213
+
214
+ def snapshot(table_key, pids, st=None):
215
+ """`(fields, cells)` for `pids` β€” the shared stratum's SCHEMA and VALUES, from ONE read.
216
+
217
+ β›”β›” THIS EXISTS BECAUSE TWO READS OF THIS BUCKET CAN DISAGREE, AND THE DISAGREEMENT OPENS A
218
+ WALL. `perm_scope._enrich_for_wall` needs both halves: the schema decides WHICH user columns
219
+ the row wall may answer, and the cells are the values it answers with. Taking them as two
220
+ `st.get()` calls leaves a gap in which a concurrent write can land, and a wave-40 adversarial
221
+ probe drove exactly that: with the second read returning an emptied `cells`, a blank is merged
222
+ for a key the store cannot really serve and `custom_x is not West` flips from denying every row
223
+ to ADMITTING a row whose true value IS West. `is empty` does the same. The set and the values
224
+ must come from one snapshot or they are not talking about the same tenant state.
225
+
226
+ ⚠ `pids` IS REQUIRED, exactly as in `cells()` β€” the row wall expressed as a signature, so no
227
+ caller can get "every shared cell in the tenant" by forgetting an argument.
228
+ """
229
+ if pids is None:
230
+ raise TypeError('shared_overlay.snapshot: pids is required β€” pass the row set you have '
231
+ 'already scoped for this reader (there is deliberately no "all" call)')
232
+ stored = _read(table_key, st)
233
+ wanted = {_pid(p) for p in pids}
234
+ served = {pid: dict(row) for pid, row in stored['cells'].items()
235
+ if pid in wanted and isinstance(row, dict) and row} if wanted else {}
236
+ return dict(stored['fields']), served
237
+
238
+
239
+ def put_cell(table_key, pid, field_key, value, st=None):
240
+ """Write ONE shared cell. Returns the value as stored (which may be truncated)."""
241
+ return put_cells(table_key, pid, {field_key: value}, st=st).get(str(field_key or ''))
242
+
243
+
244
+ def put_cells(table_key, pid, values, st=None):
245
+ """Write several shared cells on one row, in one store update. Returns what was stored."""
246
+ row_id = _pid(pid)
247
+ clean = {str(k): _value(v) for k, v in dict(values or {}).items() if str(k)}
248
+ if not clean:
249
+ return {}
250
+
251
+ def _patch(data):
252
+ data['cells'].setdefault(row_id, {}).update(clean)
253
+
254
+ _write(table_key, _patch, st)
255
+ return clean
256
+
257
+
258
+ def put_rows(table_key, rows, st=None):
259
+ """Write shared cells on MANY rows in ONE store update. Returns `{row_id: {key: value}}`.
260
+
261
+ ⭐⭐ THE WHOLE POINT IS THE *ONE*, AND IT IS NOT AN OPTIMISATION. `put_cells` is one row per
262
+ store write, so a 1,397-row catalog import is 1,397 download-modify-upload cycles against one
263
+ JSON document. This repo has a MEASURED scar for that shape: **18 writes against one document
264
+ under the store's coalescing single-flight landed ZERO while answering 200 eighteen times.**
265
+ Batching is what makes a bulk import land at all, not what makes it fast.
266
+
267
+ ⚠ It deliberately takes the WHOLE SET rather than accepting a stream: a caller that loops over
268
+ this function has simply rebuilt `put_cells` with extra steps, and the failure it reintroduces
269
+ is silent. If the set does not fit in memory it does not fit in this store either β€” that is a
270
+ signal to change substrate, not to chunk.
271
+
272
+ β›” NO CAP HERE, ON PURPOSE. The bound belongs at the DOOR, where a caller identity and a
273
+ reportable refusal exist (`routes_grid.bulk_cells`). A silent ceiling in a store primitive is
274
+ exactly the shape the standing no-cap rule forbids.
275
+ """
276
+ clean = {}
277
+ for pid, values in dict(rows or {}).items():
278
+ row_id = _pid(pid)
279
+ cells_for_row = {str(k): _value(v) for k, v in dict(values or {}).items() if str(k)}
280
+ if cells_for_row:
281
+ clean.setdefault(row_id, {}).update(cells_for_row)
282
+ if not clean:
283
+ return {}
284
+
285
+ def _patch(data):
286
+ for row_id, values in clean.items():
287
+ data['cells'].setdefault(row_id, {}).update(values)
288
+
289
+ # `flush='sync'` because a bulk import must be durable when the call returns: the caller is a
290
+ # script that will report "1,397 rows written" and exit, and an async flush would make that
291
+ # sentence a prediction rather than a fact.
292
+ _write(table_key, _patch, st, flush='sync')
293
+ return clean
294
+
295
+
296
+ def clear_row(table_key, pid, st=None):
297
+ """Forget every shared cell on one row. True when there was something to forget."""
298
+ row_id = _pid(pid)
299
+ hit = [False]
300
+
301
+ def _clear(data):
302
+ hit[0] = data['cells'].pop(row_id, None) is not None
303
+
304
+ _write(table_key, _clear, st)
305
+ return hit[0]
306
+
307
+
308
+ def prune(table_key, live_pids, st=None):
309
+ """Drop shared cells for rows that no longer exist. Returns how many rows were dropped.
310
+
311
+ β›” NOT a scoped read wearing another name, and the difference is the whole reason `cells()`
312
+ refuses to serve everything: `live_pids` here means *every row this TABLE has*, which is a
313
+ fact about the data, whereas `pids` in `cells()` means *every row this READER may see*, which
314
+ is a fact about the session. Calling this with one session's pool would delete the shared
315
+ values of every row that session cannot see. The name says which is which; so does this note.
316
+ """
317
+ keep = {_pid(p) for p in live_pids}
318
+ dropped = [0]
319
+
320
+ def _prune(data):
321
+ gone = [pid for pid in data['cells'] if pid not in keep]
322
+ for pid in gone:
323
+ data['cells'].pop(pid, None)
324
+ dropped[0] = len(gone)
325
+
326
+ _write(table_key, _prune, st, flush='sync')
327
+ return dropped[0]
web/src/settings/permsModel.ts CHANGED
The diff for this file is too large to render. See raw diff