Skip to content

fix(objectql)!: enforce a progress field's declared min / max at the write seam (#20386) - #20482

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20386-progress-min-max
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20386-progress-min-max

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20386

Clause-②: no (narrowing)

A progress field's declared min / max are now enforced at the objectql write seam, per triage 5865053231 (ENFORCE, no decision card). A progress write outside a declared bound is refused with 400 VALIDATION_FAILED and the number field's own field codes, max_value / min_value. scale and precision stay unread on progress. Measured head: af9e5101a.

What changes

Measured premises (dispatch zone 2)

  • H1: progress bounds are skipped on origin/main. Held. Reproduced on dc0ab6a2e through the real RestServer POST /api/v1/data/:object handler, over a real ObjectQL engine on the SQLite SqlDriver and on the memory driver. This was a scratch harness, not committed, because check:driver-memory-census refuses a new driver-memory consumer.

    field value SQLite before memory before both after (5f5bc7580)
    progress, max: 100 150 201, stored 150 (real) 201, stored 150 400 VALIDATION_FAILED / max_value { max: 100 }, no row
    progress, min: 0 -5 201, stored -5 (real) 201, stored -5 400 VALIDATION_FAILED / min_value { min: 0 }, no row
    number, max: 100 (control) 150 400 / max_value, no row the same unchanged
    number, min: 0 (control) -5 400 / min_value, no row the same unchanged
    progress in bounds / on each bound 50, 0, 100 201 201 201, stored unchanged
  • H2: scale / precision after the move. Held, with a measured boundary. With the return placed below the bounds, scale and precision are still not enforced on progress: 33.5 under scale: 0 gets 201, and 99.5 under precision: 2 gets 201, on SQLite and memory. Deleting the return outright would start both. Measured by ablation (below): four pins turn red with max_scale / max_precision. So the placement is load-bearing, and it is pinned from both sides.

  • H3: producers. Zero writes outside a declared bound.

    • objectui SliderField, the progress editor (FieldEditWidget.tsx:87 progress: SliderField), is byte-identical at the pinned .objectui-sha dd3f7e1be3 and at objectui HEAD b8e0941. It passes min = field.min ?? 0 and max = field.max ?? 100 to @radix-ui/react-slider (^1.4.7). That component's updateValues does clamp(snapToStep, [min, max]) before every onValueChange (read in the 1.4.7 tarball). So it cannot emit a value outside a declared bound.
    • Example apps, on dc0ab6a2e: 2 progress fields declare bounds, showcase_task.progress and the field zoo's f_progress, both min: 0, max: 100. Their writers are 12 seed rows, the showcase_mark_done action (progress: 100) and the dogfood field-zoo matrix (60). All of them are in bounds.

Pins

  • packages/objectql/src/validation/record-validator.progress-bounds.test.ts (new, 14 tests):
    • the triage pins (150 gets max_value { max: 100 }, -5 gets min_value { min: 0 }, and in-bounds values plus both inclusive bounds are accepted);
    • envelope equality with the number refusal;
    • one bound declared alone, and no invented 0..100 bound when none is declared;
    • update mode, string-carried values, and an omitted field that is never re-read;
    • ⛔ scale / precision unread on progress, while the same declaration on slider refuses;
    • every engine write door through a stub driver: insert of one row and of an array, insertMany partial success, update by id and by predicate, the validate dry run, and a control showing that in-bounds values arrive as the same number.
  • packages/rest/src/rest-data-progress-bounds.test.ts (new, 6 tests), on the real RestServer routes over SQLite, reading the physical column past every read coercion:
    • POST, batch create, PATCH, batch update and updateMany refuse 150 / -5 with the number field's envelope and write or change nothing;
    • controls: in-bounds values and both bounds are stored unchanged, and 33.5 writes under scale: 0, precision: 2.

Verification

