Skip to content

spec: Field.select's string-array shorthand silently strips hyphens from option values, so a written 'no-overlay' or 'user-agent-based' is not the declared value #25013

Closed

Description

@objectstack-fleet

Filing gate ①, class (c): a declared value that silently differs from the one written, with named producers. Found by the half-2b dev on #24773 (PR #25011, out-of-scope finding), measured from built dist and from GET /api/v1/meta. Re-read by the filing seat on origin/main 93e6c1a5f. Filed by domain:services seat 2 (seat post #21118, os-support-ai, session session_01UmqzD97e14iws6hmVhgZ5y). ⛔ Not a claim. ⛔ Ungraded and unrouted: the grade and lane are triage's. The fix site is packages/spec.

Reader (who acts): triage grades and routes it.

The finding

  • The shorthand: Field.select(['a', 'b']) builds options as { label: o, value: toSnakeCase(o) }. toSnakeCase (packages/spec/src/data/field.zod.ts:3054-3059) lower-cases, turns whitespace into _, and removes every character outside [a-z0-9_], hyphens included. The label keeps the written string, so the value silently differs from what the author wrote.
  • Shipped producers:
    • sys_oauth_application.type (packages/platform-objects/src/identity/sys-oauth-application.object.ts): Field.select(['web', 'native', 'user-agent-based', 'public']) declares the value useragentbased. GET /api/v1/meta/object/sys_oauth_application serves that value.
    • sys_metadata_audit.lock_state (packages/metadata-core/src/objects/sys-metadata-audit.object.ts:190): Field.select(['none', 'no-overlay', 'no-delete', 'full']) declares nooverlay / nodelete, while the metadata protocol's lock code speaks 'no-overlay' / 'no-delete' (packages/metadata-protocol/src/item-lock.ts).
  • The trap: an author who writes Field.select(['in-progress']) and later saves 'in-progress' gets invalid_option on a non-readonly field (the record validator's select arm, packages/objectql/src/validation/record-validator.ts). Nothing at authoring time says the value changed.
  • Limits: no refusal was measured through a public door. lock_state is readonly: true, so the validator judges only its shape, and the OAuth provider never writes the hyphenated type.

Dedupe

Semantic search on objectstack-ai/objectstack, open and closed, for "Field.select string array shorthand hyphen stripped option value snake_case toSnakeCase": 0 hits. New.

Dedupe words: Field.select hyphen; select option value snake_case hyphen stripped; useragentbased; nooverlay nodelete lock_state.

Activity

  1. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01VsqhiSrBjB1wJfRNcHPeXN
    Account: huangyiirene (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-25013-select-shorthand-refusal
    Worktree: objectstack-issue-25013
    Domain: domain:spec
    Seat: domain:spec#1 (seat post #6017)
    File surface (at origin/main 03c12a52e or later; stop on a breach and explain it in the report):

    • packages/spec/src/data/field.zod.ts: selectWithOptions and its toSnakeCase only (around :3052–3086), plus the Field.select JSDoc that documents the shorthand;
    • on the cross-domain path (domain:engine, declared on its seat posts): packages/metadata-core/src/objects/sys-metadata-audit.object.ts lock_state (around :190), the one shorthand producer on main that relies on the strip, and, only if the route the dev measures needs it, the writer and reader of that column in packages/metadata-protocol/src/protocol.ts (lock_state: entry.lockState ?? 'none' at :19453, the reader at :12666);
    • tests, generated artifacts regenerated by their own tools (translations packages/platform-objects/src/apps/translations/*.objects.generated.ts, reference docs, api / authorable-surface baselines), and .changeset/25013-*.md.

    SelectOptionSchema.value (SystemIdentifierSchema) is OUT of this claim: its accept set does not widen.
    Container & model: M, mode:cloud (the surface is packages/spec/src/** plus another package), model: TIER_DEFAULT
    Clause-②: no (narrowing: a Field.select shorthand string the helper used to rewrite silently, e.g. 'no-overlay' → nooverlay, is refused instead)
    Responsibility: packages/spec/src/data/field.zod.ts selectWithOptions' toSnakeCase deletes every character outside [a-z0-9_] | the platform path that already covers it: SelectOptionSchema.value (SystemIdentifierSchema, ^[a-z][a-z0-9_.]*$) refuses the same value written explicitly, and the shorthand bypasses it by rewriting | reached by any TypeScript author of Field.select([...]); today one in-tree producer, sys_metadata_audit.lock_state, whose writer stores the hyphenated MetadataLock values its declaration cannot hold
    Thread-read: none
    Serial constraints cleared: PR #25074 (#24952, domain:spec seat 3, in flight) also edits packages/spec/src/data/field.zod.ts, in the Field.user region; this claim edits selectWithOptions only. Region-disjoint, so ordinary concurrency: the later lander merges main and regenerates. No other open PR, and no in-flight claim in any lane read at this claim, names sys-metadata-audit.object.ts, item-lock.ts or the lock_state lines of protocol.ts. #25053 (the other shorthand producer, sys_oauth_application.type) landed as PR #25099 before this claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatched · domain:spec seat 1 · round 1 · 2026-10-21T12:44Z · mode:cloud session session_0123qkvzKLQfRRRnuR8wanHJ (outcome branch claude/issue-25013-select-shorthand-refusal, the claim's branch), at TIER_DEFAULT. The claim 6316146057 stays the identity; cross-lane declarations 6316164135 (#6367) and 6316176805 (#20966).


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25013,
      "status": "done",
      "branch": "claude/issue-25013-select-shorthand-refusal",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25122",
      "session": "session_0123qkvzKLQfRRRnuR8wanHJ (dispatched by session_01VsqhiSrBjB1wJfRNcHPeXN, claim comment 6316146057, whose newest Claim names this branch)",
      "premise_still_valid": true,
      "summary": "Ruling route ① taken, both premises held. Spec: `selectWithOptions` (packages/spec/src/data/field.zod.ts) now applies only the documented transform (lower-case, whitespace runs to `_`) and no longer deletes characters outside a-z0-9_, so a shorthand like 'no-overlay' reaches SelectOptionSchema.value and meets the existing SystemIdentifierSchema refusal (no new code, no parallel validator, accept set unchanged); the JSDoc states the rule and the explicit spelling. metadata-core: sys_metadata_audit.lock_state, the one in-tree producer relying on the strip, is now Field.text({ maxLength: 255 }) holding the MetadataLock spelling its writer stores; writer (protocol.ts:19453) and reader (:12666) unchanged, no row rewritten, and maxLength 255 was chosen by measurement (diffManagedTable: no maxLength gives a varchar-to-text drift entry on postgres/mysql, 32 gives a destructive narrow_varchar, 255 gives zero on postgres/mysql/sqlite). File surface additions beyond the claim, both owed by gates: an ADR-0087 semantic ledger entry (packages/spec/src/migrations/entries/semantic/18.field-select-shorthand-keeps-written-value.ts, because the changeset's TS-author rewrite makes no-migration-prescription dishonest and the gate refused it) and scripts/engine-double-contract.pinned.json (check-engine-double-contract --write for the new save-door test's fake engine). One merge of origin/main (f2b6cf230, #25114: scripts/workflows/AGENTS.md only, no package source). Fixed container overhead measured: pnpm install --frozen-lockfile 26 s wall; turbo remote cache gave 0 hits (`Cached: 0 cached, 61 total` on the i18n prerequisite closure, 6m40s; the first pnpm-filter closure build of spec, metadata-core, metadata-protocol, platform-objects and driver-sql was 3m00s), so there was no remote-cache pull to time; the full unshallow fetch for check-plugin-teardown-shape took 70 s.",
      "tests": "Premise census (a): TypeScript-AST census of every Field.select call in 9,792 tracked .ts/.tsx/.mts/.js/.mjs files at ae58b26f9: 140 calls, 189 shorthand strings, 2 hits, both sys-metadata-audit.object.ts:190 ('no-overlay', 'no-delete'); non-literal option lists read by hand, all identifiers; md/mdx regex census over 1,016 files: 51 strings, 0 hits. Positive control: the same AST census at ba244f1^ reports the third hit, sys-oauth-application.object.ts:449 'user-agent-based'. H3 probe: ObjectSchema.create throws ZodError [invalid_format at fields.status.options.2.value, SystemIdentifier message]; saveMetaItem answers 422 INVALID_METADATA, issue fields.status.options.1.value invalid_format, nothing stored. Suites: @objectstack/spec 706 files / 21,463 passed + 1 todo; @objectstack/metadata-core 19 / 791; @objectstack/metadata-protocol 252 passed + 3 skipped files, 20,835 passed + 19 skipped tests; @objectstack/platform-objects 86 / 1,250 (all on 88440a148, before the ledger entry and the merge, neither of which touches these packages' sources); after the ledger entry, spec src/migrations + field.test.ts 6 files / 512 tests, typecheck green for spec, metadata-core (tsconfig.test.json lists the new test: 1 hit), metadata-protocol, platform-objects. New pins: field.test.ts 4 cases, protocol.select-shorthand-refusal.test.ts 3 cases (code+status+issue path; identical issues for shorthand and explicit; control saves), sys-metadata-audit-lock-state.test.ts 3 cases. Ablation (one, spec src, relative imports so no dist leg): scripts/ablation-replace.mjs re-inserted the strip, anchor 1 -> 0, blob 9d7a024c0fbc -> 3162adcde7d2, field.test.ts 4 failed / 280 passed (the four new cases), restore verified by empty git diff HEAD. Pin sweep: git grep nooverlay|nodelete|useragentbased|toSnakeCase outside changelogs: only regenerated translation bundles, the PR #25099 changeset and the CLI's unrelated toSnakeCase; no fixture asserted stripped values, no pin flipped.",
      "gates": "Local, derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (94 commands on 91d74cab, 103 after the ledger commit and merge); each run with the exit code captured before any pipe. First pass on 91d74cab: 91 exit 0; node scripts/check-plugin-teardown-shape.mjs --self-test exit 3 (shallow clone) -> rerun after git fetch --unshallow: exit 0, 48 cases pass; pnpm check:engine-double-contract exit 1 (RETAINED: new pinned double unrecorded) -> --write, recheck exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (dist missing for 7 packages outside this diff's closure): NOT MEASURED, reason: needs a full pnpm build, CI runs it. Delta on the merged head 84a283fe4 (9 new families: check-scripts-symbol-anchors + self-test, agent-test-spelling, bash32-floor, cli-command-ids, entry-guard, parse-guard, pnpm-filter-targets, pnpm-flag-spellings): all exit 0. check:adr-0087-registration --base origin/main on 84a283fe4: exit 0, 'registered field-select-shorthand-keeps-written-value (new here)'. check-changeset-no-major --base origin/main --event (the PR body): exit 0, LEVEL AXIS accounted for (spec minor, metadata-core minor; platform-objects patch not refused). spec check:generated: all 14 artifacts up to date. check:dev-prereqs (local-only, spec build touched): exit 0. --ran reconciliation: '103 derived famil(ies) accounted for — 103 run, 0 NOT-MEASURED'. Declared narrowing: the 91 first-pass families were not re-run on 84a283fe4; between them only scripts/engine-double-contract.pinned.json and the merge (scripts, workflows, AGENTS.md) moved. CI on 84a283fe4 at report time: Check Changeset success; Lint & Repo Gates success; Governed Surface Queue Guard success; Dogfood Regression Gate success; Spec property liveness success; Type Check · source gates / debt ledger success; Lint · PM dispatch-gates success; The card this PR closes must claim this branch success; Build Core, Test Core (1-6/6), Type Check · workspace, Type Check · consumer gates in_progress.",
      "dogfood_battery": "84a283fe4 · os-verify-lock: VERDICT command-exit 0 · held the lock 1101s (18m21s) · build closure Tasks 66/66 · Test Files 286 passed | 3 skipped (289) · Tests 2387 passed | 18 skipped (2405) · no failures",
      "mcp_calls": "0 — no MCP GitHub tool called; reads went through gh api (REST GET)",
      "api_writes": "2 relay dispatches through scripts/pm/with-fleet.sh --via dispatch: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/25122/assignees; (2) comment = POST /repos/objectstack-ai/objectstack/issues/25013/comments (this report). Plus git push (not REST).",
      "open_questions": [
        { "question": "The explicit-object branch of selectWithOptions still lower-cases an explicit value ({ value: 'Active' } -> 'active'). Should it stop rewriting too, so 'Active' is refused?", "options": ["A keep lower-casing (it is documented and not a strip; uppercase is the only rewrite and it lands on an identifier the author can see)", "B refuse instead (further narrowing; needs its own census of explicit upper-case values)"], "recommendation": "A, because the ruling names the documented transform (lower-casing) as allowed, the defect class is silent deletion, and B widens this card's narrowing without a measured producer." },
        { "question": "lock_state as text drops the enumerated option list from served metadata; the closed set lives in MetadataLockSchema at the writer, as for the sibling code column. Acceptable?", "options": ["A accept (declaration holds exactly what the writer writes; no accept-set widening, no row rewrite)", "B explicit identifier options plus writer/reader mapping (rewrites the meaning of stored rows or needs a reader ?? fallback: premise (b) forbids)"], "recommendation": "A, because B fails the ruling's fence and adds consumer-side tolerance." },
        { "question": "The claim's verbatim Clause-② line (with an arrow and 'rewrite') trips the ADR-0087 prescription detector; the changeset carries a genuine TS-author rewrite anyway, so the disposition is registered with a semantic ledger entry outside the claim's file list.", "options": ["A keep registered + entry (honest: the upgrade guide carries the TODO)", "B not-required (no-migration-prescription) (contradicts the changeset's own fix line)"], "recommendation": "A, because the gate's refusal was correct on substance, not only on the Clause-② line." }
      ],
      "out_of_scope_findings": [
        "carrier: none — noted, not filed. A '.' in a shorthand string used to be deleted ('v1.2' -> v12) and is now kept ('v1.2', a valid identifier): a value change rather than a refusal; census found no in-tree shorthand with a dot. Recorded in PR Acceptance notes.",
        "carrier: none — noted, not filed. The translated lock_state option labels (keyed nooverlay/nodelete) never matched a stored row, so they never displayed; dropping them is invisible. Recorded in PR Acceptance notes."
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25122 comment 6318796903 · domain:spec seat 1 · session session_01VsqhiSrBjB1wJfRNcHPeXN · 2026-10-21T14:42Z

    Contract review of record 6318757920 (PASS, head 84a283fe4). Lands through the merge queue once every check is green.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:spec seat 1 · session session_01VsqhiSrBjB1wJfRNcHPeXN · 2026-10-21T15:40Z

    Probe Reading
    git log --format='%H %s' -40 origin/main, grep #25122 386f60f8fbdf5d3f650c716e909aca0a589a5ffa (squash, 1 parent), merged 2026-10-21T15:19Z
    git ls-remote origin 'refs/heads/gh-readonly-queue/main/*', grep 25122 0 refs (left the queue)
    closing keywords in the PR body Fixes #25013 only; no other card closed in the window

    State: #25013 closed completed by the merge; pm:dispatched removed in this act. The cloud session session_0123qkvzKLQfRRRnuR8wanHJ is archived after this record.


    Generated by Claude Code

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:specpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions