Skip to content

field.searchable: true has no reader: $search skips the field and $searchFields naming it answers 400 "is not searchable" while the served schema says searchable: true #29008

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbX9avDwM2goWd7zimE
Ruled: 6573569376 · letter A · 2026-11-02T09:27Z

Filing gate: ① a product defect, a QA run clause fail (studio-authoring.no-entry-property-matrix acceptance[1]).
Business reason: field.searchable is a declared, defaulted spec key (field.zod.ts, default(false)) that authors set expecting the field to be found by list search. Today it is stored and served, and nothing reads it: the field is not searched, and naming it in $searchFields is refused with "is not searchable" while the same object's served schema says searchable: true. A user who searches for text in that field finds nothing, and the server contradicts its own schema.
Reader who acts: the triage seat, then the domain:server lane (search-field resolution) or the domain:spec lane if the key should be retired in favour of object.searchableFields (ADR-0049 enforce-or-remove). QA suggestion only: p2.
Duplicate search: searchable field → 122 hits; the near ones are closed and different: #28121 (resolveSearchFields fallback on unknown names), #28120 (empty searchFields read), #27705 / #27829 (secret fields in searchableFields). resolveSearchFields searchable → 12 hits, none about field.searchable. searchable:true field search → 1 hit (#13675, a skills sweep).
Batch-of: release-18.3.0-w4-c3b1 — 16 cards from QA run #29002 (subject 7fa9873f; the implementer re-checks the landing site on current main).

Reproduction

  1. On a writable package, publish object qar8_fsrch2 with fields snippet {type:'code', searchable:true} and body {type:'html', searchable:true} (no searchableFields on the object). The live read carries searchable: true on both.
  2. POST /api/v1/data/qar8_fsrch2 {name:'fs2 row', snippet:'select QQCODE from x', body:'(an html paragraph holding QQHTML)'}.
  3. GET /api/v1/data/qar8_fsrch2?$search=QQCODE and ?$search=QQHTML, then ?$search=QQCODE&$searchFields=snippet.

Expected: the row is found through its searchable fields. Actual: [], [], then 400 INVALID_FIELD "Field 'snippet' on object 'qar8_fsrch2' is not searchable. With no 'searchableFields' declared, 'search' scans the text-like columns … and 'snippet' is type 'code'". Opposite posture: on another object a text field with searchable absent is scanned exactly like one with true. Reproduced 3 times.

Mechanism (suspected)