Heavy runs went through scripts/pm/os-verify-lock.sh. All readings are at af9e5101a unless stated otherwise.

  • Build. pnpm turbo run build --filter='@objectstack/rest...' --concurrency=2: 25/25, VERDICT command-exit 0. It was re-run after each merge of main (last at 5c4148234). The objectql source has not changed since.
  • objectql.
    • Validator and door suites (progress-bounds, blank-typed-value, precision, number-value, record-validator, engine-number-value-door, engine-blank-typed-value-door): 7 files, 333/333.
    • Full --project local: 328 files, 6081/6081 (at 6a029a923, before the second merge, which brought only spec and driver-sql commits).
    • typecheck (tsc, scripts, and check:test-typecheck, whose tsconfig.test.json includes src/**/*): exit 0.
  • rest. rest-data-progress-bounds, rest-data-number-value, rest-data-blank-typed-value and import-integration: 4 files, 100/100. typecheck including check:test-typecheck: exit 0.
  • Reverse verification.
    • REST pin, with a build. Against the dist/ built from base dc0ab6a2e, the REST pin read 5 failed / 1 passed. Each failure was expected 201 to be 400 or a batch row reporting success. The one green is the in-bounds control. The dist/index.js number arm was read directly before and after: the return sat above the bounds, then below max. After pnpm --filter @objectstack/objectql build at 5f5bc7580 the pin reads 6/6.
    • Ablation 1, fix committed first (3b7b55406). scripts/ablation-replace.mjs re-planted if (t === 'progress') return null; above the bounds: anchor 1 → 0, blob 9ede5b5b → d396c53a. Result: the new objectql file reads 10 failed / 4 passed. The 4 greens are the controls: in-bounds values, scale unread, precision unread, and the engine in-bounds control. Restored: blob 9ede5b5b equals HEAD, and git diff HEAD is empty. The subject resolves through a relative import to src/, so no rebuild was involved. Direction: red.
    • Ablation 2, the H2 reading. The same tool deleted the return: blob 9ede5b5b → d683b83e. 4 failed: the two progress-bounds pins for scale / precision, the blank-typed-value scale pin, and the precision test that excludes progress. Restored the same way. Direction: red.
  • Gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at af9e5101a derived 64 commands. 62 exit 0. 2 are NOT MEASURED (exit 3, PREREQUISITE NOT MET): check:dual-build-cjs-loads and check:type-check-debt both need a whole-repo build. --ran reconciliation: 64 derived, 62 run, 2 NOT-MEASURED (derived from exit 3), 0 UNRUN, exit 0.
    • First pass at 5c4148234: check:error-code-casing exit 1 on four bare { code: 'max_value' } style assertions. They are now field-addressed (af9e5101a), and the gate reads 0.
  • Lint, a declared narrowing. eslint --no-inline-config --format json on the 5 changed .ts files: 5 files, 0 errors, 0 warnings.
    • Population: --print-config applies the config's rules to each file (6 rules on the validator, 5 on the REST test).
    • Invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project), so this diff cannot move any untouched file's verdict.
    • The repo-wide pnpm lint is declared to CI.
  • Not run locally, declared to CI: the rest of the rest suite, runtime and dogfood, and the 6 path-matched families that take a value from the workflow.

ADR-0087 disposition: which precedent, and why

not-required (no-migration-prescription), following PR #20423, not #7501:

One conflict with the dispatch order's wording is worth stating. The seat's dispatch order asks for "a FROM → TO line" (claim 5873443045 itself names only .changeset/20386-*.md; seat edit after review 5875022310). Measured with the gate's own exported findMigrationPrescription: a line opening with the literal FROM → TO label is read as a migration prescription (branch from-to-label), and that refuses no-migration-prescription. The only category left would then be registered, which would need a new ledger entry in packages/spec. That is out of scope for this card and wrong on the facts, since nothing authorable moves. So the changeset carries the mapping 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 detector reads that as no prescription, and check:adr-0087-registration passes.

Acceptance notes


Generated by Claude Code

