Skip to content

data: a multi-user field keeps a deleted user's id, so a later update carrying the field unchanged is refused reference_not_found #29062

Closed

Description

@objectstack-fleet

domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC

Filing gate: ① out-of-scope finding carried from #28881 (order ruling 4; the dev's os-dev-report 6570817610 on #28881, PR #29060). Filed by domain:server seat 1. ⛔ Not a claim. Graded by this seat p3 under 「定级你可以自己决定」 (Leehom, 2026-11-01): a stored dangling id the write path already refuses at write time; nothing is granted or disclosed.

Reach: a record with a multi_user (or multi-lookup) field naming a user who is later deleted.

What happens (named by the #28881 dev, not measured on this card)

The write-time reference check (assertReferencesResolve, packages/objectql/src/engine-referential-integrity.ts) refuses a write that names a missing user. But when a user referenced from a multi-user field is deleted afterwards, the delete walk (cascade / set-null) does not reach the array member, so the stored array keeps the deleted user's id. The next update of that record that carries the field unchanged is then refused reference_not_found for a value the editor never touched.

Wanted

Measure first: say what the delete walk does today for a single user / lookup field and for its multi-valued sibling, on main. Then the multi-valued field follows its single sibling's delete rule (drop the member on set-null, or refuse the delete on restrict), or the write check judges only members the write changes. The route is the dispatching seat's call.

Dedupe: searched multi_user deleted user id, set null multi lookup array member. Related: #28881 / PR #29060. New.


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

    Claim: PM loop round 1 · card thread · 2026-11-02T06:56Z
    Session: session_01FEkQ5kE5BrPUPNMMyv6oCH (the seat); the card thread is session_01FEkQ5nPQ5s2XpToMJtoBDm
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-cljmpx (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#3
    Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」, and 2026-11-01T06:39Z 「server 队列还是很多任务,再开一个席位吧」. Seat 1 handed this card to seat 3 at 2026-11-02T06:48Z under the maintainer's 2026-11-02T05:15Z 「队列进了很多任务,抓紧派发」.
    File surface: packages/objectql/src/engine-referential-integrity.ts and/or packages/objectql/src/engine-delete-cascade.ts (route by measurement), objectql tests, a changeset
    Container & model: card thread cmsg_01TenzsRopZj4jMbvjX8uz1XFBxyXPUfh7hY87x8YuedEk (same thread as #28946), mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: high
    Clause-②: no (expected for route A) — route B widens a write door's accept set and the dev then declares yes (widening) with a TIER_DEFAULT review
    Metadata-protocol: none (expected; the dev measures and declares)
    Responsibility: domain:server seat 3 owns the PR to merge
    Thread-read: none
    Premise: named by the #28881 dev (os-dev-report 6570817610), not measured on the card; measure-first in the order
    Serial constraints cleared: PR #29060 (#28881, seat 1, same check) in review: the dev merges origin/main once if it lands first
    Resume-from: none — the branch restarts from origin/main at 4df62925 or later (its PR #28999 is merged)

    State: → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-29062.md (project folder).

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5nPQ5s2XpToMJtoBDm
    os-dev-report
    {
    "issue": 29062,
    "status": "blocked",
    "branch": "claude/project-thread-cljmpx",
    "pr": null,
    "session": "session_01FEkQ5nPQ5s2XpToMJtoBDm",
    "premise_still_valid": true,
    "summary": "Measurement only; stopped before any fix on the seat instruction that new work is paused (maintainer short on tokens). Nothing pushed, no PR, no build of a fix. The premise holds with one correction: the delete walk excludes the user TYPE altogether, single and multi alike (engine.ts DELETE_SEMANTICS_REFERENCE_TYPES = master_detail, lookup, tree; its docblock says a user field never joined the walk and moving it is a spec change). Lookups follow their rule for single and multi alike (set_null drops only the deleted member, restrict refuses 409, cascade deletes). Every user-delete door that deletes reaches engine.delete (admin remove-user via the better-auth ObjectQL adapter), and the walk leaves the user id in place. A later update that carries the field unchanged is refused 400 VALIDATION_FAILED reference_not_found, on a single user field as well as a multi-user one; an update that omits the field passes. So route A does not apply (no multi/single divergence) and route B does: the write check judges every value the payload carries, including members already stored. Route B would also fix the single user field, which the card did not name.",
    "measurement": {
    "base": "6b3ba82c (origin/main at restart)",
    "real_sqlite_engine": "ObjectQL + SqlDriver(better-sqlite3), non-system caller, holder row naming A (single) or [A,B] (multi), then engine.delete(A). lookup single default/set_null: deleted, stored null. lookup single restrict: 409 DELETE_RESTRICTED, stored A. lookup single cascade: holder row deleted. lookup multi default/set_null: deleted, stored [B]. lookup multi restrict: 409 DELETE_RESTRICTED, stored [A,B]. lookup multi cascade: holder row deleted. Every surviving lookup row: update carrying the field unchanged 200, omitting it 200, adding a ghost id 400 VALIDATION_FAILED reference_not_found. user rows on a minimal sys_user rig: engine.delete(sys_user) 400 INVALID_FILTER (the minimal rig lacks columns the user delete reads), so the user rows were measured on the showcase instead.",
    "showcase_doors": "objectstack dev --seed-admin --fresh, records: showcase_field_zoo f_user=X and f_users=[ADMIN,X]; showcase_project team_members=[ADMIN,X]. (1) POST /api/v1/auth/admin/remove-user: 403 YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS for the stock dev admin (its legacy role scalar is not admin; the data door refuses editing role); after setting role=admin in the dev database: 200, sys_user row 404 afterwards, f_user still X, f_users still [ADMIN,X], team_members still [ADMIN,X]. PATCH carrying both fields unchanged: 400 VALIDATION_FAILED [f_user reference_not_found, f_users reference_not_found]; carrying f_users only: 400 [f_users reference_not_found]; carrying f_user only: 400 [f_user reference_not_found]; project carrying team_members: 400 [team_members reference_not_found]; PATCH omitting the fields: 200. (2) DELETE /api/v1/data/sys_user/ID: 405 OBJECT_API_METHOD_NOT_ALLOWED, user kept. (3) POST /api/v1/auth/delete-user: 404 (deliberately unconfigured, ledger disposition disabled), user kept.",
    "fix_shape_if_resumed": "Route B: pass the stored row to assertReferencesResolve at the by-id update site (engine.ts, priorRecord is always read there) and skip a value, or a multi member, the stored row already holds; added dangling members keep being refused as today. The bulk update site has per-row priors only on its rows-read branch; without them it keeps today behaviour. Clause-② yes (widening). Red first is the showcase PATCH above."
    },
    "tests": "Probe only, deleted after the run (no file left in the tree): a scratch real-SQLite vitest under packages/runtime/src, 16 configs (user/lookup x single/multi x default/set_null/restrict/cascade), exit 0; showcase HTTP script against a fresh dev boot. No fix, so no ablation.",
    "gates": "none run: no diff (git status empty at 6b3ba82).",
    "dogfood_battery": "n/a: no PR",
    "mcp_calls": "0",
    "api_writes": "1: this os-dev-report comment via post-stamped. No push, no pr_create. Write budget spent: 1.",
    "deviations": "Stopped at measurement on the seat instruction (work paused). To measure remove-user the dev admin role scalar was set to admin directly in the fresh dev database, because the stock dev admin is refused by better-auth and the data door refuses role edits.",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: public door, POST /api/v1/auth/admin/remove-user answers 403 YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS for the showcase dev platform admin (role scalar User), so a stock dev boot cannot remove a user through the admin surface · evidence: showcase fresh boot at 6b3ba82, platform admin token · dedupe words: remove-user dev admin role 403, platform admin better-auth role scalar",
    "class: c · reach: named producer needed · a user field may author deleteBehavior (any type parses it verbatim) but the walk never reads it on a user field, so restrict or cascade on a user field is declared and not enforced · evidence: probe rows user single/multi restrict and cascade keep the id unchanged · dedupe words: user field deleteBehavior ignored, deleteBehavior on user type"
    ]
    }

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

    Release: claim 6571089500 (session_01FEkQ5kE5BrPUPNMMyv6oCH, card thread session_01FEkQ5nPQ5s2XpToMJtoBDm, branch claude/project-thread-cljmpx) · 2026-11-02T07:37Z · seat 3 shift close (maintainer 2026-11-02T07:10Z 「尽快提交代码,然后收工」, 2026-11-02T07:33Z 「pm-dispatch 中车道下班的流程,都要走完」); handed back, no stay-behind
    Produce: none (measurement only; os-dev-report 6571331581, status blocked)
    Resume-from: none — fresh from origin/main

    • Next: build route B of pm-orders/server/order-29062.md (the write-time reference check judges only reference values the write adds). Measured: the delete walk leaves out the user type by declaration (DELETE_SEMANTICS_REFERENCE_TYPES), single and multi alike; admin remove-user leaves the id; a later PATCH carrying the field unchanged is refused reference_not_found. Out-of-scope findings in the report (dev admin 403 on remove-user; deleteBehavior on a user field declared but not enforced).
    • State: pm:dispatched → pm:queue, assignee cleared.
  6. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 2, 2026
  7. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  8. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

    Claim: PM loop round 1 · card thread · 2026-11-02T08:20Z
    Session: session_01ADHkmQJdgarUjPLNgFBY67 (the seat); the card thread is session_01ADHkmSDzgkXapyAsvrXkrV
    Account: os-steve (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-8t26yj
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#3
    Ruling-ref: the maintainer, verbatim, in the project thread: 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」
    File surface: packages/objectql/src/engine.ts (by-id update reference check), packages/objectql/src/engine-referential-integrity.ts, objectql tests, a changeset
    Container & model: card thread cmsg_01FdL1XeUyvVNxLreh3F4Qi93jDJpyZ4T5x8DmbvSnfEjZ, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: high
    Clause-②: yes (widening)
    Metadata-protocol: none
    Responsibility: packages/objectql write path (engine reference check); no platform path covers it; any editor of a record holding a deleted user id hits it today
    Thread-read: 6571710050
    Premise: holds (measured by the previous card thread, re-measured red first per the delta)
    Serial constraints cleared: PR #29060 (#28881) edits the same two files: the dev merges origin/main before the PR and again if it lands first, keeping both intents
    Resume-from: none — fresh from origin/main

    State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim. Order: project file server-seat3/standard-order.md + server-seat3/29062-delta.md.

  9. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-steve · session_01ADHkmSDzgkXapyAsvrXkrV
    os-dev-report

    {
     "issue": 29062,
     "status": "done",
     "branch": "claude/project-thread-8t26yj",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/29245",
     "head": "49f236e7",
     "session": "session_01ADHkmSDzgkXapyAsvrXkrV",
     "premise_still_valid": true,
     "summary": "Route B. assertReferencesResolve takes an optional storedRows reader; after the probes, a refused value or multi-valued member that every stored row already holds is dropped, so only references a write adds are judged. By-id update passes its priorRecord (always read there); predicate update passes the readPriorRows memo, read only after a refusal, and forgives only when every matched row holds the value (zero matched rows forgive nothing). Insert and the insert-mode dry run pass nothing and are unchanged; the engine has no upsert door. Covers single user, multi user, lookup and master_detail alike. Delete walk untouched. origin/main merged twice; the second brings #29060 (flow-write reference check), auto-merged clean, both intents kept and its suites green.",
     "tests": "Red first on base 2af0ae2a: engine-lookup-referential-integrity.test.ts Tests 4 failed | 17 passed (21), cause ValidationError 'no sys_user record has id u_gone'. Ablation 1 (stored-row read replaced by empty, via ablation-replace, blob dbd8047a -> 7daaf379 -> restored == HEAD): 4 failed | 17 passed. Ablation 2 (every -> some): 1 failed (bulk) | 20 passed; restored == HEAD. Final head 49f236e7: objectql test Test Files 527/527, Tests 10145/10145; objectql typecheck exit 0 (lock VERDICT command-exit 0); service-automation runas-system-reference-check.integration + builtin/crud-runas: 2/2 files, 44/44 tests.",
     "gates": "dispatch-gates --commands (no paths) at 49f236e7: 63 families; all run, exit codes recorded; --ran: 62 run, 1 NOT MEASURED = node scripts/check-plugin-teardown-shape.mjs --self-test exit 3 PREREQUISITE NOT MET (positive control pinned to a commit outside this shallow clone), declared to CI Lint & Repo Gates. Every other family exit 0. pnpm lint exit 0 (also at 0299a25b). CI check-runs: in_progress at report time (not waited on).",
     "line_budget": "5 files, +201/-16 (217 changed lines vs 3000 threshold); no governed surface",
     "files_changed": [
      ".changeset/29062-stored-reference-not-rejudged.md",
      "packages/objectql/src/engine-referential-integrity.ts",
      "packages/objectql/src/engine.ts",
      "packages/objectql/src/engine-lookup-referential-integrity.test.ts",
      "packages/objectql/src/engine-unreadable-master-detail-header.test.ts"
     ],
     "dogfood_battery": "n/a — no ! in the PR title and Clause-② is yes (widening), no (narrowing) arm",
     "deviations": "(1) One forced test edit, named in the PR body: engine-unreadable-master-detail-header.test.ts repoint-by-id / bulk-repoint door wrote the header line l1 already stores, now names h_mine_locked. (2) The first merged-head rebuild accidentally also built objectql's dependents (filter spelled objectql...); output discarded, no effect on the tree. (3) check-plugin-teardown-shape --self-test NOT MEASURED locally (needs unshallow clone).",
     "mcp_calls": "0",
     "api_writes": "2 — relay pr_create (#29245, run 41427163731) and this os-dev-report comment via post-stamped. git push is not counted.",
     "open_questions": [
      {
       "question": "Route B is applied by type-agnostic rule, so a master_detail header the caller cannot read, carried back unchanged, now passes (as omitting it already did); a repoint to it is still refused. Keep master_detail in the rule?",
       "options": [
        "A keep it: the stored value is not the write's reference; the #22519 existence oracle is not reopened because the forgiveness does not depend on whether the header exists",
        "B exclude master_detail: keep judging a carried master-detail header every time"
       ],
       "recommendation": "A, because forgiving depends only on the stored value, never on the header, so it reveals nothing about the header, and it fixes the same edit-form refusal for a header whose sharing changed"
      }
     ],
     "out_of_scope_findings": [
      "class: a · reach: public door, POST /api/v1/auth/admin/remove-user answers 403 YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS for the showcase dev platform admin (role scalar is not admin) · evidence: previous card thread measurement 6571331581 on showcase fresh boot at 6b3ba82c; not re-measured here · dedupe words: remove-user dev admin role 403, platform admin better-auth role scalar",
      "class: c · reach: named producer needed · deleteBehavior on a user field parses and is never read by the delete walk (DELETE_SEMANTICS_REFERENCE_TYPES excludes user), so restrict/cascade on a user field is declared and not enforced · evidence: 6571331581 probe rows user single/multi restrict and cascade keep the id · dedupe words: user field deleteBehavior ignored, deleteBehavior on user type"
     ]
    }

    Stamped 2026-11-02T10:15Z.


    Generated by Claude Code

  10. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

    Pointer · 2026-11-02T10:22Z: PR #29245 seat ACCEPT is on the PR. The dev's two out-of-scope findings are filed: #29253 (deleteBehavior on a user field not enforced) and #29254 (dev admin 403 on admin remove-user).

  11. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

    Landing record · #29062 · PR #29245

    domain:server seat 3 · session_01ADHkmQJdgarUjPLNgFBY67 · 2026-11-02T11:42Z

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-29245-*' 0 refs (released)
    Ancestry git merge-base --is-ancestor 344a16d7ab origin/main yes
    Content git grep -n "\[#29062\] a reference value already stored" origin/main -- packages/objectql/src/engine-lookup-referential-integrity.test.ts line 418
    • Landed: merged as 344a16d7. A write now judges only the reference values it adds. A stored reference that is carried back unchanged is not refused because it stopped resolving, and that includes a single member of a multi-user field. A new id that does not exist is still refused, and inserts still check each value.
    • Seat ACCEPT 6574694888 (with the new-head addendum 6575062643) is on the PR (Clause-②: yes (widening), TIER_DEFAULT declaration review inside the ACCEPT). Fixes #29062 closed this card on merge.
    • State: pm:dispatched and the assignee removed in the same round.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:databugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions