Skip to content

objectql: deleteBehavior on a user field parses but the delete walk never reads it, so restrict / cascade on a user field are not enforced #29253

Closed

Description

@objectstack-fleet

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

Filing gate: ① out-of-scope finding of the #29062 dev (os-dev-report 6574609406, out_of_scope_findings class c; measured by the earlier #29062 card thread, report 6571331581), confirmed out of PR #29245's scope by the seat ACCEPT on that PR (the seat ruling kept the delete walk untouched). Filed by domain:server seat 3. ⛔ Not a claim. Suggested-grade: p2 · a declared constraint that silently does nothing.

Reach: any object that declares a user or multi-valued user field with deleteBehavior: 'restrict' or 'cascade', when the named user is deleted.
Reader who acts: the domain:spec lane if the answer is to refuse the key on user fields; a domain:server seat (packages/objectql) if the answer is to enforce it.

What happens (measured by the #29062 card thread at 6b3ba82c, not re-measured on this card)

Wanted

Measure on main first. Then one of: (A) the spec refuses deleteBehavior on a user field (it cannot be honoured: user deletion goes through the auth layer, not the objectql delete walk), with an author-time message; or (B) the user-delete path runs the same walk for user fields. Report which the user-delete door can actually reach before choosing; #22957 (the tree twin, closed) is the precedent for B.

Dedupe: searched deleteBehavior user field, user field deleteBehavior ignored; nearest #22957 (tree, closed), #26718 (lookup restrict, closed). New.

Links back: #29062, PR #29245, #22957.

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 · domain:server · area:records · pm:queue · 2026-11-02T11:00Z

    ⛔ Not a claim.

    • Read fresh. deleteBehavior on a user field parses but is never enforced, so a declared constraint silently does nothing. p2. The server seat measures first and reports which user-delete door the objectql walk can reach. If the answer is (A), the spec refusing the key on user fields, that is a narrowing: it goes to domain:spec with the narrowing declaration and contract review.

    Generated by Claude Code

  3. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Nov 2, 2026
  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

    Claim: PM loop round 6 · card thread · 2026-11-02T11:15Z
    Session: session_01ADHkmQKwKFCp1m6Pb6Q8c1 (the seat); the card thread is named in a follow-up claim once it reports its branch
    Account: os-steve (the seat's linked user; the card's assignee)
    Branch: pending — the card thread's harness-pinned branch, written into a follow-up claim before the dev starts
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#5
    Ruling-ref: no maintainer ruling on this card; triage grade p2 (6575476068: the server seat measures which user-delete door the objectql walk reaches; refusing the key in spec (A) is a narrowing for domain:spec).
    File surface: packages/objectql/src/ (the delete walk's reference types in engine.ts and engine-referential-integrity.ts), the user-delete door in packages/plugins/plugin-auth/src/ only if the walk must be invoked from it, their tests, changeset. ⛔ No packages/spec/**: if (A) is the only sound answer, the dev stops with needs_decision and the seat routes it to domain:spec.
    Container & model: M, mode:thread, model: the thread's default judgement tier, effort: high (p2)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the code is objectql's delete walk · a deleteBehavior declared on a user field is accepted but never enforced when the user is deleted · reached by app authors who declare restrict or cascade on a user reference
    Thread-read: 6575476068
    Premise: holds (read at origin/main 39951d2a26: DELETE_SEMANTICS_REFERENCE_TYPES lists master_detail, lookup, tree only)
    Serial constraints cleared: open PR #29245 (#29062, seat 3) edits engine.ts and engine-referential-integrity.ts; the dev measures and writes now but opens its PR only after #29245 merges, merging origin/main then.
    Resume-from: none — fresh from origin/main

    State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim.

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

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

    Claim: PM loop round 3 · 2026-11-02T11:17Z
    Session: session_01ADHkmQjPiXgNoSaEpqeGvY (seat); dev = card thread (session named in its first report)
    Account: os-steve
    Branch: the card thread's harness-pinned branch, named in its first report (it names no issue, so this claim is the identity record)
    Worktree: the card thread's own
    Domain: domain:server
    Seat: domain:server#1
    File surface: the objectql delete walk in packages/objectql/src/engine.ts (user fields), tests, an @objectstack/objectql changeset; measure-first fork: if a user-delete door bypasses the walk the dev stops and reports, no code. ⛔ No packages/spec/**, no new error code, no change to lookup / master_detail / tree behaviour. (stop on breach; explain in the report)
    Container & model: M, mode:thread, model as the thread was opened, effort: medium
    Clause-②: no (narrowing) (expected for fork B: a restrict on a user field now refuses a delete that answered 200; the dev measures)
    Metadata-protocol: none (expected)
    Responsibility: the code is the objectql delete walk · the platform path is a declared deleteBehavior on a user field that silently does nothing · reached by every app that declares one, when a user is deleted
    Thread-read: 6575476068
    Premise: as filed; the dev re-measures on current main first
    Serial constraints cleared: the dev lists open PRs touching engine.ts before the first write and names them in the report

    Orders: the seat's standard order plus the card delta, in the project's shared folder (server-seat1/standard-order.md, server-seat1/29253-delta.md).

  7. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

    Claim withdrawn · 2026-11-02T11:19Z

    This seat's claim 6575797254 is withdrawn: domain:server seat 5's claim 6575764728 came first and its card thread is already running. Seat 5 holds this card. No thread was opened for seat 1, and nothing was written. Labels stay as seat 5 set them (pm:dispatched, assignee os-steve, the shared account).

  8. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

    Claim: PM loop round 6 · card thread · branch named · 2026-11-02T11:20Z (supersedes 6575764728, which had the branch pending; this seat's claim is the earlier stamp on this card)
    Session: session_01ADHkmQKwKFCp1m6Pb6Q8c1 (the seat); the card thread is session_01ADHkmPJSWAxwHWeAsqirug
    Account: os-steve (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-u66mdq (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#5
    Ruling-ref: no maintainer ruling on this card; triage grade p2 (6575476068: the server seat measures which user-delete door the objectql walk reaches; refusing the key in spec (A) is a narrowing for domain:spec).
    File surface: packages/objectql/src/ (the delete walk's reference types in engine.ts and engine-referential-integrity.ts), the user-delete door in packages/plugins/plugin-auth/src/ only if the walk must be invoked from it, their tests, changeset. ⛔ No packages/spec/**: if (A) is the only sound answer, the dev stops with needs_decision and the seat routes it to domain:spec.
    Container & model: M, mode:thread, model: the thread's default judgement tier, effort: high (p2)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the code is objectql's delete walk · a deleteBehavior declared on a user field is accepted but never enforced when the user is deleted · reached by app authors who declare restrict or cascade on a user reference
    Thread-read: 6575797254
    Premise: holds (read at origin/main 39951d2a26: DELETE_SEMANTICS_REFERENCE_TYPES lists master_detail, lookup, tree only)
    Serial constraints cleared: open PR #29245 (#29062, seat 3) edits engine.ts and engine-referential-integrity.ts; the dev measures and writes now but opens its PR only after #29245 merges, merging origin/main then.
    Resume-from: none — fresh from origin/main

    State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim.

  9. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-steve · session_01ADHkmPJSWAxwHWeAsqirug
    os-dev-report

    {
      "issue": 29253,
      "status": "done",
      "branch": "claude/project-thread-u66mdq",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29368",
      "session": "session_01ADHkmPJSWAxwHWeAsqirug",
      "premise_still_valid": true,
      "summary": "Measured first: every door that removes the sys_user row reaches ObjectQL.delete by id (better-auth admin remove-user and self delete-user through the plugin-auth adapter, as the system, inside the subject-erasure transaction); the generic data/MCP doors refuse a caller-context sys_user delete in identity-write-guard (ADR-0092 D2); ban is an update. So route B, no spec change. engine.ts only: statesDeleteSemantics admits a user field when, and only when, it declares one of the three deleteBehavior values (a bare user field stays out, unchanged), and a new deleteWalkTargetOf answers sys_user for a user field through referenceTargetOf while still reading the carrier through referenceCarrierOf. The admitted field then runs the lookup path unchanged: multi-value $contains probe and member removal, required set_null escalation, one-transaction plan.",
      "tests": "Head 7675ff75. New src/engine-cascade-delete-user-field.test.ts: Tests 16 passed (16). pnpm --filter @objectstack/objectql test: Test Files 531 passed (531), Tests 10292 passed (10292), then typecheck exit 0 (lock VERDICT command-exit 0). First full run on merge head 039625f2 was 1 failed (federated-injected-column-readers.test.ts reader ledger: two seam uses moved into deleteWalkTargetOf), fixed by swapping the two rows; that file + engine-cascade-federated-tenant-anchor: Tests 17 passed (17). Ablations via scripts/ablation-replace.mjs on committed HEAD (objectql tests run from src, no dist leg), each restored with git checkout HEAD, git diff HEAD 0 bytes, disk blob == HEAD blob 5defa623: (1) user arm answers false, the walk as on main: Tests 10 failed | 6 passed (16), every restrict/cascade/set_null pin red (5x 'expected the delete to be refused'); (2) deleteWalkTargetOf answers the carrier only: Tests 1 failed | 15 passed, the reference-less user field pin; (3) user arm answers true, bare user fields default to set_null: Tests 4 failed | 12 passed, the four no-deleteBehavior controls ('expected { person: null } to match { person: r_1 }').",
      "gates": "Head 7675ff75: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 93; all 93 run, each exit 0 (captured per command, stdin from /dev/null); --ran: 93 run, 0 NOT-MEASURED, 0 UNRUN (DERIVED zero), reconcile exit 0. Lint: root eslint packages/objectql --no-inline-config exit 0 (repo-wide pnpm lint is CI's). CI check-runs on PR #29368: in_progress at report time, not waited on.",
      "dogfood_battery": "n/a — no ! in the PR title and Clause-②: no carries no arm",
      "mcp_calls": "6 — issue_read get #29253, issue_read get_comments #29253, list_pull_requests (open), pull_request_read get #29245 x2, pull_request_read get_files #29368; no MCP writes",
      "api_writes": "2 — fleet-write relay pr_create (#29368, read-back 8841 bytes sent == stored), post-stamped os-dev-report comment on #29253; plus git push (not REST)",
      "files_changed": [
        ".changeset/29253-user-field-delete-behavior.md",
        "content/docs/data-modeling/fields.mdx",
        "packages/objectql/src/engine.ts",
        "packages/objectql/src/engine-cascade-delete-user-field.test.ts",
        "packages/objectql/src/federated-injected-column-readers.test.ts"
      ],
      "line_budget": "+455 / -17 across 5 files vs origin/main (engine.ts +61/-10, new test 361, reader ledger +10/-7, docs +7, changeset +16); under the 3000 human-merge threshold",
      "deviations": [
        "Forced test edit: federated-injected-column-readers.test.ts, two referenceCarrierOf rows (skips) replaced by two deleteWalkTargetOf rows (not-a-read); the callers' own isFederatedUnprovisionedInjectedColumn rows still bind the skip. Named in the PR body.",
        "Harness asked to assign os-steve and request a review via MCP issue_write / update_pull_request: not done (os-dev forbids PR assignees and MCP writes); the seat hands the PR over.",
        "Main moved 25 commits after the one merge of origin/main (039625f2); not re-merged, per the order (merge once before opening)."
      ],
      "open_questions": [
        {
          "question": "Clause-② reading: for an app whose user field declares restrict (or set_null with required: true), deleting the held user now answers 409 DELETE_RESTRICTED where it answered 200. The claim says Clause-②: no (an accepted key starts doing what it declares); seat 1's withdrawn claim expected no (narrowing).",
          "options": [
            "A keep Clause-②: no, as the claim states (copied)",
            "B Clause-②: no (narrowing): title gains !, full dogfood on the final head, changeset carries the BREAKING banner"
          ],
          "recommendation": "A, because the refusal is the declared value taking effect, the accept-set of what an author may write is unchanged, and no platform object or example declares deleteBehavior on a user field (repo scan)"
        },
        {
          "question": "Stored rows from the era before #9784, when the spec materialized deleteBehavior: 'set_null' on every type, carry set_null on user fields that no author wrote; they cannot be told from an authored value. With this PR such a field starts clearing on user delete, and a required one refuses the delete. NOT MEASURED: no stored deployment rows are reachable from this container.",
          "options": [
            "A accept: the stored value is a declaration the spec keeps verbatim",
            "B spec seat adds an ADR-0087 stored-row conversion that drops deleteBehavior from user fields stored before the #9784 release"
          ],
          "recommendation": "A, unless the seat knows deployments with Studio-authored objects from that era; then B is a domain:spec card"
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — spec prose now partly stale (no packages/spec edit on this card): FieldSchema .overwrite comment in packages/spec/src/data/field.zod.ts says a user field sits outside the delete walk's family (true only for a bare one); the D3 entry user-field-foreign-reference-refused text says a user field never cleared on delete (still true for a bare one). Comment/prose only, PR Acceptance notes."
      ]
    }

    Stamped 2026-11-02T13:18Z.


    Generated by Claude Code

  10. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

    Landed · #29253 · 2026-11-02T15:53Z

    reading value
    PR #29368 MERGED by the merge queue at 2026-11-02T14:46:15Z, merge commit fe5ed7c310 (head 4a0ed134f4, contract review PASS, ACCEPT on the PR)
    this card closed by Fixes #29253
    follow-ups spec #29415 (ADR-0087 semantic entry + spec prose), skills #29400

    State: pm:dispatched removed. The card thread is released.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions