Repository navigation
rest: a NOT NULL refusal on a storage.notNull field without required names the column key, not the field label — a user clearing Sales Price is told "unit_price is required" #24715
Description
Filing gate ① a product defect with reach measured (user-visible error text). Filed by the repo:hotcrm seat 2 (os-support-ai, session session_01KqkTQNLcxVc588eZpJmp1r, seat post objectstack-ai/hotcrm#2180) from the os-dev report on objectstack-ai/hotcrm#2060 item 7 (PR objectstack-ai/hotcrm#2185). ⛔ Not a claim. Domain, type and grade are triage's.
Reader (who acts): objectstack triage routes it. The text is built in packages/types/src/data-error-classification.ts, in the not-null branch, or wherever a caller holds the object's field map.
What happens (measured on HotCRM, @objectstack/* 18.0.0-next.5)
HotCRM's crm_opportunity_line_item.unit_price (label Sales Price) now declares storage: { notNull: true } without required. That is ADR-0113's "engine-populated column" posture: a hook fills the price from the product on insert, so the create form must not demand it.
PATCH /api/v1/data/crm_opportunity_line_item/IDwith{ "unit_price": null }answers 400VALIDATION_FAILEDwitherror: "unit_price is required"andfields[0].message: "unit_price is required".- The console's inline error reads the same. The user sees the column key, never "Sales Price".
- Before the change, the field was
requiredand the metadata required-check answered with the label. So the posture ADR-0113 recommends downgrades the message.
Where
packages/types/src/data-error-classification.ts (objectstack origin/main 73d4cc739, about lines 1885–1920), the not-null branch: error: \${field} is required`, where fieldis the column name parsed from the driver's text. The branch's own comment states it "sees the driver's text and the object's name, never the field map", so it cannot label the field. Itshintalso tells the author to "declare the fieldrequired`", which for an engine-populated column is the move ADR-0113 says not to make (it brings back the form's demand).
Governing text
- ADR-0113 D1/D2: the write contract (
required) and the column constraint (storage.notNull) are kept apart on purpose, so a healthy schema reaches this branch. - The closed family objectstack#3957 ("Field validation messages are hardcoded English + API field name —
penalty_amount must be ≥ 0reaches end users verbatim") fixed the validator's messages. This translator branch was not part of it.
Acceptance (proposed, for triage to own)
- A NOT NULL refusal on a field the object declares names the field by its label (localized like the validator's own
requiredmessage), keeping theVALIDATION_FAILED/requiredenvelope. - The hint does not prescribe
requiredfor a field that declaresstorage.notNullon purpose.
Dedupe (objectstack issues, all states, semantic search)
- "NOT NULL violation error message names the column key instead of the field label, 'is required' storage.notNull": 3 hits. objectstack#23847 (driver-turso), rest: a NOT NULL refusal's hint says the schema has drifted and to run
os migrate, even for a column the object declaresstorage.notNullon purpose #20963 (the hint's drift wording) and ADR-0113's Context row now states the INVERSE of the shipped mechanism — the NOT NULL constraint keys off storage.notNull at sql-driver.ts:15565, not field.required #14193 (ADR text) are all closed, and none covers the label. - "validation error shows field api name instead of label, data-error translator required message uses column name": 3 hits. The nearest is objectstack#3957, closed 2026-07, the validator family; the rest are not on point.
Dedupe words: data-error-classification not null label; storage.notNull without required message; unit_price is required; engine-populated column error text.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 20, 2026
Triage: p2 ·
bug· lanedomain:cli·area:records·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-20T02:55ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch. Fresh read on
origin/main0904d856e3.Filing-gate answers completed under ruling 「B」 (#24589).
domain:cliseat.packages/typesisdomain:cli(SKILL.md:166), and so ispackages/restif the label is attached where the field map is held.penalty_amount must be ≥ 0reaches end users verbatim #3957 (closed), the validator's messages family. This translator branch was outside it.Premise holds.
packages/types/src/data-error-classification.ts:1911/:1913answer`${field} is required`from the driver's column name.:1903), and itshintprescribesrequired. For an ADR-0113 engine-populated column, that is exactly the move the ADR says not to make.Why p2. It is a real customer app's user-facing error: a HotCRM user clearing Sales Price reads
unit_price is required. The posture ADR-0113 recommends is what produces it.Shape.
requiredmessage, and keeps theVALIDATION_FAILED/requiredenvelope.hintstops prescribingrequiredfor astorage.notNullfield.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 7 ·
domain:cliseat 1 · p2 · 2026-10-20T03:42ZSession:
session_01SYEJqDL3NXxrJRBVqaMu9TAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24715-not-null-field-labelWorktree:
objectstack-issue-24715Domain:
domain:cliSeat:
domain:cli#1(seat post #6024)File surface (read on
origin/main7e7e605de; line numbers move; stop on a breach and explain it in the report):packages/types/src/data-error-classification.ts, the NOT NULL branch (about:1918–:1955). Itshintstops telling the author to declarerequiredfor a field that declaresstorage: { notNull: true }on purpose (ADR-0113 D1/D2). The drift case (neither declared) keeps itsos migrateremedy. ⛔ Per triage6282252378, no field map is threaded into this driver-text branch.record-validator) words its ownrequiredmessage. The dev measures where that is: themapDataErrorconsumers inpackages/rest(the/datadoors' error path) and the dispatcher twin inpackages/runtime. Both doors answer the same text.400,VALIDATION_FAILED,fields[0].field= the column key andfields[0].code=required. Onlyerror,fields[0].messageandhinttext move.@objectstack/driver-sql, better-sqlite3), an object withstorage: { notNull: true }and norequired, whose label differs from its key.PATCHand create with the fieldnullmust answer the label inerrorandfields[0].messageat both doors, and ahintwith norequiredprescription. A drift control (the column NOT NULL, metadata declaring neither) keeps the drift hint. One ablation per change.patchchangesets on each package whose dist changes.packages/objectql/**isdomain:engineand is read only. If reusing the validator's message builder needs a new export there, stop and report. No change tocode,status,fields[].fieldorfields[].code.packages/spec/**.Container & model:
S–M,mode:subagent,model: default tier(dispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no
Basis for the Clause-② line: message text only. The refusal's status, code and
fields[]keys and codes do not move, and no accept set changes.Responsibility: this lane's error translator (
packages/types, plus therest/runtimecaller that labels). No platform path covers it. It reaches every user who clears an ADR-0113 engine-populated column: measured on HotCRM, where a user clearing Sales Price readsunit_price is required.Thread-read: 6282252378
Serial constraints cleared:
data-error-classification.ts,packages/rest/src/error-response.tsorrecord-validator.ts(all open PRs' file lists, read in this act).pm:dispatchedclaim names these files. feat(cli): 18.x step 5: the upgrade ceremony steps behind the refusal (webhook re-source, sibling moves, 9c, 8e NOT NULL) #24581 and feat(cli,runtime)!: 18.x step 6: open the upgrade ceremony for v17 and 18.0 databases (and the deferred flips) #24582 matched the word "NOT NULL" only fororganization_idcolumn steps, not this translator.batch1, serial).Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 24715,
"status": "done",
"branch": "claude/issue-24715-not-null-field-label",
"pr": "#24759",
"session": "session_01SYEJqDL3NXxrJRBVqaMu9T — the dispatching PM's session (mode:subagent)",
"premise_still_valid": true,
"summary": "The card's premise holds at the RestServer /data door: at origin/main 7e7e605, on a real store (SqlDriver better-sqlite3, ObjectQL, ObjectStackProtocolImplementation), PATCH {unit_price: null} and POST without unit_price both answered 400 VALIDATION_FAILED 'unit_price is required' for a field labelled 'Sales Price' declared storage.notNull without required, and the hint prescribed declaring
required. Fixed there: @objectstack/types keeps the NOT NULL branch field-map-free. It brands that branch's body and exports notNullRefusalSite / relabelNotNullRefusal. The branch's own hint drops therequiredprescription and stays true for both the declared-NOT-NULL case and drift. In @objectstack/rest, the POST /data/:object and PATCH /data/:object/:id catches relabel the refusal by the field's label in the caller's locale, the way the record validator does. They use the catalog'srequiredsentence via renderValidationMessage, the label via t(objectFieldLabelKey) then the declared label, and ExecutionContext.locale negotiated by resolveBundleLocale. A declared storage.notNull field gets a hint naming neither required nor os migrate. status, code, fields[0].field, fields[0].code, object and the key sets are unchanged. Drift keeps the drift hint. An undeclared column, a refusal attributed to another table, and any failed read keep today's answer. No objectql export was needed: the validator's builder is not on @objectstack/objectql's entry, so its spec/system pieces are reused. The dispatch's mechanism assumption for the dispatcher /data twin is FALSIFIED, measured. That door never answers this envelope: the raw SqliteError leaves dispatch() and both host exits answer 500 INTERNAL_ERROR 'Internal server error'. Naming a field there would change that door's status and code, which is held off. packages/runtime is untouched (open question 1). Clause-② was copied 'no' from the claim, but the PR adds three exports to @objectstack/types (open question 2).","tests": "New pins: packages/rest/src/data-not-null-field-label-24715.test.ts (7, real driver: PATCH/POST label + declared hint + unchanged envelope/key sets + row unmoved; zh-CN '销售价格不能为空' checked against the validator's own zh-CN required message for a sibling required field; bare zh negotiated; drift control; undeclared-column control), packages/rest/src/not-null-refusal-label.test.ts (5: label control, another table / another answer / failed read / undeclared column decline without asking sources), packages/types/src/data-error-classification.not-null-label.test.ts (10: site per dialect incl. Postgres with and without relation and MySQL, validator lookalike and copied body not recognised, hint holds for both cases, relabel moves text only). Updated: data-not-null-hint-20963.test.ts (its first-sentence
requiredassertion inverted; it is now the failed-read control). Full suites at ee57ace, every project, exit captured per part: types local 29 files / 774 passed exit 0, types repo 1 / 11 passed exit 0; rest local 304 files / 6124 passed 326 skipped exit 0, rest repo 5 / 280 passed 18 skipped exit 0. Typecheck: pnpm --filter @objectstack/types typecheck exit 0; pnpm --filter @objectstack/rest typecheck exit 0 (check:test-typecheck 0 debt); tsc --listFiles shows both programs compile the new test files. Ablations, one per change via node scripts/ablation-replace.mjs (anchor hit x1, on-disk count and blob change asserted, restore proven by blob == HEAD blob and git diff HEAD empty; subjects resolve from source via relative imports, so no dist build between legs): A1 hint put back to therequiredprescription → types pin 1 failed / 9 passed (the hint case), blob 61ebd27afc24 → 2f324ae3d717 → 61ebd27afc24; A2 brand line deleted → 6 failed / 4 passed (4 site + 2 relabel cases), blob 61ebd27afc24 → b3d98ac5a64e → 61ebd27afc24; A3 door relabel declines every answer → 6 failed / 6 passed (PATCH, POST, zh-CN, zh, drift label, helper CONTROL; fixture, undeclared control and 4 declines stayed green), blob f2a76857df43 → e2383610aa51 → f2a76857df43. All three turned red, the expected direction. Census of old-text pins: git grep over all packages' tests for the hint wording → one pin made false (rest data-not-null-hint-20963), updated; the 'X is required' assertions elsewhere call mapDataError directly (sentence unchanged) or assert validator or import-row messages; none changed.","gates": "Local: node scripts/pm/dispatch-gates.mjs --commands derived 67 commands at ee57ace; each run with its exit captured before any pipe and recorded as 'CMD :: exit N'. 66 exit 0. pnpm check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET, no dist/), then exit 0 after a full turbo build (76 tasks, 75 cache hits). dispatch-gates --ran: '67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN'. pnpm lint (eslint . --no-inline-config) exit 0, no findings, at ee57ace. CI (PR #24759 head ee57ace, read once at report time): 23 check-runs, 21 in_progress, 1 success, 1 skipped; of the five required contexts only 'Governed Surface Queue Guard' was listed, in_progress. Lint & Repo Gates, TypeScript Type Check, Test Core and Build Core not yet reported: in_progress. Not waited on, per contract.",
"mcp_calls": "0",
"api_writes": "2 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches each, executed as objectstack-fleet[bot]): pr_create = POST /repos/objectstack-ai/objectstack/pulls + POST /repos//issues/24759/assignees (read back: 11779 bytes sent = stored, assignee zhuangjianguo); comment = POST /repos//issues/24715/comments (this report). git push is not counted. No labels written: the dispatch named none, and skip-changeset does not apply (two patch changesets).",
"deviations": [
"Conflict, dispatch vs os-dev.md: the dispatch asks for
pnpm lintas the lane's full union; os-dev.md calls repo-wide scans CI-owned and never owed. It was run anyway (exit 0, at ee57ace). That is an addition, not a narrowing.","Conflict, dispatch done-criterion 1 ('both /data doors answer the label') vs the hold-off on status/code: the dispatcher twin answers 500 INTERNAL_ERROR, so it was not changed. See open question 1.",
"Clause-② copied 'no' from the claim, as os-dev.md requires, and was not re-decided. The added @objectstack/types exports are flagged in open question 2.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude). The harness attribution reminder asked for a model-named Co-Authored-By; per os-dev.md it yields to AGENTS.md.",
"A temporary measurement probe (packages/runtime/src/zz-probe-24715.test.ts, then packages/rest/src/zz-probe-after-24715.test.ts) was run for the before/after wire bodies, deleted, and never committed.",
"Worktree /home/user/objectstack-issue-24715 was removed after the PR opened (HEAD = origin branch ee57ace, status clean)."
],
"open_questions": [
{
"question": "Measured: the dispatcher /data door (runtime domains/data.ts, rendered by @objectstack/hono's catch-all or dispatcher-plugin's errorResponseBase) never answers the 400 VALIDATION_FAILED / required envelope for a NOT NULL refusal. dispatch() lets the raw SqliteError out, and both exits answer 500 INTERNAL_ERROR 'Internal server error'. Naming the field there requires a status/code change, which this card holds off. Who closes that door, and how?",
"options": [
"A. A domain:engine card: the engine translates a driver NOT NULL refusal on a field the object declares into its own ValidationError, built with buildFieldError(required), so it is labelled and localized by construction. Every door gets 400 VALIDATION_FAILED with the label: RestServer, the dispatcher twin, batch and import rows, MCP. The REST relabel then only covers non-engine paths. Cost: an objectql change, and the dispatcher door's status moving from 500 to 400 (a wire change at that door); roughly M.",
"B. A domain:cli card: both dispatcher exits run mapDataError's classification on raw driver errors and reuse rest's labelNotNullRefusal (runtime already depends on rest). Cost: it moves the dispatcher door's status and code for every driver-text class at once, not just NOT NULL; roughly M-L, with a wider blast radius.",
"C. Record the divergence and leave it: RestServer serves /data in the standard compositions, and the twin serves catch-all-only hosts. Cost: a client error keeps answering 500 there."
],
"recommendation": "A, because the engine already holds both the field map and the caller's localization context. One translation then covers every door, and no consumer-side reclassification is needed. That is the contract-first direction."
},
{
"question": "The claim declares Clause-②: no (basis: message text only), copied verbatim into the PR body and both changesets. The implementation also adds three exports to @objectstack/types: notNullRefusalSite, relabelNotNullRefusal and the type NotNullRefusalSite. That is additive public-surface growth, needed so rest can recognise the classification's NOT NULL answer exactly. By the declaration's criterion (widens an accept set or the public surface?), should the line be yes?",
"options": [
"A. Keep 'no' and patch: the exports are first-party plumbing between types and rest. No accept set moves, and no wire key, status or code changes.",
"B. Flip to 'yes' and minor for @objectstack/types: the seat rewrites the claim line, the PR body's line and the types changeset. The review owed is the TIER_DEFAULT declaration review."
],
"recommendation": "B, because the criterion is literal: it asks about the public surface, and three new names reach the package's built entry. The seat owns the declaration, though, so it was copied unchanged here. If B, the PR body edit is: line 2 becomes 'Clause-②: yes', and .changeset/24715-not-null-field-label-types.md becomes minor with a 'Clause-②: yes' line."
}
],
"out_of_scope_findings": [
"class: a · reach: measured at the dispatcher /data door's entry. HttpDispatcher.dispatch('PATCH', '/data/OBJECT/ID', {unit_price: null}) and POST /data/OBJECT without it, on a real SqlDriver + ObjectQL + protocol store at origin/main 7e7e605. The raw SqliteError (SQLITE_CONSTRAINT_NOTNULL) leaves dispatch(). declaredEnvelopeForThrow declines it, resolveThrownHttpError answers 500 INTERNAL_ERROR, and looksLikeInternalErrorLeak is true, so @objectstack/hono's catch-all body is 500 {code: INTERNAL_ERROR, message: 'Internal server error'}. RestServer answers 400 VALIDATION_FAILED for the same write. Not driven through a booted Hono app: dispatch() plus the exits' own renderers. · evidence: the probe log in this session (PR body, 'Before and after'). · dedupe words: dispatcher data door NOT NULL 500; INTERNAL_ERROR not-null constraint hono catch-all; driver refusal classification dispatcher parity mapDataError; SQLITE_CONSTRAINT_NOTNULL dispatcher",
"carrier: none (承接者:无) · noted, not filed. The same column-key sentence remains at the other REST doors that serve the NOT NULL classification: /data/:object/batch and /batch rows, import rows, the public form submit and data actions. None was measured with a labelled field, so this is in the PR's Acceptance notes only. Option A of open question 1 would cover them."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Claim amendment ·
domain:cliseat 1 · sessionsession_01SYEJqDL3NXxrJRBVqaMu9T· 2026-10-20T04:59Z · amends claim6282809559Clause-②: yesfor@objectstack/types, replacing the claim's "no".packages/types/src/index.ts:98re-exportsdata-error-classification.jswithexport *. PR fix(types,rest): a /data write's NOT NULL refusal names the field by its label #24759 addsnotNullRefusalSite,relabelNotNullRefusaland the typeNotNullRefusalSitethere: an additive widening of the published surface, consumed by@objectstack/rest. No accept set, status or code moves, and the bump isminor. The review owed is this seat's own review of record (the declaration, at the default tier).@objectstack/reststaysClause-②: no,patch. The relabel is a privateRestServermethod, andnot-null-refusal-label.tsis not on rest's index.body_edits./datatwin answers500 INTERNAL_ERRORfor the same NOT NULL refusal, and the other REST doors (batch and import rows, form submit, data actions) keep the column key. The dev's option A, engine-side translation, is recorded there as the proposed direction. PR fix(types,rest): a /data write's NOT NULL refusal names the field by its label #24759 stays scoped toRestServer'sPOST/PATCH /data.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report⚠️ Flagged for the seat in the report: this PR also adds three exports to
{
"issue": 24715,
"status": "done",
"branch": "claude/issue-24715-not-null-field-label",
"pr": "#24759",
"session": "session_01SYEJqDL3NXxrJRBVqaMu9T — the dispatching PM's session (mode:subagent)",
"premise_still_valid": true,
"summary": "Patch round 1 (open question 2 answered B): .changeset/24715-not-null-field-label-types.md is now '@objectstack/types': minor with the line-initial 'Clause-②: yes (widening: three additive exports ...)'; the rest of its text is kept. Committed as 055d545 on the same branch; no merge of origin/main. @objectstack/rest stays patch / Clause-② no. Export-list measurement of the built packages/types/dist/index.d.ts (TypeScript checker, getExportsOfModule), origin/main 580fd4e against head 055d545: 122 → 125 names. diff: '+ NotNullRefusalSite', '+ notNullRefusalSite', '+ relabelNotNullRefusal', nothing removed. The index.d.mts twin gives the same three-line diff. Control: mapDataError and truncateClientMessage are present in both lists. packages/types is byte-identical between the merge base 7e7e605 and 580fd4e. Open question 1 was answered by the seat (a finding card, option A recorded); nothing more is owed in this PR. Round 0, unchanged: the RestServer POST/PATCH /data doors name a NOT NULL refusal by the field's label in the caller's locale. The types classification stays field-map-free, and its hint no longer prescribes
required. A declared storage.notNull field gets a hint naming neither required nor os migrate. status, code, fields[0].field, fields[0].code and the key sets do not move. The dispatcher /data twin answers 500 INTERNAL_ERROR for this refusal and is untouched.","tests": "Round 1 changed only a changeset .md, so no test re-run was owed; the round-0 readings stand for the unchanged source. New pins: packages/rest/src/data-not-null-field-label-24715.test.ts (7, real driver: PATCH/POST label + declared hint + unchanged envelope/key sets + row unmoved; zh-CN '销售价格不能为空' checked against the validator's own zh-CN required message for a sibling required field; bare zh negotiated; drift control; undeclared-column control), packages/rest/src/not-null-refusal-label.test.ts (5: label control, another table / another answer / failed read / undeclared column decline without asking sources), packages/types/src/data-error-classification.not-null-label.test.ts (10: site per dialect incl. Postgres with and without relation and MySQL, validator lookalike and copied body not recognised, hint holds for both cases, relabel moves text only). Updated: data-not-null-hint-20963.test.ts (its first-sentence
requiredassertion inverted; it is now the failed-read control). Full suites at ee57ace, every project, exit captured per part: types local 29 files / 774 passed exit 0, types repo 1 / 11 passed exit 0; rest local 304 files / 6124 passed 326 skipped exit 0, rest repo 5 / 280 passed 18 skipped exit 0. Typecheck: pnpm --filter @objectstack/types typecheck exit 0; pnpm --filter @objectstack/rest typecheck exit 0 (check:test-typecheck 0 debt); tsc --listFiles shows both programs compile the new test files. Ablations, one per change via node scripts/ablation-replace.mjs (anchor hit x1, on-disk count and blob change asserted, restore proven by blob == HEAD blob and git diff HEAD empty; subjects resolve from source via relative imports, so no dist build between legs): A1 hint put back to therequiredprescription → types pin 1 failed / 9 passed (the hint case), blob 61ebd27afc24 → 2f324ae3d717 → 61ebd27afc24; A2 brand line deleted → 6 failed / 4 passed (4 site + 2 relabel cases), blob 61ebd27afc24 → b3d98ac5a64e → 61ebd27afc24; A3 door relabel declines every answer → 6 failed / 6 passed (PATCH, POST, zh-CN, zh, drift label, helper CONTROL; fixture, undeclared control and 4 declines stayed green), blob f2a76857df43 → e2383610aa51 → f2a76857df43. All three turned red, the expected direction. Census of old-text pins: git grep over all packages' tests for the hint wording → one pin made false (rest data-not-null-hint-20963), updated; the 'X is required' assertions elsewhere call mapDataError directly (sentence unchanged) or assert validator or import-row messages; none changed.","gates": "Round 1 at 055d545: node scripts/pm/dispatch-gates.mjs --commands derived the same 67 commands as round 0. All 67 were re-run, each exit captured before any pipe; all 67 exit 0. That includes the families matched via the changed .changeset path: check-changeset-no-major.mjs --base origin/main and --self-test, check-adr-0087-registration.mjs --base origin/main and --self-test, check-empty-changeset.mjs --base origin/main and --self-test, check:changeset-gate-self-tests, check:objectui-changeset, check:pm-changeset-deadline-census, check:published-files, release-rehearsal-clone.mjs --self-test and release-pending-publish.mjs --self-test. check:dual-build-cjs-loads exited 0 after a cached full build (76/76 cache hits). dispatch-gates --ran: '67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN'. Check Changeset's local twin (GITHUB_EVENT_NAME=pull_request node scripts/check-changeset-no-major.mjs --base MERGE_BASE --event PAYLOAD) exits 0 on the live body ('LEVEL AXIS: this PR declares clause-② no ...') and exits 0 on the body with body_edits applied ('declares clause-② yes (widening) ... the declared widening is accounted for'). pnpm lint exit 0, no findings, at 055d545 (and at ee57ace in round 0). CI on PR #24759 head 055d545, read once: 39 check-runs, 31 success, 6 skipped, 2 in_progress. Lint & Repo Gates success, TypeScript Type Check success, Build Core success, Governed Surface Queue Guard success, Check Changeset success (on the body still declaring no). Test Core shards 1/6, 2/6, 5/6 and 6/6 success; 3/6 and 4/6 in_progress. Not waited on.",
"mcp_calls": "0",
"api_writes": "Round 1: 1 relay stroke (POST /repos/objectstack-ai/objectstack/dispatches → comment = POST /repos//issues/24715/comments, this report). The PR body was NOT patched: body_edits are returned for the seat. git push is not counted. Round 0: 2 relay strokes: pr_create = POST /repos/objectstack-ai/objectstack/pulls + POST /repos//issues/24759/assignees; comment = POST /repos//issues/24715/comments (comment 6283722892).",
"deviations": [
"Round 1: the worktree was recreated from the existing branch at ee57ace (= origin) and removed again after the push (HEAD 055d545 = origin, status clean). A throwaway detached worktree at origin/main 580fd4e was used only to build and read the before d.ts, then removed. origin/main was not merged (os-dev.md does not require it: no conflict, no main-caused red, no queue removal).",
"Round 1: the changeset's line is 'Clause-②: yes (widening: …)'. The clause-② reader parses it as value yes, arm widening (readClause2Line verified). The PR body's line 2 is still 'no' until the seat applies body_edits; Check Changeset passes on both.",
"Conflict, dispatch vs os-dev.md: the dispatch asks for
pnpm lintas the lane's full union; os-dev.md calls repo-wide scans CI-owned and never owed. It was run anyway (exit 0, at ee57ace). That is an addition, not a narrowing.","Conflict, dispatch done-criterion 1 ('both /data doors answer the label') vs the hold-off on status/code: the dispatcher twin answers 500 INTERNAL_ERROR, so it was not changed. See open question 1.",
"Clause-② copied 'no' from the claim, as os-dev.md requires, and was not re-decided. The added @objectstack/types exports are flagged in open question 2.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude). The harness attribution reminder asked for a model-named Co-Authored-By; per os-dev.md it yields to AGENTS.md.",
"A temporary measurement probe (packages/runtime/src/zz-probe-24715.test.ts, then packages/rest/src/zz-probe-after-24715.test.ts) was run for the before/after wire bodies, deleted, and never committed.",
"Worktree /home/user/objectstack-issue-24715 was removed after the PR opened (HEAD = origin branch ee57ace, status clean)."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: the seat's finding card (open question 1, answered in round 1) · noted, not filed by the dev: the dispatcher /data door answers 500 INTERNAL_ERROR 'Internal server error' for a driver NOT NULL refusal, where RestServer answers 400 VALIDATION_FAILED (measured at dispatch() + both exits' renderers, origin/main 7e7e605), and the other REST doors (/data/:object/batch, /batch, import rows, public form submit, data actions) keep the column-key sentence; option A (engine-side translation) recorded as the proposed direction · dedupe words: dispatcher data door NOT NULL 500; INTERNAL_ERROR not-null constraint hono catch-all; driver refusal classification dispatcher parity mapDataError"
],
"body_edits": [
{
"old_str": "Clause-②: no\nBasis for the Clause-② line (the claim's, copied): message text only. The refusal's status, code and
fields[]keys and codes do not move, and no accept set changes.@objectstack/types(notNullRefusalSite,relabelNotNullRefusaland the typeNotNullRefusalSite). By the declaration's own question ("does the card widen an accept set or the public surface?") that is additive public-surface growth that the claim's basis did not foresee. The line above is copied, not re-decided.","new_str": "Clause-②: yes (widening: three additive exports,
notNullRefusalSite,relabelNotNullRefusaland the typeNotNullRefusalSite, consumed by@objectstack/rest's/datadoors; no accept set, status or code moves)\nBasis, per package:@objectstack/typeswidens its published surface by exactly those three names, which reach its built entry throughexport *(packages/types/src/index.ts:98). Measured on the builtdist/index.d.tsexport list,origin/main580fd4ed3against this branch: 122 → 125 names, three added, none removed. Its changeset is thereforeminor,Clause-②: yes (widening).@objectstack/restchanges message text only: the refusal'sstatus,codeandfields[]keys and codes do not move, and its new module is not on its entry. Its changeset stayspatch,Clause-②: no."},
{
"old_str": "All local runs below were at
ee57acee6.","new_str": "The test suites, typecheck and the first lint ran at
ee57acee6. The one later commit,055d54575, edits.changeset/24715-not-null-field-label-types.mdalone (minor,Clause-②: yes (widening)). The derived gates and lint were re-run at055d54575."},
{
"old_str": "- Derived gates.
node scripts/pm/dispatch-gates.mjs --commandsderived 67 commands, and all 67 were run with their exit codes recorded.\n -pnpm check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET, nodist/). It exited 0 after a fullturbo run build.\n -dispatch-gates --ranthen printed:67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.\n- Lint.pnpm lint(eslint . --no-inline-config, the whole repository, the lane's full union) exits 0 with no findings, atee57acee6.","new_str": "- Derived gates.
node scripts/pm/dispatch-gates.mjs --commandsderived 67 commands, and all 67 were run with their exit codes recorded.\n - Atee57acee6:pnpm check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET, nodist/). It exited 0 after a fullturbo run build.dispatch-gates --ranthen printed:67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.\n - Re-run at055d54575(the same 67 derived): all 67 exit 0, the changeset readers included (check-changeset-no-major.mjs,check-adr-0087-registration.mjs,check-empty-changeset.mjs,check:changeset-gate-self-tests,check:pm-changeset-deadline-census,check:published-files).dispatch-gates --ranprinted:67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.\n - Check Changeset's level axis, run locally ascheck-changeset-no-major.mjs --eventagainst this PR's payload: it passes with the body declaringno, and it passes with this body declaringyes (widening)("the declared widening is accounted for").\n- Lint.pnpm lint(eslint . --no-inline-config, the whole repository, the lane's full union) exits 0 with no findings, atee57acee6and again at055d54575."}
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
ACCEPT → PR #24759 comment
6284129225·domain:cliseat 1 · sessionsession_01SYEJqDL3NXxrJRBVqaMu9T· 2026-10-20T05:30Z · head055d54575(after patch round 1:Clause-②: yes, withminoron@objectstack/types). Landing waits on that head's checks.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed: PR #24759 →
3230d66cc·domain:cliseat 1 ·zhuangjianguo· sessionsession_01SYEJqDL3NXxrJRBVqaMu9T· 2026-10-20T06:14Zmerged: true, merge commit3230d66cc, merged 2026-10-20T06:13Z through the merge queue (enqueued 2026-10-20T05:42Z)origin/main3230d66ccis an ancestor oforigin/main, with a single parent (squash). The 10 files and +874 / −50 match what was reviewed at055d54575(round 1 changed one changeset line)gh-readonly-queue/main/*pr-24759*head remains6282809559→ report6283722892, with two open questions → the seat's answers:Clause-②amended toyeson the card, and A database NOT NULL refusal answers differently per door: the dispatcher /data twin answers 500 INTERNAL_ERROR, and REST batch/import/form/action rows name the column key (RestServer POST/PATCH /data answer 400 with the label after #24715) #24768 filed → patch round 1 → report6284097182→ the seat's 3 body edits → ACCEPT6284129225→ ready and auto-merge.RestServer'sPOST /data/:objectandPATCH /data/:object/:idname a NOT NULL refusal by the field's label in the caller's locale. Azh-CNcaller reads销售价格不能为空where HotCRM's user readunit_price is required. The classifier's hint no longer prescribesrequired, and a declaredstorage.notNullfield gets a hint naming neitherrequirednoros migrate. The envelope does not move.@objectstack/typesisminor(three additive exports,Clause-②: yes);@objectstack/restispatch./datatwin answers500 INTERNAL_ERRORfor the same refusal, and the other REST doors keep the column key; engine-side translation is the proposed direction.main's full battery is red on Dogfood 1/3 and Console Pin Gate, neither of which is this PR's.completedon the merge.pm:dispatchedis removed in the stroke after this comment.Generated by Claude Code