…write seam (#20386)

The number arm returned for `progress` right after the finite check, above
the bounds, so a `progress` field declaring `max: 100` stored 150 and one
declaring `min: 0` stored -5 (201 on memory and SQLite) while a `number`
field refused both. `FieldSchema.min` / `max` declare the check with no type
exclusion; triage ruled ENFORCE.

The early return moves below the bounds and stays above `scale` and
`precision`, whose own contracts name type sets `progress` is not in, so
only the bounds start binding. Refusals carry the `number` field's codes,
`min_value` / `max_value`.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
…ite door on SQLite (#20386)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
…0386)

check:error-code-casing reads a bare `{ code: 'max_value' }` as an error.code
emission; naming the field makes it the field-addressed validator code it is.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 17 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8e028591857980ae69b9f9badb380dfa61367e62 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 038a4c54ed8bd2e37b5e76e43c14571c80da88c8 — the merge of head af9e5101ac2b36a0c9028189d837fa7393231c0b into base 8e028591857980ae69b9f9badb380dfa61367e62, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 038a4c54ed8bd2e37b5e76e43c14571c80da88c8 && git checkout 038a4c54ed8bd2e37b5e76e43c14571c80da88c8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8e028591857980ae69b9f9badb380dfa61367e62 af9e5101ac2b36a0c9028189d837fa7393231c0b && git checkout -B drift-repro 8e028591857980ae69b9f9badb380dfa61367e62 && git merge --no-ff af9e5101ac2b36a0c9028189d837fa7393231c0b

node scripts/docs-audit/affected-docs.mjs --json 8e028591857980ae69b9f9badb380dfa61367e62

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: af9e5101ac2b36a0c9028189d837fa7393231c0b
Local-runs: none

Read: card #20386 (body and all 3 comments: triage 5865053231, claim 5873443045, os-dev-report 5874793983); PR #20482 (body, 6-file list, the net diff against the merge-base 75b216924 of origin/main); the head's check-runs, twice (first read at dispatch, final read 2026-09-28T17:15:32Z, recorded in ③). Read-only git (git show / git diff / git grep at the head), REST GETs (the PR, the card, PR #20423 and its changeset at bb33240df, PR #20469's file list and its record-validator.ts hunk, objectui SliderField.tsx and FieldEditWidget.tsx at the pin dd3f7e1be3 by contents GET), and a blob-filtered sparse clone of the public radix-ui/primitives slider source into the scratchpad (a read, for one H3 sentence; nothing built, run or re-run). The dev's local-run numbers (turbo build, vitest counts, ablations, eslint, dispatch-gates --ran) are not re-run: the check-runs are the gate verdicts.

① Derived judgments

The one code change. The diff on packages/objectql/src/validation/record-validator.ts (blob 84ef8a2e7 → 9ede5b5be) moves exactly one statement, if (t === 'progress') return null;, from above the min / max checks to directly below max and above the scale branch, and rewrites the comment around it plus one header line (line 31). No other statement moves; min, max, scale and precision code is byte-identical for every other type. The door is NUMERIC_VALUE_TYPES ∖ COMPUTED_VALUE_TYPES = number / currency / percent / rating / slider / progress (field-value.zod.ts:64, :325).

  1. progress answers max_value / min_value with VALIDATION_FAILED / 400 exactly as number does, on insert, update and batch, and admits every in-bound value — RIGHT. The same two fail('min_value', { min }) / fail('max_value', { max }) lines now reach progress, so the code, constraint and four-locale template (validation-message.ts:90-91, 139-140, 178-179, 217-218) are the number field's by construction. Pinned in record-validator.progress-bounds.test.ts (14 tests, counted): 150 → max_value { max: 100 }, -5 → min_value { min: 0 }, envelope equality with number for both, update mode, string-carried '150' / '-5' / '50', one bound alone, no invented 0..100 when none is declared, an omitted field never re-read, and every engine door through a recording stub driver (insert of one row and an array, insertMany partial success, update by id and by predicate, validate, plus an in-bounds control arriving as the same number). rest-data-progress-bounds.test.ts (6 tests, counted) drives the real RestServer routes over SQLite: POST, batch create, PATCH, batch update, updateMany refuse both values with the number envelope and the physical column is read unchanged. In-bound admission: 50 / 0 / 100 / 33.5 and both inclusive bounds (the two comparisons against def.min / def.max are strict, so 0 and 100 pass) — RIGHT.
  2. scale / precision stay unread on progress; no other numeric type's checks move — RIGHT. The return now sits below max and above the scale branch, so progress never reaches max_scale / max_precision. Pinned from both sides: scale: 0 with 33.5 and precision: 2 with 99.5 pass on progress and refuse on slider with the same declaration; REST control p_whole (scale: 0, precision: 2) stores 33.5 and still refuses 150. The scale set (number / percent / rating / slider, currency excluded since [finding] FieldSchema.scale on a currency field is offered to authors by the field designer, ignored by every display face, and still enforced on writes — half-live in the direction that surprises #19629) and the precision set (number / currency / percent / rating / slider) are untouched.
  3. Header line and record write door: '' skips every type check, so a number, boolean, date, datetime or time column stores an empty string — normalise it to null at the door (seam from objectui#10813) #20308 docblock — TRUE. Line 31 now lists progress under min / max and says it takes neither scale nor precision: true of the door and of the return's placement. The new docblock's quotes of the contracts are exact against field.zod.ts:1245 (precision: "Enforced on writes of number, currency, percent, rating and slider fields … Not read on any other field type") and :1270 (scale: "Applies to number, percent, rating and slider fields, where it is enforced on writes"); the triage citation, the measured 201 / 150 / -5 facts and "shipped BREAKING" match the card. One imprecision carried over from the old record write door: '' skips every type check, so a number, boolean, date, datetime or time column stores an empty string — normalise it to null at the door (seam from objectui#10813) #20308 text, not false in substance: "scale and precision below keep the five types they always read" — scale has read four since [finding] FieldSchema.scale on a currency field is offered to authors by the field designer, ignored by every display face, and still enforced on writes — half-live in the direction that surprises #19629; the next sentence names each key's exact set, which is what a reader acts on. Noted, not a FAIL input.
  4. The two edited tests — RIGHT. record-validator.blank-typed-value.test.ts: the removed assertion looped progress and summary with max: 100, scale: 0 and 150.5 accepted — that was the old boundary (progress max unread), so it had to go. The replacement pins what stays true: summary (in COMPUTED_VALUE_TYPES, subtracted from the door) still accepts 150.5, and progress with max: 100, scale: 0 accepts 50.5 — which pins scale unread on progress while staying inside the bound. record-validator.precision.test.ts:190: only the it name changed ("bounds the numeric branch never reads" → "which takes only min / max (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)"); the assertion (precision: 1 on progress, 50 accepted) is unchanged and remains true.

Accept-set and public-surface census. The only accept-set change is the narrowing on progress values under an already-declared min / max. No export, type, registration, enum member, message key, spec schema or stored-metadata shape changes; packages/spec is untouched; packages/rest gains a test file only. No widening anywhere in the diff.

Sentence audit — changeset. Every sentence TRUE: the launch-window minor reading (check-changeset-no-major.mjs header: "During the launch window we ship breaking changes as minor"); min / max keep key, type (z.number().optional(), field.zod.ts:1271-1272) and legality; the old return sat right after the finite check; the 201 / stored 150 / -5 facts are the card's table; the before → after line (201 stored → 400 VALIDATION_FAILED + max_value / min_value, nothing stored) is pinned on POST, batch, PATCH, updateMany and validate; "in all four locales" (en / zh-CN / ja-JP / es-ES templates exist); inclusive bounds; the WRITTEN-value-only clause (the { other: 1 } update pin); 33.5 under scale: 0 still writes; the affected census (below); the marker text (judged in ②).

Sentence audit — PR body. TRUE, with the following readings:

  • H3, objectui: SliderField.tsx and FieldEditWidget.tsx at the pin dd3f7e1be3 (the head's .objectui-sha) are byte-identical to objectui HEAD b8e0941 (diffed). FieldEditWidget.tsx:87 is progress: SliderField and it is the only progress edit widget (PercentCellRenderer at index.tsx:3488 is read-side). SliderField passes min = field.min ?? 0, max = field.max ?? 100 to @object-ui/components Slider, which wraps @radix-ui/react-slider at ^1.4.7 (packages/components/package.json:62). Radix slider.tsx at package version 1.4.7: updateValues computes clamp(snapToStep, [min, max]) before setValues / onValueChange — TRUE, so no emitted value leaves a declared bound.
  • H3, examples: at the head, exactly two progress fields declare bounds (task.object.ts:69 and field-zoo.object.ts:173, both min: 0, max: 100); no other example, template or platform object declares a progress field. Writers: 10 task seed rows (seed/index.ts:188-197: 100, 80, 45, 0, 0, 55, 90, 0, 0, 100) + 2 zoo rows (:341, :350: 80, 0) = 12; showcase_mark_done writes progress: 100 (ui/actions/index.ts:51); the dogfood matrix writes 60 (field-zoo.matrix.ts:123). Zero outside a bound — TRUE.
  • Acceptance notes: ObjectFieldInspector.tsx:277 isNumeric is number / currency / percent, and precision (:994-999) and offersScale (:293) hang under it, so the designer offers neither on progress — TRUE. No example progress field declares scale or precision — TRUE. The checklist item records-forms.field-type-constraints has no progress in its variants, fixtures or 10 steps — TRUE. PR fix(core,objectql)!: a date or datetime names a year from 0001 to 9999, refused at the comparand door and the write door (#20264) #20469's record-validator.ts hunks are line 55 (the date line), one import at line 85 and the date / datetime arm near line 1105; this PR's are lines 28-35 and 942-970 — no overlap, TRUE.
  • A number field's declared scale is never enforced — values with more decimals are accepted and stored verbatim (min/max on the same field are enforced) #7501's changeset (951476719, number-scale-enforced-by-rejection.md) carries no BREAKING banner and no adr-0087 marker — TRUE.
  • One sentence FALSE as a citation: "The claim asks for 'a FROM → TO line'" (ADR-0087 disposition section; repeated in the dev report's deviation 3 as "(claim 5873443045)"). Claim 5873443045 on the card names only ".changeset/20386-*.md" and carries no FROM → TO instruction; neither does the card body or the triage. The instruction, if it exists, is in the dispatch order, which is not an input here. The wording the dev chose is right either way (②); the fix is a one-line body edit (attribute it to the dispatch order, or drop the sentence). Not a FAIL input.
  • Local verification sentences (build counts, 333/333, 6081/6081, the two ablations, eslint, dispatch-gates --ran): not re-run, not judged; the head's own subject ("field-address the progress-bounds code assertions") matches the reported check:error-code-casing fix. The check-runs decide these families (③).

② Semver level

  • @objectstack/objectql minor, BREAKING banner, Clause-②: no (narrowing) — RIGHT. The diff narrows the write accept set of a published package (values outside a declared bound on progress, previously admitted) and widens nothing: no export, registration, enum member or spec shape moves. no is the correct Clause-② declaration (no published surface widens; check-widening-tells has nothing to read) and (narrowing) is the arm that carries the BREAKING reading, matching the claim line, the PR body and the precedent .changeset/10164-legacy-webhook-cleartext-refusal.md. minor is the launch-window level (check-changeset-no-major refuses major), so the breaking-ness rides on the banner and the marker, as the changeset says. No changeset for @objectstack/rest (test-only) — right. PR title fix(objectql)!: agrees.
  • ADR-0087 marker not-required (no-migration-prescription) — HONEST. Judged in substance, not only by the detector: a migration prescription is, in the gate's own words, "instructions for rewriting a consumer's code" — a FROM identifier and a TO identifier the author must perform to stay valid. This changeset has neither side: min and max keep their keys, type and legality on every field type, packages/spec is untouched, and no stored value is ever re-read (transition-gate class, pinned), so no author must do anything to remain valid. The "What a caller sees, before → after" line maps a server response (201, stored → 400 VALIDATION_FAILED, nothing stored) for a write that already contradicts the field's own declaration; both sides are response facts, not authored keys. The "fix" sentence (send an in-bound value, or widen or delete the bound) is caller-side remediation advice of exactly the shape PR feat(objectql,spec)!: enforce a field's declared precision (total digits) at the write seam — max_precision (#19992) #20423's changeset carries ("The fix is one of three. Write a value that fits. Raise precision… Or delete the key"), which the [finding] currencyConfig.precision is declared and validated against ISO 4217, but no renderer or runtime reads it — an ADR-0049 enforce-or-remove case, filed on ruling 乙 on #19910 #19992 record (5868385995) accepted under the same marker on the same arm with the reasoning "no FROM → TO mapping and no conversion is owed". Rewording away from a literal FROM → TO label was a change of label, not of substance: the literal label would have asserted a rename that does not exist, and FROM_TO_LABEL_RE + labelPositioned do read a line-opening FROM → TO as branch from-to-label (verified in check-adr-0087-registration.mjs:1319, :1693). The detector's arrow branches need a code-ish OPERAND on each side of the arrow (REWRITE_RE, :1124) under migration framing; the before → after line has bare words next to its arrow and no framing word, so it is read as no prescription — consistent with the substance. The other categories are closed on facts as the marker says. Check Changeset — the job that runs check-adr-0087-registration.mjs --base MERGE_BASE (pr-automation.yml:244, :940) — is success on the head.
  • Precedent choice: feat(objectql,spec)!: enforce a field's declared precision (total digits) at the write seam — max_precision (#19992) #20423 (same arm, same narrowing kind, gate-era marker) over A number field's declared scale is never enforced — values with more decimals are accepted and stored verbatim (min/max on the same field are enforced) #7501 (no banner, never asked for a marker) — right.

③ Boundary flags

Implemented-by: claude/issue-20386-progress-min-max
Reviewed-by: session_01N8TPEsoJxPsdSdNKGnNGEN

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 17:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 9801da1 Sep 28, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20386-progress-min-max branch September 28, 2026 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants