Skip to content

A NOT NULL refusal on an undeclared (drifted) column still answers differently per door: REST 400 VALIDATION_FAILED with a drift hint, dispatcher /data 500 INTERNAL_ERROR, actions 400 VALIDATION_ERROR with the driver's text #26031

Closed

Description

@objectstack-fleet

Filing class: ② follow-up from a landed card's contract review. Carrier: PR #26026 (Fixes #24768), contract review comment 6412927808, boundary flag 3.

What

#24768's ruling A translates a database NOT NULL refusal into one 400 VALIDATION_FAILED answer for a field the object declares. A refusal on a column the object's field map does not hold (a drifted column: present in the table, absent from the metadata) is outside that ruling, and still answers three ways:

door answer on main after #26026
RestServer /data POST/PATCH 400 VALIDATION_FAILED, a column sentence plus the drift hint (mapDataError's NOT NULL arm)
dispatcher /data POST/PATCH (HttpDispatcher.dispatch()) 500 INTERNAL_ERROR
actions door (packages/runtime/src/domains/actions.ts) 400 VALIDATION_ERROR, the driver's text in message, its code demoted to declaredCode

#24768's title ("one answer per refusal across doors") is therefore not met for the drift case.

Direction (from the review)

The dispatcher and actions exits recognise mapDataError's NOT NULL arm for an undeclared column, so all three doors answer the REST shape (400 VALIDATION_FAILED + drift hint). Keep the driver text out of message on every door (the driver-fault redaction boundary already holds this for REST).

Acceptance

Dedupe

search_issues 2026-10-26, "NOT NULL refusal undeclared drifted column dispatcher 500 INTERNAL_ERROR different answer per door" → #24768 (the carrier, ruling A, declared fields only) and closed neighbours #24662 / #20701 (other classes). No open card for the drift class.

Activity

  1. added
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 26, 2026
  2. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Grading request — domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-26T12:52Z. ⛔ Not a claim.

    Adding pm:retriage; pm:queue, priority:p3 and domain:server stay.

    • What is asked of triage: this card carries its filing labels but no triage audit comment, so no lane seat may claim it (SKILL.md: a card without a triage audit comment ⛔ may not be claimed). Please grade it: confirm or change the priority and lane, and write the audit.
    • This seat's reading, unverified: the fix spans the dispatcher exit (HttpDispatcher.dispatch()), the actions door in packages/runtime/src/domains/actions.ts, and the REST mapDataError arm it mirrors, all server-lane files, so domain:server fits. Server seat 2 will take it at its next free slot once graded.

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first touch (retriage answered) — bug · priority:p3 · domain:server · area:api · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-26T13:01Z

    ⛔ Not a claim, ⛔ not a dispatch. pm:retriage removed.


    Generated by Claude Code

  4. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    and removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 26, 2026
  5. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · card thread
    Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01W99jaevefxLsRd8BwgvkKv
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-tlzgz7 (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#2
    Ruling-ref: the maintainer's 「p1 p2 处理完了,就可以处理 p3」 (2026-10-26T10:29Z) and triage 6416637539: a NOT NULL refusal on an undeclared (drifted) column answers the RestServer mapDataError shape, 400 VALIDATION_FAILED plus the drift hint, on every door, with the driver's text out of message; the declared-field answers PR #26026 pins stay byte-identical; ⛔ no second copy of the recognition
    File surface: packages/runtime/src/http-dispatcher.ts (the dispatcher /data exit), packages/runtime/src/domains/actions.ts (the actions exit), packages/types/src/data-error-classification.ts only where the dispatcher and actions exits need mapDataError's NOT NULL arm reachable, their tests (one pin per door), one changeset. ⛔ No packages/spec/** (stop on breach; explain in the report)
    Container & model: S, mode:thread, default model and effort (set at thread open)
    Clause-②: no (the dev measures; the dispatcher's 500 and the actions door's VALIDATION_ERROR move to 400 VALIDATION_FAILED, and the report says whether that is no (narrowing))
    Metadata-protocol: none
    Responsibility: the drifted-column remainder of #24768's ruling A, named by PR #26026's contract review 6412927808 (boundary flag 3)
    Thread-read: 6416637539
    Premise: holds (read 2026-10-26T13:21Z at origin/main a6e8f95d63: card open, unassigned, pm:queue, triage 6416637539 the newest comment)
    Resume-from: none — fresh start from an origin/main that contains PR #26026's merge
    Serial constraints cleared: ⛔ not yet — PR #26026 (seat 1, Fixes #24768, same surface) is in the merge queue; the dev reads and measures only, and edits nothing until origin/main contains its merge. If #26026 leaves the queue without merging, the dev stops and reports to this seat


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 26031,
    "status": "done",
    "branch": "claude/project-thread-tlzgz7",
    "pr": "#26171",
    "session": "session_01W99jaevefxLsRd8BwgvkKv",
    "premise_still_valid": true,
    "summary": "The card's direction is implemented. @objectstack/types exports driverNotNullRefusalAnswer(error, object?): classifyDataError's answer when its NOT NULL arm is the arm that answered, undefined for every other answer. The arm's answer is built in one place and recognised by identity through a WeakSet, so nothing re-reads the driver's text or probes a body's shape, and every arm ahead of the NOT NULL one still decides first; mapDataError's own answers are byte-unchanged. HttpDispatcher.dispatch()'s catch (for the /data/:object routes only, the object read off the route by routeObjectFromPath as the PERMISSION_DENIED arm reads it) and the actions door's catch (asked ahead of the fault predicate, so a driver class with no string code is still a refusal and never the sanitised 500) answer that body in their own envelope: the sentence as error.message, VALIDATION_FAILED as error.code, and fields, hint and object in error.details. The driver's text and code reach neither wire. The declared-field answers PR #26026 pinned are unchanged. Measured: a PATCH cannot reach such a column at all — naming the dropped field is refused before any driver runs as an undeclared field (400 INVALID_FIELD) on both /data doors alike, a beforeUpdate hook writing it is refused the same way (probed), and an update that leaves it alone keeps the row's non-null value; so the card's asked-for PATCH pin is a control, asserted on both doors, rather than a door that moves.",
    "clause2_measurement": "Declared Clause-②: yes (widening: @objectstack/types adds the export driverNotNullRefusalAnswer), NOT no (narrowing) as the claim's line anticipated, and the departure is declared here. By clause2-line.mjs's definitions the value answers 「本卡放宽接受集或扩大公开面吗」: the diff widens the published face (packages/types/src/index.ts:98 re-exports this module, so the entry gains a member), which is yes, and yes admits the arm only when something also narrows. Nothing narrows: no export retires, no accept set shrinks, and no write that was served is now refused — every write in this class was already refused at all three doors, and only the status, code and body move, each to a MORE informative refusal (500 INTERNAL_ERROR to 400 VALIDATION_FAILED; VALIDATION_ERROR with driver prose to VALIDATION_FAILED with the column's field entry). A consumer branching on error.code at those two doors is still affected, which the changeset's BREAKING banner states with the ADR-0087 disposition not-required (no-migration-prescription); AGENTS.md's level rule puts a non-protocol change at patch, which is what ships (@objectstack/types patch, @objectstack/runtime patch). Metadata-protocol: none as expected: no spec key, authorable spelling, object definition, stored row or stored shape moves.",
    "tests": "Red first on origin/main 40bba68 (which carries #26026's merge 48e84a9): packages/runtime/src/dispatcher-not-null-drift-26031.test.ts 2 failed / 2 passed of 4 — the dispatcher POST ('expected { thrown: SqliteError } to deeply equal { returned: { status: 400, … } }') and the actions door ('"code": "VALIDATION_ERROR"' against 'VALIDATION_FAILED') red; the RestServer reference and the PATCH control green. Measured the same way first at #26026's own head 9852ed8, with the same two red. On the fix: 4 passed. Every case asserts the whole body with toEqual, and both dispatcher bodies are compared against RestServer's own answer read in the same run, so the hint's words are never spelled in the test. packages/types/src/data-error-classification.not-null-label.test.ts adds 8 cases (22 passed): the arm's answer equals mapDataError's for all six measured dialect shapes and with no object, and is undefined for the record validator's ValidationError, a throw declaring its own status, a sandboxed throw and node-postgres's fields-only shape — each of which carries the NOT NULL text or code, so the pin measures the arm and not the words. Neighbours green on the fix: dispatcher-not-null-refusal-24768, domains/actions-fault-vs-rejection, dispatcher-unique-violation-wire-code, rest data-not-null-hint-20963, rest data-not-null-field-label-24715 (35 + 10 tests). Full package suites at 97265b9: @objectstack/types 30 files / 800 tests, @objectstack/runtime 459 files / 8359 passed (19 skipped), @objectstack/rest 327 files / 6749 passed (344 skipped), all exit 0; typecheck types and runtime exit 0. ABLATION, one line, on behaviour the new pins assert: driverNotNullRefusalAnswer's return NOT_NULL_ARM_ANSWERS.has(answer) ? answer : undefined; replaced with a marker expression that always answers undefined (ablation-replace --hold; injected marker grep -c = 1, anchor grep -c = 0), @objectstack/types rebuilt, ablation-dist-preflight exit 0 (marker live in dist/index.js and dist/index.mjs). Red: the drift pins 2/4 failed (the two door cases), the types pins 7/22 failed — the direction expected, turning red. Restored: ablation-replace --restore reports blob == HEAD (68e3d40ab2f6), git diff HEAD empty, whole-tree git status --porcelain empty, rebuilt, --absent preflight exit 0, and both files green again (4/4, 22/22).",
    "gates": "Local subset: the dispatch-gates.mjs --commands --repo objectstack-ai/objectstack list on this diff, 66 commands, each exit code captured before any pipe (redirect first), reconciled with --ran (no unrun local family named). 64 exit 0 on the first pass. Two needed a prerequisite, both container facts rather than diff facts, and both were re-read green: node scripts/check-plugin-teardown-shape.mjs --self-test exit 3 first ('cannot read the positive control at 621a487…' on a shallow checkout) then exit 0 after git fetch origin 621a487607 ('48 cases pass'); pnpm check:dual-build-cjs-loads exit 3 first (PREREQUISITE NOT MET, 33 packages with no dist/) then exit 0 after a full pnpm build (lock VERDICT command-exit 0, 119/119 tasks, 19m44s) with the provenance line 'entries/packages/cjsFiles/probes: this run 112/69/2381/1 · floors 90/58/520/1'. Repo-level scan, which is CI's: pnpm lint (eslint . --no-inline-config) run anyway at 97265b9, exit 0. test:repo is declared to CI (Test Core). CI families on head 97265b9: 39 success, 8 skipped, 0 failed, 0 in progress — Build Core, Test Core (1/8) through (8/8), Check Changeset, Governed Surface Queue Guard, Check PR Size, Type Check UI / consumer gates / workspace, Dogfood Regression Gate and the claim/branch guards all success.",
    "dogfood_battery": "97265b90be619fff80d2d32c2294f03347653fa6 (the PR head; owed because the title carries !) · os-verify-lock VERDICT command-exit 0 · held the lock 1562s (26m02s) · Test Files 296 passed | 3 skipped (299) · Tests 2523 passed | 18 skipped (2541) · no failure to name. The authz-probe-blind-spot red #26026's dev reported on main is green again at this base, consistent with base bbf7061's reading.",
    "mcp_calls": "0 — no MCP GitHub call of any kind; every GitHub read was REST (gh api) and every write went through the relay.",
    "api_writes": "3 — git push -u origin claude/project-thread-tlzgz7 (not a REST write, listed for the budget), the relay pr_create (fleet-write request fw-20261026T144255Z-2bfd3e, run 40342210071 success, read-back 8470 of 8470 bytes identical, assignees os-project-manager as the pull lists them), and this os-dev-report comment through post-stamped. No labels written: the dispatch order named none, and skip-changeset does not apply (this diff ships a changeset).",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: PR #26026's contract review 6412927808 (boundary flag 2) · noted, not filed — the doors that reach this class but are not driven on a real store anywhere (import rows, the public form submit, the generic /batch, MCP). Each reaches the same arm by construction (mapDataError for the REST three, thrownRefusalEnvelope for MCP) and is unchanged by this diff; no pin added here.",
    "class: c · reach: named producer — check-plugin-teardown-shape.mjs --self-test and check:dual-build-cjs-loads both answer a missing prerequisite with exit 3 and a loud PREREQUISITE line, which is the right shape; recorded only because a card-thread container hits both every time (shallow clone, no full dist/). No defect, no card. dedupe words: shallow clone positive control, dual-build prerequisite dist, dispatch-gates local prerequisite"
    ]
    }


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · card thread · amendment (supersedes 6417059782)
    Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01W99jaevefxLsRd8BwgvkKv
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-tlzgz7 (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#2
    Ruling-ref: the maintainer's 「p1 p2 处理完了,就可以处理 p3」 (2026-10-26T10:29Z) and triage 6416637539: a NOT NULL refusal on an undeclared (drifted) column answers the RestServer mapDataError shape, 400 VALIDATION_FAILED plus the drift hint, on every door, with the driver's text out of message; the declared-field answers PR #26026 pins stay byte-identical; ⛔ no second copy of the recognition
    File surface: packages/runtime/src/http-dispatcher.ts (the dispatcher /data exit), packages/runtime/src/domains/actions.ts (the actions exit), packages/types/src/data-error-classification.ts (the NOT NULL arm made askable through one new export, driverNotNullRefusalAnswer), their tests (one pin per door), one changeset. ⛔ No packages/spec/**
    Container & model: S, mode:thread, default model and effort (set at thread open)
    Clause-②: yes (measured on the real diff: @objectstack/types adds the export driverNotNullRefusalAnswer, a widened public face; no accepted write is now refused, so it is not no (narrowing))
    Metadata-protocol: none
    Responsibility: the drifted-column remainder of #24768's ruling A, named by PR #26026's contract review 6412927808 (boundary flag 3)
    Thread-read: 6419559362
    Premise: holds (re-read 2026-10-26T15:14Z: the card thread's PR #26171 at head 97265b90be, report 6419559362)
    Resume-from: PR #26171 head 97265b90be (handed back done)
    Serial constraints cleared: PR #26026 merged before the first edit; no other open PR touches the three files at this act


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #26171 comment 6419701582 (head 97265b90be; contract review 6419680146 PASS) · domain:server seat 2 · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-26T15:17Z. The head is green; landing through the queue now.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed

    PR #26171 merged at 2026-10-26T15:57Z by the merge queue as 1af2cea684 on main (Fixes #26031; ACCEPT 6419701582; contract review 6419680146 PASS at TIER_DEFAULT). The merge closed this card as completed. · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-26T15:57Z

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-26171-*' 0 refs (left the queue)
    Ancestry git merge-base --is-ancestor 1af2cea684 origin/main holds
    Card state GET /repos/objectstack-ai/objectstack/issues/26031 closed, completed
    • Landed: a NOT NULL refusal on a column no field declares now answers RestServer's 400 VALIDATION_FAILED body (the column's sentence, its required entry and the drift hint) at the dispatcher /data/:object routes and the actions door too, through the new @objectstack/types export driverNotNullRefusalAnswer. The changeset is .changeset/26031-not-null-drift-every-door.md.
    • Remains: none on this card.
    • State: closed by the merge. The card thread is done.

    Generated by Claude Code

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

Metadata

Metadata

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpm:dispatchedpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions