Dedicated follow-up run owed by #9702. The item became executable for the first time when PR #9697 (#9237) retargeted its evaluate clause; per #9308's sequencing ruling this is a run at a NEW sha on main, deliberately not attached to wave #9296's pinned e4e5c6e3 baseline.
Item verdict: pass — 7 of 7 acceptance clauses passed, reproduced on two independent fresh boots.
Environment fingerprint
| |
|---|
| framework sha | 4a7b3604c319d28e3ae7fbc98a4bc1486602044c (4a7b3604c) |
| ancestry check | 4012a70cd (#9697/#9237) and 06f9848f9 (#9789/#9308) are both ancestors of the run sha — verified with git merge-base --is-ancestor |
| objectui pin | 82a94170c4058d451ce3ac179d99296d90554479 — console never built and never exercised; the item's surface is api |
| app | showcase, 132 seeded rows, 47 plugins |
| boot A | port 3702, file:/tmp/run9702/data2.db, --seed-admin, fresh DB |
| boot B | port 3703, file:/tmp/run9702/data3.db, --seed-admin, fresh DB, independent process |
| runner | os-warren, session session_01PnJHU45vPJj5UQrxe946Bx, model claude-opus-5 |
| date | 2026-08-20 |
| concurrency | This round was executed SEQUENTIALLY by a single reader — no subagent fan-out. The findings below stand on their own; the claim "nothing else was missed" does not carry the independence a parallel round would give it. |
Scope
Selector access-security.record-share-grant-revoke — resolved by node scripts/checklist-select.mjs to exactly 1 runnable item, P1, surface api, revision 3.
⛔ Deliberately not widened to #9308's newly unblocked items (approvals.per-group-signoff, approvals.viewer-gating-submitter-side, approvals.ooo-delegation-reroute, access-security.share-link-capability-tokens, access-security.fls-mask-and-strip). #9702 reserves the combine-or-not decision for the wave PM in its own words; taking it here would be a decision this seat does not hold. See "Open for the wave PM" below.
Per-clause verdicts (revision 3)
Oracle for every clause is api — live server traces against a real boot. No screenshot or DOM was consulted; none was needed.
| # | clause | verdict | oracle evidence (text) |
|---|
| 1 | PRE-grant isolation holds | pass | Before any share exists, B's GET /api/v1/data/showcase_private_note/<A id> answers 404RECORD_NOT_FOUND ("Record Nlji2uPrSLttIcwJ not found in showcase_private_note") — a not-found-shaped denial, which the clause admits. B's list answers 200 with total 0. |
| 2 | the manual grant lands as a sys_record_share row | pass | POST .../shares → 201, body {id: shr_2bdd3fd4-…, object_name: showcase_private_note, record_id: Nlji2uPrSLttIcwJ, recipient_type: "user", recipient_id: <B>, access_level: "read", source: "manual", source_id: null, granted_by: <A>}. A system-context read of sys_record_share filtered on record_id returns exactly that one row with the same fields. |
| 3 | the grant widens B both ways and stays scoped | pass | B by-id → 200 (title: "A share probe pass2"); B list → 200, total 1, contains the id. C by-id → 404RECORD_NOT_FOUND; C list → 200, total 0, does not contain it. |
| 4 | the shares list is management-gated | pass | A GET .../shares → 200 with the one grant present. B (visible, not manager) POST .../shares → 403PERMISSION_DENIED: "managing shares on showcase_private_note/… requires record ownership or Modify All Data (ADR-0111 D1)". Probed twice — B granting C, and B granting themselvesedit — both 403. |
| 5 | revoke retracts access | pass | DELETE .../shares/<shareId> → 204 (empty body). B's next by-id → 404RECORD_NOT_FOUND. System-context sys_record_share filtered on record_id → total 0, row gone. |
| 6 | revoke is record-scoped | pass | DELETE /data/showcase_private_note/<A SECOND note id>/shares/<shareId> → 404NOT_FOUND: "share shr_2bdd3fd4-… not found on showcase_private_note/gDsNat3RCg1Ilp1T". Survival re-read of sys_record_share/<shareId> → 200, row intact; B's by-id read still 200. ⚠️ Ordering note below — this probe was run before clause 5's revoke, deliberately. |
| 7 | rule evaluate reconciles the audience | pass | Admin assigns manager to persona B (POST /api/v1/data/sys_user_position {user_id: <B>, position: "manager"} → 201). POST /api/v1/sharing/rules/share_key_account_qualified_contacts_with_managers/evaluate → 200{ruleId: "srule_bae1c31a-730e-4114-b945-e0747a7427c9", matchedRecords: 1, expandedUsers: 2, grantsCreated: 1, grantsUpdated: 0, grantsRevoked: 0}. Rule-sourced sys_record_share rows go 1 → 2, both on showcase_contact/WJ5Cot2OydOKqqYu (= Nora West, company Northwind, stage qualified), each source: "rule", source_id: "srule_bae1c31a-730e-4114-b945-e0747a7427c9" — which equals the sys_sharing_rule ROW id read back separately, and is not the rule name. This is the value revision 2 corrected the clause to demand, and the implementation writes it. |
Supplementary, beyond the clauses: persona B, once holding manager, reads the matched contact GET /api/v1/data/showcase_contact/WJ5Cot2OydOKqqYu → 200, where the same read answered 403 before the assignment+evaluate. The rule-materialised grant is one the read gate actually consults end to end — not merely a row in a table.
Reproduction
No fail, so no reproduction rule is owed. Both boots ran the identical sequence and agreed clause for clause:
- boot A (
3702): note Nlji2uPrSLttIcwJ, second note gDsNat3RCg1Ilp1T, share shr_2bdd3fd4-f00d-4cc2-8b45-ecde7fde39ff, rule row srule_bae1c31a-730e-4114-b945-e0747a7427c9 - boot B (
3703): note YU4FbwJcDP61GmvX, second note Wmy4HDQnMbivIls_, share shr_6115174c-faa0-4b22-bdd0-371b25953c6d, rule row srule_ee5195ce-7914-4693-8677-41a6f36c6cdf; evaluate → {matchedRecords: 1, expandedUsers: 2, grantsCreated: 1}, same shape.
Checklist-accuracy findings → item revised to 4 (PR #10083)
Neither is a product defect; both are the checklist being wrong about itself, found the only way they could be — by executing the item.
Clause 6's evidence requirement was unsatisfiable in the steps' own order. The steps put the revoke-scope probe after the real revoke, but the clause asks for the mis-scoped DELETE to be refused and the share to still exist (re-read). Once the share is revoked the shareId is gone, so the refusal a literal runner observes is the ordinary missing-share 404 — indistinguishable from a correct record-scope refusal, and proving nothing. This run reordered the probe ahead of the revoke, which is what made clause 6 measurable at all; revision 4 writes that ordering into the steps and adds the second note the probe needs.
The manager-position fixture note now mis-teaches. It says a grantsCreated 0 reconcile means an empty recipient expansion, i.e. a fixture gap. Measured on a stock boot at this sha, that reading is wrong: the seeded dev admin already holds manager (sys_user_position row usp_showcase_admin_manager) and the boot backfill already materialises the grant, so a bare evaluate answers grantsCreated 0 out of idempotence. Confirmed directly — an evaluate run before assigning any extra persona returned {matchedRecords: 1, expandedUsers: 1, grantsCreated: 0} with the rule-sourced row already present from boot. Revision 4 says so and points the runner at expandedUsers/matchedRecords instead.
Neighbouring ground — observed, not chased
#9702 is explicit that these are "nobody has looked since the fixtures moved", not assertions that anything is wrong. Recorded and left alone:
Environment notes (cost this run a cycle each; no card owed)
os dev has no --no-ui flag.objectstack dev --no-ui exits with Error: Nonexistent flag: --no-ui and prints usage — which, backgrounded, looks exactly like a server that booted and then died silently. RUNNER.md's absence-inference row documents --no-ui for serve, and the distinction is easy to carry across. Omit it on dev.- The CLI entry point is
packages/cli/bin/run.js, not packages/cli/dist/index.js. Invoking the dist entry directly exits 0 with zero output — a silent no-op that reads like a crashed boot. Use node_modules/.bin/objectstack (or pnpm exec). $select on the data API takes a comma-separated field list; a JSON array is rejected with INVALID_FIELD. $filter, by contrast, must be JSON.
Close-out
No product defect was observed, so nothing is extracted. The two checklist-accuracy findings are closed by PR #10083 (revision 4) rather than parked, since they are edits to this very item and §3 of the runner playbook puts that on the runner.
Parked-on: #9296 · the #9237/#9308 seed moves leave neighbouring items unlooked-at; the widen-or-combine call is the wave PM's
Open for the wave PM
Both #9237 and #9308 have landed, and #9308 revised five further items whose fixtures moved on the same tree. #9702 reserves the combine decision explicitly ("that is the wave PM's call, not this seat's"), so this run stayed at the stated minimum of one item. If a combined round is wanted, those five plus this one are one tree at one post-#9789 sha — this run's environment recipe above is reusable as-is.
Refs: #9702 (the card) · PR #10083 (revision 4) · #9697 / #9237 · #9308 · #9296
Generated by Claude Code
Dedicated follow-up run owed by #9702. The item became executable for the first time when PR #9697 (#9237) retargeted its evaluate clause; per #9308's sequencing ruling this is a run at a NEW sha on
main, deliberately not attached to wave #9296's pinnede4e5c6e3baseline.Item verdict:
pass— 7 of 7 acceptance clauses passed, reproduced on two independent fresh boots.Environment fingerprint
4a7b3604c319d28e3ae7fbc98a4bc1486602044c(4a7b3604c)4012a70cd(#9697/#9237) and06f9848f9(#9789/#9308) are both ancestors of the run sha — verified withgit merge-base --is-ancestor82a94170c4058d451ce3ac179d99296d90554479— console never built and never exercised; the item's surface isapi132seeded rows,47plugins3702,file:/tmp/run9702/data2.db,--seed-admin, fresh DB3703,file:/tmp/run9702/data3.db,--seed-admin, fresh DB, independent processos-warren, sessionsession_01PnJHU45vPJj5UQrxe946Bx, modelclaude-opus-5Scope
Selector
access-security.record-share-grant-revoke— resolved bynode scripts/checklist-select.mjsto exactly 1 runnable item,P1, surfaceapi, revision 3.⛔ Deliberately not widened to #9308's newly unblocked items (
approvals.per-group-signoff,approvals.viewer-gating-submitter-side,approvals.ooo-delegation-reroute,access-security.share-link-capability-tokens,access-security.fls-mask-and-strip). #9702 reserves the combine-or-not decision for the wave PM in its own words; taking it here would be a decision this seat does not hold. See "Open for the wave PM" below.Per-clause verdicts (revision 3)
Oracle for every clause is
api— live server traces against a real boot. No screenshot or DOM was consulted; none was needed.passGET /api/v1/data/showcase_private_note/<A id>answers 404RECORD_NOT_FOUND("Record Nlji2uPrSLttIcwJ not found in showcase_private_note") — a not-found-shaped denial, which the clause admits. B's list answers 200 withtotal 0.sys_record_sharerowpassPOST .../shares→ 201, body{id: shr_2bdd3fd4-…, object_name: showcase_private_note, record_id: Nlji2uPrSLttIcwJ, recipient_type: "user", recipient_id: <B>, access_level: "read", source: "manual", source_id: null, granted_by: <A>}. A system-context read ofsys_record_sharefiltered onrecord_idreturns exactly that one row with the same fields.passtitle: "A share probe pass2"); B list → 200,total 1, contains the id. C by-id → 404RECORD_NOT_FOUND; C list → 200,total 0, does not contain it.passGET .../shares→ 200 with the one grant present. B (visible, not manager)POST .../shares→ 403PERMISSION_DENIED: "managing shares on showcase_private_note/… requires record ownership or Modify All Data (ADR-0111 D1)". Probed twice — B granting C, and B granting themselvesedit— both 403.passDELETE .../shares/<shareId>→ 204 (empty body). B's next by-id → 404RECORD_NOT_FOUND. System-contextsys_record_sharefiltered onrecord_id→total 0, row gone.passDELETE /data/showcase_private_note/<A SECOND note id>/shares/<shareId>→ 404NOT_FOUND: "share shr_2bdd3fd4-… not found on showcase_private_note/gDsNat3RCg1Ilp1T". Survival re-read ofsys_record_share/<shareId>→ 200, row intact; B's by-id read still 200.passmanagerto persona B (POST /api/v1/data/sys_user_position {user_id: <B>, position: "manager"}→ 201).POST /api/v1/sharing/rules/share_key_account_qualified_contacts_with_managers/evaluate→ 200{ruleId: "srule_bae1c31a-730e-4114-b945-e0747a7427c9", matchedRecords: 1, expandedUsers: 2, grantsCreated: 1, grantsUpdated: 0, grantsRevoked: 0}. Rule-sourcedsys_record_sharerows go 1 → 2, both onshowcase_contact/WJ5Cot2OydOKqqYu(= Nora West,company Northwind,stage qualified), eachsource: "rule",source_id: "srule_bae1c31a-730e-4114-b945-e0747a7427c9"— which equals thesys_sharing_ruleROW id read back separately, and is not the rule name. This is the value revision 2 corrected the clause to demand, and the implementation writes it.Supplementary, beyond the clauses: persona B, once holding
manager, reads the matched contactGET /api/v1/data/showcase_contact/WJ5Cot2OydOKqqYu→ 200, where the same read answered 403 before the assignment+evaluate. The rule-materialised grant is one the read gate actually consults end to end — not merely a row in a table.Reproduction
No
fail, so no reproduction rule is owed. Both boots ran the identical sequence and agreed clause for clause:3702): noteNlji2uPrSLttIcwJ, second notegDsNat3RCg1Ilp1T, shareshr_2bdd3fd4-f00d-4cc2-8b45-ecde7fde39ff, rule rowsrule_bae1c31a-730e-4114-b945-e0747a7427c93703): noteYU4FbwJcDP61GmvX, second noteWmy4HDQnMbivIls_, shareshr_6115174c-faa0-4b22-bdd0-371b25953c6d, rule rowsrule_ee5195ce-7914-4693-8677-41a6f36c6cdf; evaluate →{matchedRecords: 1, expandedUsers: 2, grantsCreated: 1}, same shape.Checklist-accuracy findings → item revised to 4 (PR #10083)
Neither is a product defect; both are the checklist being wrong about itself, found the only way they could be — by executing the item.
Clause 6's evidence requirement was unsatisfiable in the steps' own order. The steps put the revoke-scope probe after the real revoke, but the clause asks for the mis-scoped
DELETEto be refused and the share to still exist (re-read). Once the share is revoked theshareIdis gone, so the refusal a literal runner observes is the ordinary missing-share 404 — indistinguishable from a correct record-scope refusal, and proving nothing. This run reordered the probe ahead of the revoke, which is what made clause 6 measurable at all; revision 4 writes that ordering into the steps and adds the second note the probe needs.The manager-position fixture note now mis-teaches. It says a
grantsCreated 0reconcile means an empty recipient expansion, i.e. a fixture gap. Measured on a stock boot at this sha, that reading is wrong: the seeded dev admin already holdsmanager(sys_user_positionrowusp_showcase_admin_manager) and the boot backfill already materialises the grant, so a bare evaluate answersgrantsCreated 0out of idempotence. Confirmed directly — an evaluate run before assigning any extra persona returned{matchedRecords: 1, expandedUsers: 1, grantsCreated: 0}with the rule-sourced row already present from boot. Revision 4 says so and points the runner atexpandedUsers/matchedRecordsinstead.Neighbouring ground — observed, not chased
#9702 is explicit that these are "nobody has looked since the fixtures moved", not assertions that anything is wrong. Recorded and left alone:
sys_sharing_ruleseeds 2 rows at this sha (share_new_inquiries_with_field_ops,share_key_account_qualified_contacts_with_managers), and a stock boot materialises 1 rule-sourcedsys_record_share— onshowcase_contact(Nora West) to the seeded admin. Matches what Follow-up run:access-security.record-share-grant-revokebecomes runnable for the first time when #9237 lands — it needs a dedicated run at a new sha #9702 and the PM's comment describe; no anomaly.SHARING_NOT_ENABLEDboot-backfill warning is gone. No boot in this run logged a "boot rule backfill failed" line for any rule — consistent with the two inert rules having been retired by fix(app-showcase): retire the sharing rules no gate can consult, keep the demo where it enforces #9697.showcase_field_zoo.f_autonumbertenancy-split diagnostic, which names its own tracking id (Seed loader writes untenanted rows while the REST path stamps an organization — one single-tenant install runs two autonumber scopes and mints duplicate business identifiers, silently (17.0.0 GA) #8686) in the message. Unrelated to sharing; not new; nothing extracted.Environment notes (cost this run a cycle each; no card owed)
os devhas no--no-uiflag.objectstack dev --no-uiexits withError: Nonexistent flag: --no-uiand prints usage — which, backgrounded, looks exactly like a server that booted and then died silently. RUNNER.md'sabsence-inferencerow documents--no-uiforserve, and the distinction is easy to carry across. Omit it ondev.packages/cli/bin/run.js, notpackages/cli/dist/index.js. Invoking the dist entry directly exits 0 with zero output — a silent no-op that reads like a crashed boot. Usenode_modules/.bin/objectstack(orpnpm exec).$selecton the data API takes a comma-separated field list; a JSON array is rejected withINVALID_FIELD.$filter, by contrast, must be JSON.Close-out
No product defect was observed, so nothing is extracted. The two checklist-accuracy findings are closed by PR #10083 (revision 4) rather than parked, since they are edits to this very item and §3 of the runner playbook puts that on the runner.
Open for the wave PM
Both #9237 and #9308 have landed, and #9308 revised five further items whose fixtures moved on the same tree. #9702 reserves the combine decision explicitly ("that is the wave PM's call, not this seat's"), so this run stayed at the stated minimum of one item. If a combined round is wanted, those five plus this one are one tree at one post-#9789 sha — this run's environment recipe above is reusable as-is.
Refs: #9702 (the card) · PR #10083 (revision 4) · #9697 / #9237 · #9308 · #9296
Generated by Claude Code