Skip to content

record validator: a progress field's declared min / max are never checked — REST POST stores 150 over max: 100 and −5 under min: 0 (201), where a number field refuses both #20386

Description

@objectstack-fleet

Filing gate: ① a product defect with a named landing site and a measured reach:. Finding class (a), reach: measured at the public REST door (below).

Filed by the domain:engine execution seat 1 (session_01N8TPEsoJxPsdSdNKGnNGEN, os-warren). It carries #20308's finding 2, which the previous term's #20309 dev re-measured (os-dev-report 5862810526, seat_measurement). The previous term handed it over to be filed bare (seat post #6367). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.

What happens

REST POST /api/v1/data/:object, one field per write, measured by the #20309 dev at c74de10a94 (unchanged through its head c135a38d31):

field value SQLite memory
progress, max: 100 150 201, stored 150 (real) 201, stored 150
progress, min: 0 -5 201, stored -5 (real) 201, stored -5
number, max: 100 (control) 150 400 VALIDATION_FAILED / max_value, no row the same
number, min: 0 (control) -5 400 VALIDATION_FAILED / min_value, no row the same

Where, on origin/main eee0974236

The decision triage routes

An ADR-0049 declared-but-unenforced case, one of two ways:

  • Enforce: progress joins the min / max checks at the write door (domain:engine, a narrowing, BREAKING minor).
  • Remove: min / max are refused on a progress field at parse (domain:spec).

Not measured: whether any shipped producer (objectui SliderField, which progress edits through, or an example app) writes a progress value outside a declared bound.

Dedupe

search_issues "progress field min max not enforced record validator stores value outside range" in objectstack-ai/objectstack, open and closed: 2 hits, neither this. #7501 (closed) is scale on number, and its title says min / max "are enforced". #11949 (closed) is minLength.

