Skip to content

Commit 8e3668c

Browse files
committed
Merge origin/main into claude/project-thread-jcuzjj
2 parents f09120b + c4a4aef commit 8e3668c

33 files changed

Lines changed: 1779 additions & 211 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@object-ui/app-shell': patch
4+
---
5+
6+
fix(spec)!: an object that declares one action name twice is refused at every door that parses it, and Studio never writes the pair
7+
8+
Clause-②: no (narrowing)
9+
Metadata-protocol: narrowed
10+
11+
**BREAKING** for an object whose `actions` array repeats a name.
12+
13+
- **Before.** The object save door accepted two entries of one object's `actions` under one name: `PUT /api/v1/meta/object/X?mode=draft` answered 200, the publish answered 200, and both were served. An action name is the key the action runs under on its object, so which declaration ran was undefined. `defineStack` and `os validate` already refused the pair inside a stack; the save door parses an object alone and did not.
14+
- **Now.** `ObjectSchema` refuses the pair, so every door that parses an object refuses it: `defineStack`, `os validate`, `os build` and the object save door (422 `INVALID_METADATA`). One issue per repeated name, at `actions[j]` of its second declaration, naming the name and every index: `Object 'X' declares the action 'Y' twice in actions: actions[i] and actions[j]. …`. Inside a stack the pair is now refused by the object parse (`STACK_SCHEMA_INVALID`) rather than by the cross-reference pass's duplicate action-key check, which keeps the pairs that span two collections (a bound standalone action beside an embedded one, two standalone actions).
15+
- **Not changed.** Distinct names, the same name on two different objects, and a standalone action bound to an object by `objectName` (judged by `defineStack` for one stack and reported by the runtime's bound-action collision warning across packages).
16+
- **Fix.** Rename all but one of the colliding entries (and every button, flow and caller that runs the renamed one by name), or delete the duplicate.
17+
- **Stored rows.** A stored object that repeats a name still loads, under a `[metadata_spec_invalid]` diagnostic carrying the refusal, and its next save is refused. `os migrate meta --stored` lists each such object under the TODO id `object-duplicate-action-name-refused`; nothing rewrites it, because which declaration keeps the name only the owner knows.
18+
- **Studio** (`@object-ui/app-shell`). New already derived a name no listed action held; renaming an action in the editor's Name field to a name another action of the object holds now writes the first free `<name>_N` instead of the duplicate.
19+
20+
<!-- adr-0087: registered object-duplicate-action-name-refused -->
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): "Add filter" in the Studio list-view inspector adds a row that can be edited
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The `filter-builder` widget (the Filter row of the Studio list-view inspector and of each tab preset) now holds its condition rows until they are complete. It used to rebuild the rows from the stored rules on every render, and the save-side fold drops a row that is not complete yet, so the blank row "Add filter" inserts vanished on the next render and no list-view filter could be authored in Studio. An incomplete row now stays on screen and writes nothing; completing it writes exactly the complete rule. What is saved is unchanged: an incomplete rule still never reaches the view, and a stored filter shows as its rows and round-trips as stored.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
---
4+
5+
fix(plugin-auth)!: the set-user-manager depth cap counts the whole chain a link creates, including the reports already below the edited user, so a chain linked bottom-up cannot pass `MAX_MANAGER_CHAIN_DEPTH` (#26775)
6+
7+
Clause-②: no (narrowing) — `POST /api/v1/auth/admin/set-user-manager`, the bulk user import's manager pass and the development boot's demo staff manager links refuse a link that would make a reporting chain longer than 20 links counted through the reports below the edited user, which they admitted before. Nothing is widened.
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type changes: the endpoint's documented depth cap is now enforced on the whole chain it creates. The refusal is the existing VALUE_OUT_OF_RANGE / 400 with details.reason max_depth_exceeded, so there is nothing for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing at the manager-link write (`@objectstack/plugin-auth`), served by every caller of `applyUserManagerLink`.
13+
14+
- **Before.** The cap counted the link being written plus the managers above the proposed manager. The reports already hanging below the edited user were not counted, so a chain built from the bottom up was admitted one link at a time past 20 links.
15+
- **Now.** The cap counts the managers above the proposed manager, plus the link, plus the longest chain of existing reports below the edited user. A link that would make the total pass 20 is refused with the same `VALUE_OUT_OF_RANGE` / 400 and `details.reason: 'max_depth_exceeded'` as before; the message now says the count runs through the reports below the user.
16+
- **Unchanged.** A chain built from the top down is refused at the same link as before. A chain of exactly 20 links is admitted in any building order. The other refusals and their order are untouched.
17+
- **Who sees it.** An admin, an import file or a `demoStaff` declaration that would build a reporting chain deeper than 20 links. Such a chain was resolved only in part by the hierarchy scopes that walk it, so a manager silently lost sight of records owned deep in it.
18+
19+
**The remedy.** Shorten the chain: attach the subtree under a manager closer to the top, or flatten the levels below the edited user.

‎docs/qa/platform-checklist/areas/attachments-storage.json‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"qa-scratch-authz": {
99
"title": "Scratch authz parents (qa_vault / qa_shared / qa_nofiles) + two real member personas",
1010
"why": "Stock showcase has NO private files-enabled parent: its only `enable.files` object is showcase_project, which is public_read_write AND readable through the showcase `everyone` baseline. Without this recipe the entire authz core of this area — the 403 deny side of download-authz-both-sides, the invisible-parent case of read-inherits-parent-rls, and the read-but-not-edit split of attach-requires-parent-edit — is blocked(fixture) on every run. Run #7635 closed the gap at runtime and scored those items instead of blocking them; this block is that provisioning made permanent and replayable.",
11-
"provenance": "run #7635 (framework 92f26f75) → #7670 Step 6 gained persona B's sys_attachment read after QA run #23166 met the panel's up-front refusal (#23169). Step 6 pinned the three persona set payloads (#24036, from the intent QA run #23929 recorded; replayed on main c91af8ed6: 200 saves, 201 bindings, the split proof as stated), and step 5 mints three members, invited first.",
11+
"provenance": "run #7635 (framework 92f26f75) → #7670 Step 6 gained persona B's sys_attachment read after QA run #23166 met the panel's up-front refusal (#23169). Step 6 pinned the three persona set payloads (#24036, from the intent QA run #23929 recorded; replayed on main c91af8ed6: 200 saves, 201 bindings, the split proof as stated), and step 5 mints three members, invited first. #26791: replayed steps 1-4 on a fresh stock showcase boot at main 8eef9a8d (REST only, 2026-10-27). As written (no namespace) step 1 answered 201 with the derived namespace 'attachments' and steps 2-4 each answered 422 NAMESPACE_PREFIX; with step 1 declaring namespace 'qa', step 1 answered 201 and steps 2-4 each answered 200; on that boot step 6's three set saves answered 200 and the teardown DELETE answered 200, removing the three objects and the three sets.",
1212
"app": "showcase",
1313
"requires": [
1414
"an admin session that holds the `manage_metadata` capability — PUT /api/v1/meta/:type/:name is capability-gated per ADR-0066 D1 (#6603), so a bare authenticated session is NOT enough (packages/rest/src/rest-route-ledger.ts, the `PUT /api/v1/meta/:type/:name` row)",
@@ -19,9 +19,9 @@
1919
{
2020
"step": 1,
2121
"call": "POST /api/v1/packages",
22-
"body": { "manifest": { "id": "com.objectstack.qa.attachments", "name": "QA attachments fixture", "version": "1.0.0", "type": "app" }, "enableOnInstall": true },
23-
"expect": "2xx with the installed package echoed back. A re-run against a live DB 409s on the duplicate id — send `overwrite: true` deliberately rather than adding it by reflex (the guard exists so a second install never silently clobbers the first).",
24-
"source": "POST /api/v1/packages is the dispatcher install route (packages/rest/src/rest-route-ledger.ts#packages note; client `packages.install`); the body shape { manifest, enableOnInstall?, overwrite? } is URL- and body-pinned in packages/client/src/client.test.ts"
22+
"body": { "manifest": { "id": "com.objectstack.qa.attachments", "name": "QA attachments fixture", "version": "1.0.0", "type": "app", "namespace": "qa" }, "enableOnInstall": true },
23+
"expect": "2xx with the installed package echoed back, manifest.namespace 'qa'. The namespace is DECLARED on purpose: every object a package defines must be named NS_SHORT, and a manifest that declares none gets one derived from the id's last segment ('attachments'), under which steps 2-4 answer 422 NAMESPACE_PREFIX ('Rename it to attachments_qa_vault'). Declaring 'qa' keeps the qa_ names every later step and item uses. The namespace must be unique on the boot: a sibling recipe installed beside this one must not declare 'qa' too. A re-run against a live DB 409s on the duplicate id — send `overwrite: true` deliberately rather than adding it by reflex (the guard exists so a second install never silently clobbers the first).",
24+
"source": "POST /api/v1/packages is the dispatcher install route (packages/rest/src/rest-route-ledger.ts#packages note; client `packages.install`); the body shape { manifest, enableOnInstall?, overwrite? } is URL- and body-pinned in packages/client/src/client.test.ts. The install derives a missing namespace in packages/metadata-protocol/src/protocol-package-install.ts with packages/spec/src/kernel/namespace-prefix.ts#deriveNamespaceFromPackageId (an explicitly declared one wins); the prefix judge every object save asks is packages/spec/src/kernel/namespace-prefix.ts#validateObjectNamespacePrefix (ADR-0129 D2)"
2525
},
2626
{
2727
"step": 2,
@@ -248,7 +248,7 @@
248248
"title": "Gated downloads: 401 anonymous, 403 parent-invisible, signed URL for the entitled — per gating class (attachments-scope / field-owned / public_read opt-out / ungated)",
249249
"since": "v15.1",
250250
"status": "active",
251-
"revision": 6,
251+
"revision": 7,
252252
"priority": "P1",
253253
"surface": "mixed",
254254
"personas": ["admin (uploader/owner)", "signed-up member who cannot read the parent record (qa-scratch-authz persona B: the API deny clauses)",
@@ -347,6 +347,12 @@
347347
"date": "2026-10-26",
348348
"change": "One checklist-accuracy finding of Tier 1 record #25815 (release-18.2.0-tier1, at 6679644a) (acceptance[2]), re-read on main f6c61932, plus the retired console-pin wording this card carries for #26035. acceptance[2]: automated.ref said the permission-matrix file's 'uploader/entitled 200' covers clause 2, but its owner case is the admin, who also owns and reads the parent, and it never follows the url — so the uploader-bypass-against-an-unreadable-parent leg and the 'url serves the bytes' leg are unasserted. The ref now scopes the file to the envelope and names those two legs as hand-driven. The ref's 'a console bundle this repo does not build' and source[3]'s objectui path predate the objectui import: the panel is read at ui/packages/app-shell/src/views/RecordAttachmentsPanel.tsx, and the console is built from ui/ by `pnpm console:build`. No clause, step or oracle changed.",
349349
"ref": "#25839"
350+
},
351+
{
352+
"revision": 7,
353+
"date": "2026-10-27",
354+
"change": "qa-scratch-authz step 1 now declares manifest.namespace 'qa' (#26791). Replayed on a fresh stock showcase boot at main 8eef9a8d: as written, the install derived namespace 'attachments' from the package id and steps 2-4 each answered 422 NAMESPACE_PREFIX, so a literal replay provisioned no scratch parent and this item's recipe-backed clauses would have scored blocked(fixture). With the declared namespace, steps 1-4 answered 201/200/200/200 and step 6's three set saves 200. The object names (qa_vault / qa_shared / qa_nofiles) are unchanged, so no step or clause of this item changed",
355+
"ref": "#26791"
350356
}
351357
]
352358
},
@@ -355,7 +361,7 @@
355361
"title": "sys_attachment reads inherit parent visibility: a restricted member sees neither rows nor counts for invisible parents",
356362
"since": "v15.1",
357363
"status": "active",
358-
"revision": 8,
364+
"revision": 9,
359365
"priority": "P1",
360366
"surface": "api",
361367
"personas": ["member who can read the parent record", "member who cannot read the parent record"],
@@ -458,6 +464,12 @@
458464
"date": "2026-10-26",
459465
"change": "One checklist-accuracy finding of Tier 1 record #25815 (release-18.2.0-tier1, at 6679644a) (acceptance[3]), re-read on main f6c61932: the probe half (ADR-0139 D5's classes, the warning, the probe-failure deny) is pinned in-process and at unit only, which the item already says, but the verify did not name either pin's layer, and the warning's text is asserted only at unit. acceptance[3].verify now names both layers (attachment-read-leaf.integration.test.ts: real engine and SQL driver, silent logger; attachment-read-leaf.test.ts: stand-in engine, recording logger) and says the unit file asserts the warning by fragment, never the '[storage] attachment read visibility' prefix and never a server's log. No clause or oracle changed.",
460466
"ref": "#25839"
467+
},
468+
{
469+
"revision": 9,
470+
"date": "2026-10-27",
471+
"change": "qa-scratch-authz step 1 now declares manifest.namespace 'qa' (#26791). Replayed on a fresh stock showcase boot at main 8eef9a8d: as written, the install derived namespace 'attachments' from the package id and steps 2-4 each answered 422 NAMESPACE_PREFIX, so a literal replay provisioned no scratch parent and this item's recipe-backed clauses would have scored blocked(fixture). With the declared namespace, steps 1-4 answered 201/200/200/200 and step 6's three set saves 200. The object names (qa_vault / qa_shared / qa_nofiles) are unchanged, so no step or clause of this item changed",
472+
"ref": "#26791"
461473
}
462474
]
463475
},
@@ -466,7 +478,7 @@
466478
"title": "Attaching requires EDIT on the parent record; deleting requires uploader-or-parent-editor; unscoped multi-delete is refused outright",
467479
"since": "v15.1",
468480
"status": "active",
469-
"revision": 8,
481+
"revision": 9,
470482
"priority": "P1",
471483
"surface": "api",
472484
"personas": ["member with EDIT on the parent", "member with READ but not EDIT on the parent", "member who uploaded the attachment", "member who is neither uploader nor parent editor", "admin (isolates clause 5's FILES_DISABLED refusal from ATTACHMENT_PARENT_ACCESS by holding EDIT on both the original and target parent)"],
@@ -586,6 +598,12 @@
586598
"date": "2026-10-26",
587599
"change": "Two checklist-accuracy findings of Tier 1 record #25815 (release-18.2.0-tier1, at 6679644a), re-read on main f6c61932. (1) acceptance[2]: automated.ref said the permission-matrix file pins the uploader-vs-outsider delete split, but its uploader legs are the admin (who edits everything) and memberA on att_case (public_read_write, so memberA edits it), so 'the uploader succeeds even without parent edit' is not driven. (2) negative[1]: none of the refused-attach cases ((b), (item 3), (#2727)) re-reads sys_attachment for a row the refusal left. The ref now scopes clause 2 to the three delete answers the file asserts and names both gaps as hand-driven. No clause, step or oracle changed.",
588600
"ref": "#25839"
601+
},
602+
{
603+
"revision": 9,
604+
"date": "2026-10-27",
605+
"change": "qa-scratch-authz step 1 now declares manifest.namespace 'qa' (#26791). Replayed on a fresh stock showcase boot at main 8eef9a8d: as written, the install derived namespace 'attachments' from the package id and steps 2-4 each answered 422 NAMESPACE_PREFIX, so a literal replay provisioned no scratch parent and this item's recipe-backed clauses would have scored blocked(fixture). With the declared namespace, steps 1-4 answered 201/200/200/200 and step 6's three set saves 200. The object names (qa_vault / qa_shared / qa_nofiles) are unchanged, so no step or clause of this item changed",
606+
"ref": "#26791"
589607
}
590608
]
591609
},

0 commit comments

Comments
 (0)