packages/spec/src/data/search-fields.ts resolveSearchFields (declared searchableFields, else the type-based default set) and packages/metadata-protocol/src/protocol-record-reads.ts never read field.searchable; no ui/ reader exists either (plugin-list and the fields search offer read only the object's searchableFields).

QA-source: #29002 · studio-authoring.no-entry-property-matrix · acceptance[1]

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: studio-authoring.no-entry-property-matrix

    Triage: domain:spec · p2 · needs-user-decision · 2026-11-02T07:27Z

    ⛔ Not a claim.

    • Read fresh. resolveSearchFields in packages/spec/src/data/search-fields.ts never reads field.searchable, and packages/spec/liveness/field.json marks it live, so both routes (honour or retire) land in domain:spec. Honouring it would change which columns $search scans, including types like code that are excluded today, while retiring it in favour of object.searchableFields is an ADR-0049 removal; that is an unsettled contract choice, so needs-user-decision. It is a user-visible search defect on a P2 item, so p2.

    Generated by Claude Code

  2. zhuangjianguo commented on Nov 2, 2026

    @zhuangjianguo
    Collaborator

    director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw

    Brief-claimed-by: session_01QsR2jV5sBSzCYNEDgpYLJw · batch #453 · 2026-11-02T08:57Z

    Director seat (第 56 场). This card is presented to the maintainer as batch #453 item 5 in the 总监决裁 thread; the ruling comment follows the maintainer's answer. ⛔ Not a claim.


    Generated by Claude Code

  3. zhuangjianguo commented on Nov 2, 2026

    @zhuangjianguo
    Collaborator

    director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw

    Ruling: batch #453 item 5 · letter A · maintainer 「同意」 2026-11-02T09:22Z

    Director seat (第 56 场; brief-claimed above in 6573084852 and presented in the 总监决裁 thread as batch #453 item 5, recommendation A). The maintainer's word is 「同意」 for the whole batch, so this card is ruled A: field.searchable is honoured, not retired.

    What the ruling binds (A):

    • When an object declares searchableFields, the declaration stays the whole search set (unchanged). When it does not, the effective set is the type-based auto-default plus every field marked searchable: true, of any type, except the hard exclusions (secret, password, vector) which stay excluded. The $searchFields ingress allow-list is the same set, so naming a searchable: true field is accepted (resolveSearchFields and the REST 读路径:searchFields / groupBy / aggregations 指向不存在的字段时被静默降级(#4226 收口后剩下的三条轴) #4254 gate stay one source).
    • The key's .describe() says this sentence instead of "Is searchable"; the field reference pages are regenerated. Docs and examples that already write searchable: true need no change.
    • Option B (ADR-0049 retirement) is not taken.

    Workload of the letter: packages/spec/src/data/search-fields.ts resolution + describe + tests, then the REST ingress reading it; Clause-②: yes (widening) on the search accept set, CONTRACT_REVIEW_TIER (this seat).

    Execution: domain:spec first, domain:server for the ingress if it does not already read the resolved set; the lane seat moves the label. Card #29007 (titleFormat silent no-op) is presented separately in the next batch. The Ruled: body line is written by this act.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

    Out of the decision box · 2026-11-02T12:19Z

    ⛔ Not a claim. The lane seat (domain:spec#2, by the split with seat 1 in 6576333149) takes the exit stroke.

    • Ruling read this act: 6573569376 (director seat, batch Fix missing @objectstack/core dependency in examples/basic #453 item 5, letter A, the maintainer's 「同意」), the newest comment on this card; the body's Ruled: line names it. No later comment changes it.
    • Governing text: ADR-0049 (declared ⇒ enforced, the enforce-or-remove choice the ruling settles as enforce), and the packages/spec/src/data/search-fields.ts docblock (resolveSearchFields and the ingress gate stay one source).
    • Premise re-read on origin/main: resolveSearchFields (search-fields.ts) still resolves declared searchableFields, else the type-based default set, and never reads field.searchable; field.zod.ts still describes the key as 'Is searchable'.
    • Blocked-by: none on the card.
    • State: needs-user-decision → pm:queue in this stroke; the seat dispatches it next under its own claim.
  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

    Claim: PM loop round 1 · 2026-11-02T12:26Z
    Session: session_01ADHkmSduNGC2SZXszyrNut (seat); dev = card thread session_01ADHkmSR2TvJTmoYyVtcQv9
    Account: os-steve
    Branch: claude/project-thread-8ymp4z
    Worktree: the card thread's own checkout of objectstack-ai/objectstack
    Domain: domain:spec
    Seat: domain:spec#2
    File surface: packages/spec/src/data/search-fields.ts (resolution and docblock), the searchable describe in packages/spec/src/data/field.zod.ts, their tests, packages/spec/liveness/field.json if its note changes, regenerated references, root .changeset/; a one-line meta projection fix in a caller only if it strips searchable (else reported for domain:server) (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default — the tier is the PM's per-card call (floor sonnet · default opus · ceiling fable); Clause-② widening of the search accept set, one CONTRACT_REVIEW_TIER review by the seat before landing, effort: xhigh
    Clause-②: yes (widening)
    Metadata-protocol: semantic
    Responsibility: packages/spec search-fields.ts resolveSearchFields never reads field.searchable | none: the ruling settles enforce over retire | any author who sets searchable: true on a non-default type, and every $searchFields caller naming it
    Thread-read: 6576961902
    Premise: holds (on origin/main 12:19Z resolveSearchFields resolves declared searchableFields else the type-based default set, never field.searchable; field.zod.ts describes it 'Is searchable')
    Serial constraints cleared: none (no in-flight domain:spec claim names search-fields.ts or the searchable line of field.zod.ts; the dev re-reads open PR file lists before pr_create)

    Ruling-ref: 6573569376 (director record, batch #453 item 5, letter A, the maintainer's 「同意」); exit stroke 6576961902.

  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-steve · session_01ADHkmSR2TvJTmoYyVtcQv9
    os-dev-report

    {
      "issue": 29008,
      "status": "done",
      "branch": "claude/project-thread-8ymp4z",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29385",
      "session": "session_01ADHkmSR2TvJTmoYyVtcQv9",
      "premise_still_valid": true,
      "summary": "Ruling 6573569376 letter A is implemented in the one resolver every reader shares (packages/spec/src/data/search-fields.ts). When an object declares no searchableFields, the auto set is now the type-based default plus every field marked searchable: true, except the new SEARCH_FLAG_EXCLUDED_TYPES (secret, password, vector) and virtual formula fields. A declared set stays whole and reads no flag. The ingress allow-list, the engine expansion, the lint rule and the ui readers all move with it, because they pass the full field meta to the same resolver. The one projection that dropped the flag (lint indexObjectSearchTargets) now carries it, so no domain:server half was needed. The describe now says the ruling's sentence and the references are regenerated. The 400 and lint messages now offer searchable: true as the fix where they used to call the type unscannable.",
      "tests": "Every run went through os-verify-lock and read VERDICT command-exit 0. turbo build ./packages/** passed Tasks 78/78. @objectstack/spec test passed 791/791 files (23409 tests, 1 todo), and spec typecheck exited 0. @objectstack/lint test passed 189/189 files (7835 tests), and typecheck exited 0. @objectstack/metadata-protocol test passed 301 files with 4 skipped (22127 tests), and typecheck exited 0. The objectql search files passed 5/5 (331 tests). The rest search files passed 15/15 (425 tests, 22 skipped), and rest typecheck exited 0. The ui readers (searchOffer, RecordPickerDialog.searchlessTarget-12966, ListView.searchlessObject-12966, ListView.searchWithheldTreeChart-10326) passed 4/4 (39 tests). The new tests are spec search-fields.test.ts at 50/50, lint validate-searchable-fields.test.ts at 73/73, and rest data-search-field-searchable-flag-door.test.ts at 4/4. Ablation: the flag line in autoDefaultFields was replaced with a never-matching marker through ablation-replace --hold, then rebuilt, and dist-preflight found the marker in 2 spec dist files. Spec then failed 5 of 50, lint failed 1 of 73 and rest failed 1 of 4 (the card's three requests), and every control stayed green. The restore was byte-identical: blob d45c69a6 equals HEAD and git diff HEAD is empty. After a rebuild, --absent preflight found the marker absent from 588 built files and the tree clean. Forced test edit, named in the PR body: lint validate-searchable-fields.test.ts, the skill-parity lookup pin. Its old expectation 'which search cannot scan' became 'and not marked searchable: true'.",
      "gates": "At head 85ec0011, dispatch-gates --commands derived 106 commands. All 106 were run with stdin closed and all exited 0. --ran reconciliation: 106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN. One command first exited 3 as a prerequisite: check-plugin-teardown-shape --self-test could not read its pinned commit 621a4876 in the shallow clone. After a one-commit fetch it exited 0 with 48 cases. Offline check-changeset-no-major --event against the PR body exited 0, with the level axis reading widening and semantic. CI check-runs were not read: the PR had just opened, so they are in_progress.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "5 — mcp__github__issue_read get x1, issue_read get_comments x3, pull_request_read get x1 (read-back of #29385). No write tools.",
      "api_writes": "2 — relay pr_create (one repository_dispatch, which ran POST /repos/objectstack-ai/objectstack/pulls as objectstack-fleet[bot], read back with 11830 of 11830 bytes identical) and this report through post-stamped.mjs (POST issues/29008/comments via the relay). The branch pushes were git pushes, not REST.",
      "open_questions": [
        {
          "question": "ADR-0061 D2 still says field-level searchable stays pruned, and ruling 6573569376 reverses that. Where does the governed amendment note land?",
          "options": [
            "A: the seat files a separate ADR-note card on docs/adr (Tier H), and this PR stays ungoverned",
            "B: the seat adds the note to this PR, which makes it Tier H, so it stays draft until a GOVERNED_APPROVERS approval"
          ],
          "recommendation": "A, because the code change does not depend on the note, and B would hold a non-governed widening behind a governed approval. A draft of the note is in the seat's project files (spec-seat2/29008-adr-0061-note.md)."
        },
        {
          "question": "The flag is read before the auto-default's own exclusions, so a flagged hidden field or a flagged field with a system-column name joins the set. Is that intended?",
          "options": [
            "A (taken): admit them, which is the literal ruling ('every field marked searchable: true, of any type'), with tests pinning it and unflagged controls",
            "B: keep the hidden and system-name exclusions ahead of the flag, a one-line move of the flag check plus a test flip"
          ],
          "recommendation": "A, because it is what the ruling says and an author who writes the flag on a hidden field asked for it. B is cheap if the seat reads 'hidden' as a stronger signal."
        },
        {
          "question": "A flagged field whose type has no text column (number, boolean, date, datetime, time, json) now joins the set, and the engine emits $icontains for it. Should the flag be narrowed to string-capable types?",
          "options": [
            "A (taken): admit every flagged type except secret, password and vector, as ruled; the engine-side handling of a non-text column goes to domain:server (out_of_scope class a), and it covers the declared branch too, which already has this shape",
            "B: also exclude the text-operator-refused types from the flag (one line plus tests), which departs from the ruling's 'of any type'"
          ],
          "recommendation": "A, because the ruling is explicit and the same gap exists today for a declared searchableFields. The fix belongs once in the engine expansion, not in the flag."
        },
        {
          "question": "This adds the new public export SEARCH_FLAG_EXCLUDED_TYPES (@objectstack/spec/data), which lint and metadata-protocol import. Keep that name?",
          "options": [
            "A: keep it exported under this name, next to SEARCH_AUTO_EXCLUDED_TYPES",
            "B: rename it before landing",
            "C: keep it internal and duplicate the three types in the two consumers"
          ],
          "recommendation": "A, because one exported set keeps the ingress message, the lint verdict and the resolver from drifting. It is an export name, not a metadata key."
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: a · reach: $search over a non-text column (number/boolean/date/datetime/time/json), through a flagged field in the auto set or a declared searchableFields entry. The engine expansion emits $icontains per field after the #15661 text-operator door, so on Postgres it likely errors. NOT MEASURED (no Postgres in this container). Evidence: expandSearchToFilter in packages/objectql emits $icontains with no type check. Dedupe words: search icontains non-text column postgres · carrier: domain:server · noted, not filed",
        "class: b · reach: skills docs teach the auto-default as type-only. Locations: skills/objectstack-data/SKILL.md (Search Fields, about L114-118), skills/objectstack-query/SKILL.md (about L319-320), skills/objectstack-ui/rules/list-views.md (the allowed-set table, about L189). Tier H · carrier: seat (Skills-docs: follow-up owed in the PR body) · noted, not filed",
        "class: c · reach: the engine's empty-search-set refusal names only searchableFields as its remedy, not the flag. Wording only · carrier: the next PR touching packages/objectql search · noted, not filed"
      ],
      "files_changed": [
        ".changeset/29008-field-searchable-honoured.md",
        "content/docs/api/data-api.mdx",
        "content/docs/data-modeling/field-types.mdx",
        "content/docs/data-modeling/fields.mdx",
        "content/docs/data-modeling/queries.mdx",
        "content/docs/data-modeling/schema-design.mdx",
        "content/docs/protocol/objectql/query-syntax.mdx",
        "content/docs/references/data/field.mdx (regen)",
        "content/docs/references/data/object.mdx (regen)",
        "content/docs/references/system/migration.mdx (regen)",
        "packages/lint/src/rule-explanations.ts",
        "packages/lint/src/validate-searchable-fields.test.ts",
        "packages/lint/src/validate-searchable-fields.ts",
        "packages/metadata-protocol/src/protocol-query-validation.ts",
        "packages/rest/src/data-search-field-searchable-flag-door.test.ts (new)",
        "packages/spec/api-surface/data.json (regen)",
        "packages/spec/export-origins/data.json (regen)",
        "packages/spec/liveness/field.json",
        "packages/spec/src/data/field.zod.ts",
        "packages/spec/src/data/search-fields.test.ts",
        "packages/spec/src/data/search-fields.ts"
      ],
      "line_budget": "+557/-33 across 21 files. Source +139/-18: search-fields.ts +78/-6, field.zod.ts +15/-1, lint +28/-7, metadata-protocol +18/-4. Tests +379/-1. content/docs including regen +16/-12. Changeset +19. Liveness +2/-2. api-surface and export-origins +2. No skills/** or other governed path is touched.",
      "deviations": [
        "Files beyond the delta's expected surface: the lint verdict, hint and os explain text, the metadata-protocol ingress 400 message, a rest door test and six content/docs pages. These come under the os-dev 'made false must be fixed' clause, because each said a flagged field's type cannot be scanned, which this change makes false. The new message wording is mine, not dictated by the ruling.",
        "The changeset carries @objectstack/metadata-protocol and @objectstack/lint at minor besides @objectstack/spec, because their src moved and the level axis grades kind semantic as minor.",
        "check-dev-prereqs (the non-self-test run) is not green locally: ui/** dist was not built, since only ./packages/** were built. It is not in the derived gate list, and its --self-test is, which exited 0.",
        "The PR footer uses the os-dev.md session-URL form, not the harness reminder's form, because os-dev.md takes precedence. pr_create carried no assignees and no labels (a changeset exists), and no reviewer request was sent, per the relay rules.",
        "One gate (check-plugin-teardown-shape --self-test) needed a one-commit fetch of its pinned fixture commit before it could run. The first exit 3 is a prerequisite, not a verdict."
      ]
    }

    Stamped 2026-11-02T13:46Z.


    Generated by Claude Code

  7. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

    Landed: PR #29385, merged as 29c70cac at 2026-11-02T16:09Z.
    ACCEPT → PR #29385 comment 6580643825 · contract review PASS 6579041743 (85ec0011) and delta PASS 6580195493 (f60ceb0b) · name ruling A 6580271401.
    Follow-ups filed: #29391 (ADR-0061 D2 amendment), #29394 ($icontains over non-string columns, domain:server), #29395 (skills docs).

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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions