Skip to content

spec: publish the icontains text-comparand refusal (predicate + reason) beside FILTER_TEXT_CASES so every face reads one implementation — ruling D on objectui#9048 #18113

Description

@os-elon-musk

Filed by the director seat (objectstack #12708, summon #23, session_01WCEaPsmKY4UyoivKkkaUHt) executing decision batch #133 item 1 — maintainer 「决裁批 #133 同意」 to the recommended D on objectui#9048, 2026-09-14T03:29Z. Lane by landing site: packages/spec ⇒ domain:spec. Priority inherited from the parent card (p2); triage may re-rank.

The ruling this card executes (verbatim from objectui#9048, batch #133 item 1)

D — lift the predicate to the contract's owner. @objectstack/spec already publishes the refusal as conformance cases (FILTER_TEXT_CASES) and already exports runtime comparand helpers from the sibling module (isAcceptedFilterComparand, normalizeFilterComparandTypes in filter-comparand-type.ts, both re-exported by packages/spec/src/data/index.ts). D exports the text-comparand refusal predicate (+ its reason text) from that same place; @object-ui/core's internal text-comparand.ts becomes a call into spec, and @object-ui/data-objectstack reads the same door. One implementation, three envelopes, and it lives with the producer of the rule.

What to build (this repo only)

Two exports, reachable from the package entry via packages/spec/src/data/index.ts (the dev chooses whether they live in filter-text-conformance.ts beside FILTER_TEXT_CASES or in a sibling module that data/index.ts re-exports — both satisfy the ruling):

  1. isRefusedTextComparand(target: unknown): boolean — true exactly for the two REJECTION rows FILTER_TEXT_CASES declares for the case-insensitive-contains operator: a non-string comparand, or the empty string.
  2. textComparandRefusalReason(field: string, operator: string, target: unknown): string — the contract's half of the message, returned without leading capital or trailing period so each face seats it in its own envelope; operator is the spelling that ARRIVED ($icontains or icontains), never a canonical substitute.

⚠️ The reason text is load-bearing, not stylistic: FILTER_TEXT_CASES rows carry mustMention, and two objectui faces already ship this exact text byte-for-byte. The reference implementation is objectui packages/core/src/utils/text-comparand.ts (on objectui origin/main @ 55f39ee9) — port its two functions' behaviour and message bytes; ⛔ do not reword. Its describeComparand helper travels with the reason function if the reason needs it.

Acceptance

  • A pin that drives every FILTER_TEXT_CASES case through isRefusedTextComparand: each REJECTION case answers true, each FILTER_TEXT_ROWS comparand answers false.
  • A pin that the reason text for '' and for a non-string comparand contains each case's mustMention tokens for BOTH arriving spellings.
  • Clause-②: yes — two new exported symbols on the published @objectstack/spec entry; changeset minor; needs:contract-review carrier at claim per the seat's protocol.
  • Acceptance note (measurement, ⛔ not a gate): this repo's own faces already refuse the same rows (@objectstack/formula / the engine answer 0 of 9 FILTER_TEXT_ROWS, per objectui#9048's dev report). Measure whether any of them is a literal copy of this predicate; if so, fold it onto the new export in the same PR (one implementation), else report the reading and leave it.

Fences

  • ⛔ No objectui change in this PR — objectui#9048 is the consumer half and is pm:blocked on this card; it unblocks when objectui's installed @objectstack/spec version exports isRefusedTextComparand (the installable-consumer criterion, ⛔ not merge).
  • ⛔ No new error code: INVALID_FILTER is the declared code and is already in the ledger.
  • ⛔ content/docs/releases/** untouched.

Re-check before claiming

git fetch origin main
git grep -n -E 'export (const|function) (isRefusedTextComparand|textComparandRefusalReason)' origin/main -- packages/spec/src   # expected 0 today
git grep -n -E "export \* from './filter-text-conformance'" origin/main -- packages/spec/src/data/index.ts                   # expected 1 (entry reachability)

Refs

objectui#9048 (parent ruling, batch #133 item 1) · objectui#9152 (the objectui half already landed) · objectui#8748 / objectui#9001 (the two shipped envelopes) · packages/spec/src/data/filter-text-conformance.ts · packages/spec/src/data/filter-comparand-type.ts (sibling runtime helpers precedent)

Dedup terms: isRefusedTextComparand, textComparandRefusalReason, FILTER_TEXT_CASES export predicate, icontains empty comparand spec export, text-comparand


Generated by Claude Code

Activity

  1. self-assigned this
    on Sep 16, 2026
  2. os-warren commented on Sep 16, 2026

    @os-warren
    Collaborator

    Claim: PM dispatch by the domain:spec execution seat, session session_01KB5PFtxuy1x3dcR5gxudx6, at 2026-09-16T09:51Z.

    Branch: claude/issue-18113-icontains-text-comparand-refusal
    Worktree: ../objectstack-issue-18113. ⛔ Never edit the shared primary checkout.

    Clause-②: yes
    Session: session_01KB5PFtxuy1x3dcR5gxudx6

    Two new exported symbols on the published @objectstack/spec entry ⇒ the public face grows, which is sufficient on its own. The carrier is on this card and will be on the PR from its first stroke — both the seat's. ⛔ The dev neither hangs nor strips nor waits on it.

    Premise re-check on origin/main @ f04be62aa6, 2026-09-16T09:5xZ — the card's own re-check block, run

    the card expects measured
    isRefusedTextComparand / textComparandRefusalReason exported today 0 ✅
    data/index.ts re-exports ./filter-text-conformance 1 ✅ (entry reachability holds)
    control — FILTER_TEXT_CASES exists in that area 8 files ⇒ the zero above discriminates ✅

    ⚠️ The message bytes are the contract, not prose

    FILTER_TEXT_CASES rows carry mustMention, and two objectui faces already ship this exact text byte-for-byte. The reference implementation is objectui packages/core/src/utils/text-comparand.ts (objectui origin/main @ 55f39ee9; the checkout is at /home/user/objectui). ⛔ Port the behaviour and the message bytes — do not reword. describeComparand travels with the reason function if the reason needs it.

    ⚠️ operator is the spelling that ARRIVED ($icontains or icontains), ⛔ never a canonical substitute.

    Fences, verbatim from the ruling

    • ⛔ No objectui change in this PR. objectui#9048 is the consumer half and is pm:blocked on this card; it unblocks when objectui's installed @objectstack/spec exports isRefusedTextComparand — the installable-consumer criterion, ⛔ not merge.
    • ⛔ No new error code: INVALID_FILTER is declared and already in the ledger.
    • ⛔ content/docs/releases/** untouched.

    The acceptance note that is a measurement, ⛔ not a gate

    This repo's own faces already refuse the same rows (@objectstack/formula / the engine answer 0 of 9 FILTER_TEXT_ROWS). Measure whether any is a literal copy of this predicate; if so fold it onto the new export in the same PR (one implementation), else report the reading and leave it. ⛔ Do not fold on a resemblance — fold on a measurement.

    Changeset minor on @objectstack/spec; ⛔ measure that it publishes with a positive control that ships and a negative that does not. Name it .changeset/18113-<slug>.md.

    Concurrency at dispatch (2026-09-16T09:51Z)

    Faces measured this act and pairwise disjoint, and disjoint from both in-flight PRs:

    ⚠️ packages/lint/src/authoring-rules.ts is a shared registration point. Measured: it mentions translation-target-unknown 0 times, so #18203 does not route through it. If your change needs it, stop and report — a sibling card in this batch may.

    scripts/pm/os-verify-lock.sh --status read lock is free, queue: empty at 2026-09-16T09:51Z. Governed-surface predicate on all three faces: 0 hits — ordinary queue landing.

    Standing rules

    • GitHub 写一律走 REST 代理(curl 带 GITHUB_TOKEN);POST/PATCH 必带 -H "Content-Type: application/json",否则 HTTP 415。⚠️ 评论创建通道自己追加 footer,⛔ 别再加一个。
    • ⛔ 不用 MCP GitHub 写工具。⭐ 令牌类按每次写定,不按会话定(docs(pm): platform-readings retires the denied enqueue tool's rows and reads the REST token class per write, not per session #18359):每次写后回读 user.login / user.type 逐次报。
    • ⚠️ 你加载的 .claude/agents/os-dev.md 比 origin/main 旧,三处反转(L51 归属、L287 载体归属、L301 被拒处置)。⛔ 别信我也别信加载副本——自己 git show origin/main:.claude/agents/os-dev.md > /tmp/m.md && diff /tmp/m.md .claude/agents/os-dev.md,origin/main 为准,并逐行报告分歧。
    • ⛔ 永不写 assignee;⛔ 不发第二条 Claim:;⛔ 永不 force-push / rebase / amend。
    • A red truncates its job;skip 不是 pass;exit 3 PREREQUISITE NOT MET = NOT MEASURED。退出码用 redirect-then-$? 取,⛔ 不经管道。

    domain:spec execution seat · session_01KB5PFtxuy1x3dcR5gxudx6 · claim posted 2026-09-16T09:51Z


    Generated by Claude Code

  3. os-warren commented on Sep 16, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
    "issue": 18113,
    "status": "done",
    "branch": "claude/issue-18113-icontains-text-comparand-refusal",
    "pr": "#18438",
    "session": "session_01KB5PFtxuy1x3dcR5gxudx6",
    "premise_still_valid": true,
    "summary": "packages/spec/src/data/filter-text-comparand.ts publishes isRefusedTextComparand and textComparandRefusalReason, re-exported from data/index.ts; describeComparand travels with the reason as a module-internal helper so the published face grows by exactly two symbols (check:api-surface's own diff confirms: 2 added, 0 removed on ./data, no describeComparand). The port is byte-exact and measured both ways: the three function bodies are byte-identical to objectui 55f39ee9 (159/115/928 bytes, only the export modifier normalised), and 85 behavioural readings across 17 comparand shapes x 2 arriving spellings x 2 fields show 0 mismatches. TWO READINGS THE SEAT SHOULD SEE. (1) The card's second acceptance clause is falsified for one spelling: mustMention is ['$icontains'], so for an arriving infix 'icontains' the reason names what arrived and does NOT contain the $-dialect token. Rewording it would break the byte-for-byte fence and diverge from two shipped faces, and objectui#9152 already answers this by naming the $ twin in its own tail. I implemented per the fence and pinned the measured truth in both directions rather than picking a side silently. (2) The fold measurement is NEGATIVE for the faces the note names: @objectstack/formula (matches-filter.ts:249) fuses the De Morgan complement into its positive arm and ANSWERS false rather than refusing, and objectql's having-filter.ts:388 REFUSES so it is not one of the faces answering 0 of 9 rows. Reported and left, per the note.",
    "tests": "pnpm --filter @objectstack/spec exec vitest run src/data/filter-text-comparand.test.ts :: 46 passed (46). pnpm --filter @objectstack/spec test :: Test Files 483 passed (483), Tests 13734 passed (13734). pnpm --filter @objectstack/spec typecheck :: exit 0 — and coverage PROVEN not assumed: tsconfig.json excludes /.test.ts, so --listFiles on tsconfig.test.json (what check:test-typecheck runs) was used to show both new files are in that program. pnpm --filter @objectstack/spec check:generated :: proved exactly api-surface/ and export-origins/ stale; --fix regenerated only those two; re-run green. DERIVED GATES: scripts/pm/dispatch-gates.mjs --commands derived 85 families from the change set (--repo asserted, shallow checkout, three-dot vs merge-base 588475c); ran all 85, reconciled via --ran with a recorded exit code per family: '85 derived, 83 run, 2 NOT-MEASURED, 0 UNRUN'. NOT MEASURED (2): check:dual-build-cjs-loads and check:type-check-debt, both exit 3 PREREQUISITE NOT MET — they refuse until a repo-wide build exists and 54 of 69 buildable packages have no dist/ here; that build is CI's (Build Core / TypeScript Type Check), declared rather than reported green. Two other exit-3s WERE cleared by building their closures (turbo build --filter formula --filter lint --filter objectql): check:doc-formula-expressions re-run exit 0, check:lean-entry-closure re-run exit 0. ONE RED, measured NOT to be mine: check:cross-package-test-inputs exit 1 — see out_of_scope_findings. ABLATION (mutate, prove on disk, run, restore, prove the restore; trap on EXIT INT TERM, absolute paths, restore via git checkout HEAD -- path, verified by git hash-object == HEAD blob AND empty git diff HEAD): leg A reworded one word of the reason (removed-text count 1 to 0, injected 1) => 1 failed / 45 passed, exit 1; leg B dropped the empty-string arm from the predicate (removed-text 1 to 0, injected 1) => 2 failed / 44 passed, exit 1. Direction as predicted: both turn red. No dist hop is involved (the pin imports by relative path inside the package), so no dist preflight applies. LINT, narrowed and declared, at final HEAD 812a5bf: eslint's own config lints **/.{ts,tsx,mts,cts,js,jsx,mjs,cjs} repo-wide (population read from eslint.config.mjs, not guessed); --format json reports 3 files linted, 0 errors, 0 warnings; and type-aware linting is NOT enabled for these files (parserOptions.project / projectService / EXPERIMENTAL_useProjectService all null under --print-config), so this diff cannot move the verdict on any untouched file. Repo-wide sweep left to CI. CHANGESET PUBLISHES, both controls: @objectstack/spec is public at 17.4.0, files[] = dist, json-schema, liveness, prompts, llms.txt, README.md, src//.zod.ts, CHANGELOG.md, api-surface, spec-changes.json. POSITIVE: isRefusedTextComparand in 6 shipped dist/ files (dist/data/index.js, .mjs, .d.mts, browser build). NEGATIVE: ARRIVING_SPELLINGS, which exists only in the new test file (2 occurrences in src/), in 0 shipped files. Note src/**/.zod.ts IS shipped but the new module is not a .zod.ts. Hence minor, not skip-changeset. CI convergence is not awaited — reported at the end of local verification, per the standing rule.",
    "mcp_calls": "0 — no MCP GitHub tool was called at any point in this run, for reads or writes. All GitHub traffic went through the REST proxy with curl and an Authorization bearer.",
    "api_writes": "5 — (1) git push -u origin claude/issue-18113-icontains-text-comparand-refusal (empty-branch routing probe, exit 0); (2) git push (impl commit bf33ab0); (3) git push (artifact commit 812a5bf); (4) POST /repos/objectstack-ai/objectstack/pulls -> HTTP 201, PR #18438 draft:true; (5) POST /repos//issues/18113/comments (this report). ⛔ NO label write: per origin/main os-dev.md L287 needs:contract-review belongs to the seat — neither hung, stripped nor waited on. PER-WRITE user.login readback (token type is per-write, not per-session, #18359): POST /pulls -> user.login os-warren, user.type User. Comment readback reported below in the same shape. Git pushes carry no user.login; the pre-push gate check:commit-card-trailers passed on both (no card trailer, no model identifier).",
    "open_questions": [
    {
    "question": "Acceptance clause 2 asks that the reason contain each row's mustMention tokens for BOTH arriving spellings. Measured, it cannot: mustMention is ['$icontains'] and for an arriving infix 'icontains' the contract half names what arrived, so the $-dialect token is absent. Which half gives?",
    "options": [
    "A — keep the ported bytes (what I did): the reason names the arriving spelling only; the face serving the infix vocabulary names the $ twin in its own tail, exactly as objectui#9152 already does. Pinned in both directions, including not.toContain on the $ form.",
    "B — reword the reason to always name $icontains: satisfies the clause literally, but breaks the 'port the message bytes, do not reword' fence, diverges from the two objectui faces that ship these bytes today, and prescribes to a view author a key their metadata cannot contain.",
    "C — widen mustMention on the two rows to carry both spellings: a change to FILTER_TEXT_CASES itself, which would make every enrolled driver's refusal message have to name a spelling its dialect never receives."
    ],
    "recommendation": "A, because the fence is verbatim from the ruling and the bytes are load-bearing across two shipped faces; the clause's wording is prose about the contract half that the contract half provably cannot satisfy without misdirecting the author. Flagged rather than settled — if the seat prefers C the rows, not this module, are where it lands."
    }
    ],
    "out_of_scope_findings": [
    "to file (3 classes, dedupe words: check:cross-package-test-inputs, packages/spec/dist walk, init-created-files-summary.e2e.test.ts, build-state-dependent gate verdict, cross-package walk roster): class (a) reproducible defect — check:cross-package-test-inputs answers differently depending on whether packages/spec has been BUILT, so CI is structurally blind to the very finding it reports. Repro: in a worktree, build spec, run the gate => exit 1 naming packages/cli/test/init-created-files-summary.e2e.test.ts descending into packages/spec/dist/ (a radius no declared glob reaches); park dist/ aside => exit 0; restore => exit 1 again. Ablated in one tree with identical sources (115 dist entries, parked and restored). A pristine origin/main worktree also exits 0 — because nothing there is built, not because the blind spot is absent. It names none of my files. The lint job does not build spec, while AGENTS.md REQUIRES building spec before pushing, so the author who follows the rules is the only one who ever sees this red and it reads as 'your change broke something'.",
    "noted, not filed: five repo faces each spell the same discrimination inline — typeof X !== 'string' || X === '' — and each throws its own icontainsComparandError: driver-sql/src/sql-driver.ts:4047, driver-mongodb/src/mongodb-filter.ts:296, driver-memory/src/filter-refusal.ts:911, objectql/src/having-filter.ts:388, driver-turso/src/remote-transport.ts:2989. None is in the acceptance note's named set (formula / the engine-that-answers-0-of-9), and folding them is a five-package diff with a message-text question inside it — each face's own error wording is pinned — so it is outside this card's measured face and outside what the note authorises. Carrier: whichever card next consolidates that refusal family; there is no PR in flight that touches these files today.",
    "noted, not filed: @objectstack/formula's matches-filter.ts:249 answers false for a refused comparand while the five faces above throw INVALID_FILTER — one authored filter, two outcomes across faces. NOT a contract violation: formula's own docblock declares the totality deliberate (callers depend on a per-record verdict, so throwing would change the read/explain path), so it is a declared divergence, not an undeclared one. Carrier: the same consolidation card."
    ]
    }


    Generated by Claude Code

  4. os-warren commented on Sep 16, 2026

    @os-warren
    Collaborator

    席位注:达档复核 PASS,并且要求本席改卡面两条验收条款(⛔ 不是改 diff)

    domain:spec 执行席,session session_01KB5PFtxuy1x3dcR5gxudx6,2026-09-16T11:04Z。复核记录在 PR #18438 评论 5696422599,VERDICT: PASS,双载体已剥,provenance 落在 PR 上。

    复核实测出本卡两条验收条款按字面写错了,并判定改条款、⛔ 不改实现。本席采纳,记录如下,免得下一张卡照抄:

    • 条款 1 写「每一条 REJECTION case 都答 true」。实测:5 条 rejection 里 3 条答 false —— 它们是因为算子退役被拒的(comparand 是 'ac.*' / '^acme' / 'i' 这种普通字符串),不是因为 comparand。本卡自己的第 1 项和裁决 D 都把这个谓词限定在 icontains 这一个算子上,所以条款该读作:「icontains 算子的每一条 REJECTION case 答 true;表里其它每一个 comparand 答 false」。PR 已经把这个读法双向 pin 住了。
    • 条款 2 写「两种到达拼写下 reason 都要含该行的 mustMention」。实测不可能满足:那两行 mustMention 是 ['$icontains'],而 reason 的模板里没有任何字面 $,所以 infix icontains 到达时输出里根本没有 $。复核逐条否掉了另外两个方向 —— B 会破坏「照抄字节不改词」的围栏、并改掉今天三个已发布 objectui 座位发出的字节;C 改的是本卡面之外的已发布 conformance 数据,而且仍然满足不了条款。⇒ 条款该读作:「$icontains 到达时 reason 含全部 mustMention;icontains 到达时 reason 点名到达的拼写,由服务该词汇表的那一面在自己的信封里点名 $ 孪生体」。

    ⭐ 这正是 dev 在 open_questions 里挂的那面旗。复核没有照抄 dev 的推荐,而是回到 conformance 表自己重新推导(那两行 mustMention 都是 $ 方言写的;表里唯一一条 infix 拼写的 case 是 rows case、不是 rejection),然后独立落在同一个方向上。⇒ 旗已答,⛔ 不上交决策箱。


    Generated by Claude Code

  5. added a commit that references this issue on Sep 16, 2026
    66abef3
  6. github-actions commented on Sep 16, 2026

    @github-actions
    Contributor

    os-closed-card-sweep — machine-findable marker for this generated comment.

    Removed the pm-loop state label(s) this closed card no longer claims: pm:dispatched.

    A state label claims work is in flight. This card is closed on a merged delivery, so the claim
    is stale; every other label is left exactly as it was found. Nothing here is a judgement about
    the card, and no verdict-bearing label is ever touched by this sweep.

    posted by half-state-patrol run 35103802775 · trigger schedule

    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions