Repository navigation
fix(spec)!: the Field.select string shorthand keeps what it does not document transforming, so a hyphenated option is refused instead of declared as another value - #25122
Conversation
…document transforming, so a hyphenated option is refused rather than rewritten sys_metadata_audit.lock_state becomes a text column holding the MetadataLock spelling its writer stores. Claude-Session: https://claude.ai/code/session_0123qkvzKLQfRRRnuR8wanHJ Co-authored-by: Claude <noreply@anthropic.com>
…t declaration; regenerate translation bundles Claude-Session: https://claude.ai/code/session_0123qkvzKLQfRRRnuR8wanHJ Co-authored-by: Claude <noreply@anthropic.com>
…t column was created with Claude-Session: https://claude.ai/code/session_0123qkvzKLQfRRRnuR8wanHJ Co-authored-by: Claude <noreply@anthropic.com>
…declaration Claude-Session: https://claude.ai/code/session_0123qkvzKLQfRRRnuR8wanHJ Co-authored-by: Claude <noreply@anthropic.com>
…antic entry Claude-Session: https://claude.ai/code/session_0123qkvzKLQfRRRnuR8wanHJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123qkvzKLQfRRRnuR8wanHJ Co-authored-by: Claude <noreply@anthropic.com>
…lect-shorthand-refusal
📓 Docs Drift CheckThis PR changes 3 package(s): ⛔ 2 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 144 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 5bf6094fc142f20e373ba5aa0ec645152053a641 && git checkout 5bf6094fc142f20e373ba5aa0ec645152053a641
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f2b6cf230b4cddd795b854b044bbcd95e37b6f55 84a283fe4f22129c9ff20d2c259454391edda7f3 && git checkout -B drift-repro f2b6cf230b4cddd795b854b044bbcd95e37b6f55 && git merge --no-ff 84a283fe4f22129c9ff20d2c259454391edda7f3
node scripts/docs-audit/affected-docs.mjs --json f2b6cf230b4cddd795b854b044bbcd95e37b6f55
|
Contract reviewServed-tier: Inputs: card #25013 (body and all three comments: claim 6316146057, dispatch 6316248493, os-dev-report 6318297750); PR #25122 body, its 12-file list, and the net diff ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对Seat record of
Generated by Claude Code |
Fixes #25013
Clause-②: no (narrowing: a
Field.selectshorthand string the helper used to rewrite silently, e.g.'no-overlay'→nooverlay, is refused instead)Metadata-protocol: narrowed
What changed
packages/spec/src/data/field.zod.tsselectWithOptions: the string shorthand's transform is now only what its JSDoc documents: lower-case, and whitespace runs become_. It no longer deletes every other character outsidea-z0-9_. A shorthand string that is no system identifier ('no-overlay') therefore reachesSelectOptionSchema.valueunchanged and is refused by the rule an explicit{ value: 'no-overlay' }already met. No new error code, no parallel validator, andSelectOptionSchema.value/SystemIdentifierSchemaaccept and refuse exactly what they did before. The explicit-object branch keeps its lower-casing (documented, and not a strip). TheField.selectJSDoc states the rule and the explicit spelling to use.packages/metadata-coresys_metadata_audit.lock_state: the one in-tree shorthand the strip altered. It declarednone,nooverlay,nodelete,full, while its writer (metadata-protocolprotocol.ts,lock_state: entry.lockState ?? 'none') stores theMetadataLockspelling (no-overlay,no-delete). A select option value cannot hold a hyphen, so the column is nowField.text({ maxLength: 255 }), read-only as before, like the siblingcodecolumn whose closed set also lives with its writer. Writer and reader unchanged, no stored row rewritten.platform-objects, regenerated withnode scripts/check-i18n-bundles.mjs --write): the fourlock_stateoption labels drop out; the field label stays.field-select-shorthand-keeps-written-value(major 18), so the upgrade guide andos migrate metacarry the TODO for a TypeScript author whose shorthand now refuses. The changeset's disposition isregistered.scripts/engine-double-contract.pinned.jsonrecords the new save-door test's fake engine (check-engine-double-contract.mjs --write).Premises verified before writing code (ruling part ②)
lock_stateis the only shorthand the strip alters. A TypeScript-AST census of everyField.select(...)call in all 9,792 tracked.ts/.tsx/.mts/.js/.mjsfiles atae58b26f9(140 calls, 189 shorthand strings), comparing the old strip with the documented transform and with the identifier rule: 2 hits, bothsys-metadata-audit.object.ts:190(no-overlay,no-delete). The non-literal option lists it could not read statically (BUILTIN_MEMBERSHIP_ROLE_OPTIONS,InvitationStatus.options,SYS_ACTIVITY_BUILTIN_TYPES, and the.map(...)builders that write explicit values) were read by hand: all identifiers. A regex census of the 1,016 tracked.md/.mdxfiles (published skills, docs, templates; changelogs excluded): 51 shorthand strings, 0 hits. Positive control: the same AST census atba244f1^(before PR fix(platform-objects)!: sys_oauth_application.type declares only web and native, the two application types its writer produces #25099) reports the third hit,sys-oauth-application.object.ts:449'user-agent-based'.lock_statecan hold what its writer writes without widening the option-value accept set and without rewriting rows.Field.textholds the hyphenated spelling; no option value is involved. Schema drift was measured withdiffManagedTableon avarchar(255)column (the width the select column was created as) on postgres, mysql and sqlite:select0 entries;textwith nomaxLength1 entry on postgres and mysql (manual_widen_varchar_to_text,needs_confirm);textwithmaxLength: 32a destructivenarrow_varchar;textwithmaxLength: 2550 entries on all three. HencemaxLength: 255.Where the refusal fires (H3, measured)
ObjectSchema.create(the shape every platform object andos inittemplate uses) throws aZodErrorwith one issue,invalid_formatatfields.FIELD.options.N.value, messageSystem identifier must be lowercase, starting with a letter, …. Before thelock_statefix, importingsys-metadata-audit.object.tswould have thrown exactly this.saveMetaItemanswers422 INVALID_METADATAwith the issue atfields.status.options.1.value, codeinvalid_format, and stores nothing; the shorthand and the explicit spelling produce identicalissues.Tests
packages/spec/src/data/field.test.ts: the hyphen is kept; shorthand and explicit meet the same issue at the same path;ObjectSchema.createrefuses at module load (invalid_formatatfields.lock_state.options.1.value); the documented transform still parses (In Progress→in_progress, andv1.2now declaresv1.2).packages/metadata-protocol/src/protocol.select-shorthand-refusal.test.ts: the save door'scode+status+ issue path, the explicit spelling's identical issues, and a control that the documented transform saves.packages/metadata-core/src/objects/sys-metadata-audit-lock-state.test.ts:lock_stateis text,maxLength: 255, no options, read-only; exactlyno-overlayandno-deleteofMetadataLockSchemaare values a select option cannot declare; the object parses.scripts/ablation-replace.mjsre-inserted the strip intofield.zod.ts(anchor 1 → 0, blob9d7a024c0fbc→3162adcde7d2);field.test.tswent 4 failed / 280 passed, the four new cases; restored,git diff HEADempty.88440a148, before the ledger entry and merge; neither touches these packages' sources):@objectstack/spec706 files / 21,463 tests passed;@objectstack/metadata-core19 / 791;@objectstack/metadata-protocol252 passed + 3 skipped files / 20,835 passed + 19 skipped tests;@objectstack/platform-objects86 / 1,250. After the ledger entry: specsrc/migrations+field.test.ts6 files / 512 tests, andtypecheckgreen for spec, metadata-core, metadata-protocol and platform-objects.git grepfornooverlay,nodelete,useragentbasedandtoSnakeCaseoutside changelogs finds only the regenerated translation bundles, the earlier OAuth changeset and the CLI's own unrelatedtoSnakeCase. No fixture asserted the stripped values, so no pin flips.Acceptance notes
lock_stateoption labels (禁止叠加, …) never displayed: they were keyednooverlay/nodelete, which no stored row holds. Dropping them changes nothing a user saw; the column now shows the stored spelling.'v1.2'used to declarev12and now declaresv1.2, a valid identifier: a value change, not a refusal. The census found no in-tree shorthand with a dot.value({ value: 'Active' }→active). That is documented and not a strip, so it is left alone here; recorded in the report as an open question.Generated by Claude Code