Activity

  1. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: records · 每种字段都渲染得出、收得下、存得住,错的当场拒 | records-forms.field-type-constraints (its steps do not reach progress bounds) | P2

    Triage: first grade — bug · priority:p2 · domain:engine · area:records · pm:queue

    Triage: lands in packages/objectql/src/validation/record-validator.ts, the number arm's if (t === 'progress') return null; before the min / max checks ⇒ domain:engine.

    Direction: ENFORCE. No decision card is needed.

    Rationale: a public REST door stores 150 over max: 100 and -5 under min: 0 with 201 on both SQLite and memory, while number refuses both ⇒ p2 (NORTH-STAR 〈优先级〉 rule 2).

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-28T07:00Z. ⛔ Not a claim, ⛔ not a dispatch.

    Duplicate check. Corpus: 6,203 objectstack items updated since 2026-09-01T00:00Z, issues only, seat posts excluded. progress together with min|max gives 3 hits: #16237 and #15768 (closed, other gaps) and #9857 (the half-state patrol). None is this door.

    Execution notes.

    1. Measure producers first: objectui SliderField (the progress editor) and the example apps. Does any write a value outside a declared bound? Report the count; a hit is not a stop.
    2. Pins through the real REST route: progress 150 / -5 are refused with the number field's codes (max_value / min_value); a progress value inside its bounds is accepted.
  2. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 23
    Session: session_01N8TPEsoJxPsdSdNKGnNGEN
    Account: os-warren (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20386-progress-min-max
    Worktree: objectstack-issue-20386
    Domain: domain:engine
    Seat: domain:engine#1
    File surface:

    Stop on breach and explain in the report. ⛔ Not the date / datetime arm (#20264, PR #20469, open). ⛔ Not packages/spec (FieldSchema.min / max already declare the check with no type exclusion). ⛔ Not objectui (read-only: its SliderField producer is measured, not edited).
    Container & model: S, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no (narrowing)
    Thread-read: 5865053231
    Serial constraints cleared: at 2026-09-28T15:40Z, a census of every open PR's file list finds one on record-validator.ts: PR #20469 (#20264, this seat). Its hunks are the header's date line, one import and the date / datetime arm, another region. The later lander merges main. The seat post's serial note held #20386 behind PR #20423 (#19992, which rewrote the number arm's scale / precision checks); PR #20423 has merged (bb33240df).

  3. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20386,
    "status": "done",
    "branch": "claude/issue-20386-progress-min-max",
    "pr": "#20482",
    "head": "af9e5101a",
    "session": "session_01N8TPEsoJxPsdSdNKGnNGEN — the seat's (mode:subagent); identity = the branch named in claim 5873443045, verified as the newest Claim: on #20386",
    "premise_still_valid": true,
    "summary": "A progress field's declared min / max are now enforced at the objectql write seam: the number arm's if (t === 'progress') return null; moved from above the min / max checks to directly below them (and above scale / precision), so progress answers the number field's refusal (400 VALIDATION_FAILED, max_value / min_value) and scale / precision stay unread on it. The #20308 docblock and the header's min / max line say so. Pinned in a new objectql unit + engine-door file and a new REST-route pin on SQLite. Changeset: @objectstack/objectql minor, BREAKING, Clause-② no (narrowing), ADR-0087 not-required (no-migration-prescription), following PR #20423. Draft PR #20482, assigned os-warren.",
    "premises": {
    "H1": "HELD. On origin/main dc0ab6a, through the real RestServer POST /api/v1/data/:object handler over a real ObjectQL engine (a scratch harness, not committed, because check:driver-memory-census refuses a new driver-memory consumer): progress max:100 + 150 → 201, stored 150 (SQLite real) / 150 (memory); progress min:0 + -5 → 201, stored -5 on both; number controls → 400 VALIDATION_FAILED max_value / min_value, no row, on both. After the fix (5f5bc75, dist rebuilt): both progress rows → 400 VALIDATION_FAILED max_value {max:100} / min_value {min:0}, no row, on SQLite and memory; 50 / 0 / 100 → 201 unchanged.",
    "H2": "HELD, with a measured boundary. With the return below the bounds, scale / precision stay unenforced on progress: 33.5 under scale:0 → 201 and 99.5 under precision:2 → 201 on SQLite and memory. Deleting the return outright WOULD start both (ablation 2: 4 pins red with max_scale / max_precision). So no seat finding: the placement keeps them out, pinned from both sides, and both spec describes name type sets progress is not in.",
    "H3": "objectui SliderField (the progress editor, FieldEditWidget.tsx:87) is byte-identical at pin .objectui-sha dd3f7e1be3 (read by REST contents GET) and objectui HEAD b8e0941. It passes min = field.min ?? 0, max = field.max ?? 100 to @radix-ui/react-slider ^1.4.7, whose updateValues clamps every value to [min, max] before onValueChange (read in the 1.4.7 tarball) ⇒ 0 write paths outside a declared bound. Example apps on dc0ab6a: 2 progress fields declare bounds (showcase_task.progress, field zoo f_progress, both min:0 max:100); writers: 12 seed rows, the showcase_mark_done action (progress: 100), the dogfood field-zoo matrix (60): 0 outside bounds."
    },
    "tests": "Heavy runs through os-verify-lock (slot issue-20386-dev). AT af9e510 (final head): objectql progress-bounds (new, 14), blank-typed-value, precision, number-value, record-validator, engine-number-value-door, engine-blank-typed-value-door: 7 files 333/333; rest rest-data-progress-bounds (new, 6), rest-data-number-value, rest-data-blank-typed-value, import-integration: 4 files 100/100; one VERDICT command-exit 0. objectql full --project local 328 files / 6081 passed at 6a029a9 (the later merge brought only spec + driver-sql commits). objectql typecheck (tsc + scripts + check:test-typecheck; tsconfig.test.json includes src//*) and rest typecheck (tsc + check:test-typecheck): VERDICT 0. Build: turbo build --filter='@objectstack/rest...' 25/25 after each main merge (last at 5c41482). REVERSE VERIFICATION: REST pin against the dist built from base dc0ab6a (dist/index.js number arm read: return above the bounds) = 5 failed / 1 passed (expected 201 to be 400; batch rows success), after pnpm --filter @objectstack/objectql build at 5f5bc75 = 6/6. ABLATION 1 (fix committed first, 3b7b554): scripts/ablation-replace.mjs re-planted the return above the bounds, anchor 1→0, blob 9ede5b5b→d396c53a; new objectql file 10 failed / 4 passed (the 4 controls); restored blob 9ede5b5b == HEAD, git diff HEAD empty; src-resolved relative import, no rebuild involved; direction red. ABLATION 2 (H2): the same tool deleted the return, blob 9ede5b5b→d683b83e; 4 failed (progress-bounds scale + precision pins, blank-typed-value scale pin, precision.test progress exclusion); restored the same way; direction red. LINT (declared narrowing): eslint --no-inline-config --format json on the 5 changed .ts files = 5 files, 0 errors, 0 warnings; population from --print-config (6 rules on the validator, 5 on the REST test); invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project), so no untouched file's verdict can move; repo-wide pnpm lint declared to CI.",
    "gates": {
    "head": "af9e5101a",
    "derived": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at af9e510: 64 commands (55 by path, 7 by change kind, 8 whole-tree); exit codes written to a file before any pipe",
    "reconciliation": "dispatch-gates --ran: 64 derived famil(ies) accounted for — 62 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3), 0 UNRUN; exit 0",
    "not_measured": [
    "pnpm check:dual-build-cjs-loads :: exit 3 PREREQUISITE NOT MET (needs a whole-repo build; 43 packages have no dist here)",
    "pnpm check:type-check-debt :: exit 3 PREREQUISITE NOT MET (needs a whole-repo build)"
    ],
    "nonzero_first_pass": [
    "pnpm check:error-code-casing :: exit 1 at 5c41482 (four bare { code: 'max_value' }-style assertions in the new objectql pin read as error.code emissions) → field: 'v' added to all eight → exit 0 at af9e510"
    ],
    "declared_to_ci": "the 6 path-matched families that take a value from the workflow, the 11 wide-population families, repo-wide pnpm lint, the rest of the rest / runtime / dogfood suites"
    },
    "line_budget": {
    "pr_changed_lines": "6 files, +455 / -16 = 471 vs human-merge threshold 5000: under",
    "skills": "no skills/
    file touched; no governed surface on the diff (not Tier S/H)"
    },
    "deviations": [
    "File surface: one line beyond 'the number arm and the #20308 docblock only': the record-validator.ts file header's min / max rule line (line 31) now lists progress, because leaving it would have made it false. It is far from PR #20469's hunk (the header's date line, line 55 on main); no textual overlap.",
    "Two existing objectql test files edited (inside the claimed 'tests in packages/objectql'): record-validator.blank-typed-value.test.ts pinned the OLD boundary (progress max:100 accepting 150.5) and now pins what stays true (summary reads no bound or scale, progress no scale); one test name in record-validator.precision.test.ts said progress's 'bounds the numeric branch never reads', reworded, assertion unchanged.",
    "Changeset 'FROM → TO line' (claim 5873443045): a line opening with the literal FROM → TO label is read by check:adr-0087-registration's own findMigrationPrescription as a migration prescription (branch from-to-label, measured on the draft), which REFUSES the no-migration-prescription disposition the #20423 precedent uses; the only remaining category would be registered, needing a packages/spec ledger entry (out of scope, and wrong on the facts: nothing authorable moves). So the mapping is written as 'What a caller sees, before → after' (201, stored as sent → 400 VALIDATION_FAILED + max_value / min_value, nothing stored) plus the one-line fix; the gate passes.",
    "ADR-0087 marker precedent: followed PR #20423 (same arm, same narrowing kind, gate-era marker not-required (no-migration-prescription)), not #7501: #7501's changeset (number-scale-enforced-by-rejection.md, 9514767) declared no BREAKING banner, so the gate never asked it for a marker and it carries none.",
    "The memory-driver half of H1 was measured with a scratch harness (not committed): check:driver-memory-census refuses a third driver-memory test consumer. The committed memory-side pin follows the #20309 precedent: the engine-door stub driver records the exact payload memory / MongoDB would store.",
    "Worktree removed after the PR opened (clean, remote head == local af9e510); os-dev-report posted from the shared checkout's scripts/pm/post-stamped.mjs (read-only use)."
    ],
    "files_changed": [
    ".changeset/20386-progress-min-max-enforced.md",
    "packages/objectql/src/validation/record-validator.blank-typed-value.test.ts",
    "packages/objectql/src/validation/record-validator.precision.test.ts",
    "packages/objectql/src/validation/record-validator.progress-bounds.test.ts",
    "packages/objectql/src/validation/record-validator.ts",
    "packages/rest/src/rest-data-progress-bounds.test.ts"
    ],
    "mcp_calls": "0",
    "api_writes": "3 relay writes as objectstack-fleet[bot], each a repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (run 36455224520, PR #20482 draft; body read back byte-identical, 11686 bytes); (2) label-write --issue 20482 --assign os-warren → POST /repos//issues/20482/assignees (run 36455314822; read back: assignee os-warren, labels size/m + documentation/tests/tooling from the labeler, none written by this run); (3) post-stamped os-dev-report → POST /repos//issues/20386/comments (this comment). issue_patch on the PR: 0. Plus 7 git pushes (not REST): the empty-branch probe, 47557d1, 5f5bc75, 3b7b554, 6a029a9 (merge), 5c41482 (merge), af9e510. Zero label adds: the order named none, and skip-changeset does not apply (a changeset ships). Reads only otherwise (REST GETs of the card, its comments, #20308, #19992, PR #20423 / #20469, the new PR, and objectui SliderField at the pin).",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed — scale / precision declared on a progress field parse and are read by nothing at the write seam; their describes name the types they bind on (precision: 'Not read on any other field type'), the designer (ObjectFieldInspector isNumeric = number / currency / percent) offers neither on progress, no example declares them. Dormant, zero pull.",
    "carrier: none · noted, not filed — objectui SliderField's undeclared-bound defaults (min ?? 0, max ?? 100) are narrower than the server; one degenerate shape, not measured in a browser: a progress field declaring min above 100 with no max would clamp into [min, 100] and emit 100, now refused. No field declares that shape. Dedupe words: SliderField default max, progress slider clamp, slider min above max.",
    "carrier: next checklist-author sweep · noted, not filed — docs/qa/platform-checklist/areas/records-forms.json item records-forms.field-type-constraints does not reach progress bounds (triage noted it); internal QA prose."
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20482 at af9e5101ac2b36a0c9028189d837fa7393231c0b

    domain:engine#1 · session_01N8TPEsoJxPsdSdNKGnNGEN (os-warren) · written 2026-09-28T17:22Z. Contract review of record: 5875022310 on PR #20482, at-tier, read-only, PASS on this head. It judged the ADR-0087 marker not-required (no-migration-prescription) honest in substance, not only by the detector: no authored key moves, and PR #20423's record 5868385995 accepted the same marker on the same arm.

    Checklist, verified against GitHub rather than the reports:

    • Form: draft, base main, first line Fixes #20386. That is the only closing keyword in the body. This card's claim 5873443045 names the branch and reads Clause-②: no (narrowing), the same line as the changeset and the PR body.
    • Scope: 6 files, +455 / −16:
    • Changeset: @objectstack/objectql minor with the BREAKING banner, a before → after line (201 stored → 400 VALIDATION_FAILED + max_value / min_value, nothing stored) and the one-line fix.
    • Governed surface: none (check-governed-merges --pr 20482: not governed). 471 changed lines, under the human-merge threshold.
    • CI: 41 check-runs on the head. 36 success, 4 path- or event-skipped, none red. One Check Changeset re-run, fired by the seat's body edit, is in progress; the push's own Check Changeset on this head is success. mergeable_state: clean.
    • Seat edit to the PR body before landing: the review found one citation FALSE. The body said the claim asked for "a FROM → TO line"; the instruction was the seat's dispatch order. The sentence now names the dispatch order. No other text moved.
    • Behaviour: a progress write outside a declared min / max answers VALIDATION_FAILED / 400 with the number field's own codes, on insert, update, batch, updateMany and validate. In-bound values and both inclusive bounds are stored unchanged. scale / precision stay unread on progress, pinned from both sides.

    Out-of-scope notes, one line each (the review answered each; carrier none):

    Landing: ready plus auto-merge through the queue. The merge closes this card (Fixes), and the seat verifies it on main and removes pm:dispatched in the same act.

  5. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing record: PR #20482 merged. This card is closed completed by its Fixes line

    domain:engine#1 · session_01N8TPEsoJxPsdSdNKGnNGEN (os-warren) · written 2026-09-28T17:46Z.

    Verified on main:

    • The squash is 9801da1234761c14eb88663635792723e41d359f, a queue merge with one parent. It is an ancestor of origin/main, and the origin/main tip is the squash itself.
    • It carries 6 files, +455 / −16, the accepted head's list.
    • The squash's changed lines are identical to the accepted head af9e5101a's changes against its merge base 75b216924 (the same md5 over every added and removed line).
    • progress's early return now sits below the min / max checks in record-validator.ts at the squash. The 20386-* changeset is present at the squash and absent at its parent.
    • The PR body's one closing keyword is Fixes #20386, so no other card was closed. The body carries the seat's one-sentence correction, made before landing.

    Delivered: a progress write outside a declared min / max answers VALIDATION_FAILED / 400 with the number field's codes (max_value / min_value), on insert, update, batch, updateMany and validate. In-bound values and both inclusive bounds are stored unchanged. scale / precision stay unread on progress.

    • @objectstack/objectql ships minor BREAKING, Clause-②: no (narrowing), ADR-0087 not-required (no-migration-prescription).
    • ACCEPT is 5875063881. The contract review of record is 5875022310 (PASS).

    Carried out of this card (Acceptance notes, carrier none): scale / precision declared on a progress field parse and are read by nothing; objectui SliderField's undeclared-bound defaults; the checklist item records-forms.field-type-constraints never reaches progress bounds.

    pm:dispatched is removed in the same act as this record. The domain, area and type labels stay.

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, searchbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions