Uh oh!
There was an error while loading. Please reload this page.
fix(objectql,showcase): keep the state-machine refusal's facts with an authored message, and stop the New Project wizard offering statuses it refuses - #14517
Conversation
…t has an authored message `checkStateMachine` emitted the full field-error envelope only when the rule left its `message` empty; declaring one dropped `constraint` and `value`. The spec REQUIRES `message` on every rule, so the machine-readable half was reachable only via `message: ''` — a create form could not learn the legal `initialStates` without parsing the author's prose. The showcase's New Project wizard is the demo of that gap: it offered all five project statuses on create while the machine admits only `planned`, so four picks were dead ends answered three steps later — in English, because an authored message is emitted verbatim unless the bundle carries `objects.<o>._validations.<rule>.message` (#14253). - objectql: the authored branch carries `constraint` / `value` too. - showcase: drop `status` from the create wizard (the `default: true` option supplies the machine's own entry point), reword the rule message so it is honest for both refusal codes, and put it on the translation channel in `en` + `zh-CN`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tion channel The New Project wizard can also trip `end_after_start` and `spent_within_budget` from its budget/schedule step, so translating only the status rule would have moved the single English sentence one step later rather than removed it. The pin is scoped to the object's whole rule set for the same reason. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…zard-initial-status
📓 Docs Drift Check1 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
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8abe29c56edf5cd4f5bbd77c10ccacecf5c0b4a4 && git checkout 8abe29c56edf5cd4f5bbd77c10ccacecf5c0b4a4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a40c0f966ef71739fcaecf0c2e4d671d3e960d34 120d1d0662bfe290d8ffd86bc1b9f07881010007 && git checkout -B drift-repro a40c0f966ef71739fcaecf0c2e4d671d3e960d34 && git merge --no-ff 120d1d0662bfe290d8ffd86bc1b9f07881010007
node scripts/docs-audit/affected-docs.mjs --json a40c0f966ef71739fcaecf0c2e4d671d3e960d34 |
Uh oh!
There was an error while loading. Please reload this page.
…#14517 PR #14517 (the New Project wizard) landed on `main` after this branch measured, and it touched two of the showcase files these numbers are computed from. The collision is exactly on the family this branch introduces, so the number could not stand: `examples/app-showcase` re-derives 454 -> 450. The move is DOWNWARD and fully attributed. #14517 added a `_validations` block to the showcase bundle carrying zh-CN for four rule messages — `project_status_flow`, `project_health_progression`, `end_after_start`, `spent_within_budget` — and those are exactly the four findings that disappear: showcase `_validations` findings 11 -> 7 (-4, no key added) showcase bulkActions / datasets 18 / 32 (unchanged) pre-existing count 393 (unchanged) No new key appeared. `project_status_flow` already existed as a rule, so #14517's rewritten message text moved its content, not its address. Those four are the first real consumers of the key shape #14253 declared and this branch scaffolds: an author-written refusal that now reaches a zh-CN caller in Chinese, which the coverage gate can only see because the walk visits the family. Re-derived with `node scripts/check-i18n-coverage.mjs --update`, never by hand. Every other line is byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Fixes#14311
What the measurement said, and where it disagreed with the card
The triage ruling asked for two engine changes: make
checkStateMachine's refusalfield-located, and put its fallback message on the built-in validation catalog.
Measured against
origin/main, both were already true:checkStateMachinehas always returnedfield: rule.field, anderror-response.tsships
fieldsverbatim in the400 VALIDATION_FAILEDenvelope. The refusal wasfield-located already.
buildFieldErrorand the built-in catalog since Field validation messages are hardcoded English + API field name —penalty_amount must be ≥ 0reaches end users verbatim #3957,and that catalog already ships
invalid_initial_state/invalid_transitioninzh-CN,ja-JPandes-ES.The English sentence the dogfood saw was not a fallback. It was the showcase's own
message: 'Invalid project status transition.', and an authored message deliberatelyoutranks the catalog. #14253 already gave authored messages a translation address
(
objects.OBJECT._validations.RULE.message); the showcase simply never populated it.So the localisation half of this card is showcase content, not engine work.
That left one genuine engine defect, in the same code path and the same class:
Engine — an authored message cost the caller the whole envelope
checkStateMachine's emitter carriedconstraintandvalueonly on the built-inbranch. The moment a rule declared a
message, both were dropped. SinceValidationRuleSchemarequiresmessageon every rule, the machine-readable halfwas in practice reachable only by declaring
message: ''— so a normally-authored statemachine refused writes with no way for a client to learn which states are legal.
That is exactly what "a multi-step form can jump to the owning step" needs, and both keys
are already declared on
FieldValidationError(mirroringFieldErrorSchema), so nothingin the contract widens.
Envelope for the create this card describes, before and after:
packages/specis untouched. Which writes are refused does not change — Clause-② holds:the refusal stays a refusal and only gains the location the ledger already declared.
Showcase — the wizard, and the message channel
new-project-wizard.page.ts:statusis dropped from the create wizard. Themachine admits exactly one initial state (
planned), so "offer only the initialstates" degenerates to a one-option select — a question with one answer. The option
marked
default: truesupplies the value server-side, and it is the declared entrypoint, so the two cannot drift. Triage ruling 1 authorises this branch explicitly.
project.object.ts: the rule message is reworded. One authored sentence answersboth codes this rule raises, and the old wording described only the update half —
a create rejected for being born
activewas told about a "transition" it neverattempted.
translations/index.ts: all fourshowcase_projectrules go onto the i18n: three authored display surfaces have no bundle key at all (bulk-action defs, custom validation messages, dataset labels) #14253channel in
en+zh-CN.The product half of the card (a create form deriving allowed values from the object's
stateMachine) is a console feature and is deliberately not built here, per triageruling 3. The sticky-toast defect belongs to objectui and is not touched by this PR.
Declared scope note
Translating
end_after_startandspent_within_budgetgoes one step past the statusrule. It is the same defect class in the same file, and it is load-bearing rather than
tidying: this same wizard trips both from its budget/schedule step, so translating
only the status rule would have moved the single English sentence one step later instead
of removing it. The new pin is scoped to the object's whole rule set for that reason.
Verification (all on the final commit
120d1d066)pnpm --filter @objectstack/objectql exec vitest run src/validation src/engine.test.ts src/plugin.integration.test.ts src/engine-seed-required-deferral.test.tspnpm --filter @objectstack/example-showcase testpnpm --filter @objectstack/objectql typecheckcheck:test-typecheckverdict line)pnpm --filter @objectstack/example-showcase typecheckpnpm lint(whole repo,eslint . --no-inline-config)check:test-source-alias,check:cross-package-test-inputs,check:engine-double-contract,check:where-matcher,check:error-code-casing,check:nul-bytes,check:dispatcher-error-vocabularyGate families re-derived after the edits with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(40 families; it takes its own change set from the merge base). The new showcase test file
is confirmed inside the app's tsc program via
tsc --noEmit --listFiles(1 hit) — atypecheckthat excluded it would have said nothing about it.End-to-end, against a real running app
Real runtime on a scratch port, seeded admin,
Accept-Language: zh-CN:That is the deliverable's proof as triage worded it — creating with
planned, refusingactivewith a field-located, zh-CN message — and it exercises the real i18n serviceresolving the new bundle key, which the unit tests (which mock
translate) cannot.Reverse verification
The engine fix was committed first, then ablated: the two
constraint/valuespreadlines were deleted and the mutation confirmed on disk by marker count (1 -> 0) and by
git hash-objectdiffering from the HEAD blob.through
dist/. The mutateddist/was rebuilt before the run: 1 failed | 4 passed.Restore leg proved rather than assumed:
git checkout HEAD -- path(never a baregit checkout --, which restores from a possibly-polluted index), restored blob hashbyte-identical to the HEAD blob,
git diff HEADempty, whole-treegit status --porcelainclean, and
dist/rebuilt and re-verified withablation-dist-preflightfinding themarker present in 4 executable files. (The first preflight marker I chose was a source
spelling that only survives into sourcemaps; the tool caught it and the check was redone
with a marker that survives transpilation.)
Declared narrowing — verification ran UNLOCKED
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: nousable
flock. The shared verify lock is declared Linux-only (flockis util-linux, anda stock macOS does not ship it), so the commands were run through the wrapper but without
the lock — a declared narrowing, not a silent one. No serialization guarantee held for
these runs, nor for any sibling agent in this container while they ran. The wrapper's own
verdict line on every run:
UNLOCKED (declared) · no usable flock on this host, so the shared verify lock was NEVER taken and NOTHING was serialized.Generated by Claude Code