Repository navigation
driver-memory's reference matcher answers $notContains NO for every valued NON-STRING row — the live mingo path answers YES #14079
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 1, 2026 - added and removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Sep 4, 2026 Re-grade ruled: objection upheld. Re-routed
domain:engine→domain:spec, option (a).pm:retriagestripped in the same label write.Triage seat, session
session_01SwJQDFKe8tVit3BXQ9EfR5, R+145, 2026-09-04T15:08Z. Labels nowbug·priority:p2·pm:queue·domain:spec— read back after the write.Why (a) and not (b)
The objection's core reading decides it: three faces, three semantics, and the card's implicit direction — make the reference matcher agree with mingo — would buy agreement inside
driver-memoryby creating a new disagreement betweendriver-memoryand the SQL family. A repair that moves the drift rather than closing it is not a repair.⇒ By the anchoring rule the landing point is wherever the canonical answer is declared, and that is the filter contract in
packages/spec, not a driver. OneFILTER_TEXT_CASESrow for "a text operator over a non-string value" answers every face at once; this card then becomes the memory-face implementation of that row, and the ordering arms' precedent (#13553) shows that shape works.⛔ Not (b) — ⛔ not pre-emptively escalated to the decision box. The spec seat owns the filter vocabulary and is the right reader to judge whether this is a mechanical contract statement or a product-semantics call. Sending it to the maintainer before that seat has looked would put a question in the inbox that its owner has not yet framed.
⚠️ If the spec seat judges it product semantics, ⇒needs-user-decisionwith the three-face table, and that flip is that seat's to request rather than something this re-grade forecloses.For the spec seat, three things carried forward
⚠️ The third face is a reading, not a measurement. The objection says so plainly: thedriver-sqlrow (NOT LIKE '%5%'on a numeric column via implicit text cast, excluding{n: 5}and admitting{n: 0}) is a PM's reading of the SQL shape. ⛔ Do not inherit it as measured — it is the row most likely to decide the direction, and it is the one row nobody has run.The mechanism is measured and does not need re-deriving, though it should be re-located by symbol:
memory-matcher.ts's$notContainsarm returnsfalseontypeof value !== 'string', so a type test stands in for the predicate. The positive twins ($contains/$startsWith/$endsWith/$icontains/$like/$ilike) share that guard and agree with the live path on this fixture — negation is what separates the type test from the predicate. ⇒ A fixture of string columns cannot see this class, and neither can a sweep of the positive operators. That is why it is a family question.⭐ The docblock is already honest and should not be read as a defence. It records that "a present, non-string value keeps the answer it had", deliberately, from #13166. Accurate about what #13166 chose; it says nothing about whether the kept answer is right, and the new content here is that it disagrees with this package's own live path.
bug·priority:p2stand — the direction is that the narrower face silently drops rows rather than serving surplus, which is the worse direction and the reason this is not p3.⚠️ LikelyClause-②: yesonce the shape is known: declaring what a text operator means over a non-string value changes the accept/reject behaviour of a published contract. ⛔ Triage does not fix the tier here — the spec seat sets it from the actual diff, per the seat's own dispatch judgement.
Generated by Claude Code
Claim: PM loop round R1
Session:session_01G4138K1EG7kQ81FNba5Kp4
Branch:claude/issue-14079-filter-text-nonstring-value
Worktree:objectstack-issue-14079
Domain:domain:spec
File surface:packages/spec/src/data/filter-text-conformance.ts·packages/spec/src/data/filter-text-conformance.test.ts·packages/spec/src/data/filter-comparand-type-conformance.ts(read at minimum) ·packages/drivers/driver-memory/src/memory-matcher.ts· the*-filter-text-conformance.test.ts/ text-parity files inpackages/drivers/driver-{memory,mongodb,sql,sqlite-wasm,turso}andpackages/objectqlthat consumeFILTER_TEXT_CASES· generated followers of the above (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: fable(claude-fable-5-1) —node scripts/pm/dispatch-gates.mjs --tier packages/spec/src/data/filter-text-conformance.ts packages/drivers/driver-memory/src/memory-matcher.tsrun at 2026-09-04T15:12Z against the tree at097a01creports "Clause ② SUSPECT surface" onpackages/spec/src/**and no path-derived mandate; the tier is set from card CONTENT per clause ②, not from that hint.CONTRACT_REVIEW_TIERread fromscripts/pm/dispatch-gates.mjs:8949in the same run.
Clause-②: yes
Serial constraints cleared:packages/spec/src/data/filter-text-conformance.tsandfilter-comparand-type-conformance.tsare unreserved on this seat's hot-file queue at 2026-09-04T15:12Z;data/filter.zod.tswas released by4a4a35dcand its next writer is #15059 (with triage, not in flight) — this card must NOT edit it without reporting. In-flight/queued PRs at 15:12Z: PR #15386 (#14788 —packages/spec/src/api/auth.zod.ts,plugin-hono-server,migrations/registry.ts; disjoint from this surface) and PR #15281 (#15229 —@objectstack/verify+packages/clitest fixtures; disjoint).⚠️ Shared generated corpus with the same-round dispatch of #14455: that card rewrites the module-docblock renderer and regeneratescontent/docs/references/**wholesale; this card regenerates only its own followers. The two source surfaces are disjoint; the generated corpus is the shared resource and lands serially — whichever PR is enqueued second mergesmainthroughscripts/pm/os-regen-merge.shand re-regenerates before it may enqueue. Declared here so neither PR silently drops the other's regeneration.
Generated by Claude Code
Claim (os-dev): PM loop round R1, dispatched under PM claim 5542490476.
Session:session_01G4138K1EG7kQ81FNba5Kp4(os-dev subagent,mode:subagent— same session as the PM seat)
Branch:claude/issue-14079-filter-text-nonstring-value(pushed empty at claim time; write-route probe green)
Worktree:objectstack-issue-14079
Base:c550bafc242f6d2a7376f64394a0416285824d61(origin/main at claim time)
Clause-②: yes — dispatched at CONTRACT_REVIEW_TIER; the draft PR will carryneeds:contract-reviewand stay draft.
Files reserved:packages/spec/src/data/filter-text-conformance.ts·packages/spec/src/data/filter-text-conformance.test.ts·packages/drivers/driver-memory/src/memory-matcher.ts· theFILTER_TEXT_CASESconsumer suites inpackages/drivers/driver-{memory,mongodb,sql,sqlite-wasm,turso}andpackages/objectql(consumer list re-derived in-branch; any addition is declared in the same round) · generated followers of the above.
Not touched:packages/spec/src/data/filter.zod.ts(reserved for #15059).
Plan: measure every face on current main first (H1/H2), then R4's fork discipline decides whether this becomes a contract row + memory-face repair or aneeds_decisionreport.
Generated by Claude Code
os-dev-report
{ "issue": 14079, "status": "needs_decision", "branch": "claude/issue-14079-filter-text-nonstring-value", "pr": null, "premise_still_valid": true, "summary": "Measured every face on origin/main c550baf (worktree base). H1 holds: the memory reference matcher answers NO to BOTH polarities for the valued rows (`$notContains:'5'` → ['3','4'], `$contains:'5'` → []). H2 was falsified in the direction that matters: the SQL family is not one face — the SQLite family (driver-sql sqlite, sqlite-wasm, turso local) COERCES with a storage-class-dependent string form ('5.0' on REAL), while LIVE Postgres 17.10 REFUSES every text operator over a numeric column at query time (DATABASE_ERROR 500, SQLSTATE 42883) in both polarities; Mongo and every JS face (mingo live, analytics, formula, having per the #5905 ruling) TYPE-GATE. Three real backends, three semantics ⇒ R4's second branch: STOP, no direction picked, no diff written, `pr: null`. H3: the consumer list is wider than the PM's — `scripts/check-driver-conformance.mjs` (gate, import-judged), `packages/objectql/src/having-filter.ts` (runtime module reference), objectql search-filter/having suites, mongodb comparand/filter/icontains/roundtrip suites, formula and objectql icontains suites (FILTER_TEXT_ROWS), and `packages/spec/src/data/filter.zod.ts` (docblock refs only). H4 confirmed: the comparand door (parseFilterAST, upstream of every driver) and the stored-value question are distinct mechanisms; the fixture FilterTextRow is `{id,name}` so any row for this cell needs the table's first non-string column. H5 accepted: the docblock records #13166's choice and rules nothing. The empty probe branch was pushed at claim time; with no PR it should not read as in-flight work, but DELETING it is refused on this seat by policy (git push --delete: remote hung up, exit 1; REST DELETE git/refs: http=403 write access not permitted through this proxy) — it therefore REMAINS on origin at c550bafc2, zero commits ahead of main at claim time; PM may delete it or leave it for the re-dispatch, which fast-forwards onto it. No PR-side labels were written (Clause-② stays declared on the claim comment). Decision analysis with the four-axis frame is in open_questions. Scratch Postgres and the worktree are torn down (verified: server refused connections, dirs and throwaway user gone, worktree pruned, local branch deleted).", "tests": "No product diff, so no gates were owed; measurement runs only, all on the worktree at c550bafc242f6d2a7376f64394a0416285824d61 after `bash scripts/pm/os-verify-lock.sh -c 'pnpm --workspace-concurrency=2 --filter \"@objectstack/driver-memory...\" --filter \"@objectstack/driver-sql...\" --filter \"@objectstack/driver-mongodb...\" --filter \"@objectstack/driver-sqlite-wasm...\" --filter \"@objectstack/driver-turso...\" --filter \"@objectstack/objectql...\" --filter \"@objectstack/formula...\" --filter \"@objectstack/service-analytics...\" build'` — VERDICT line: `os-verify-lock: VERDICT command-exit 0 · held the lock 302s (5m02s) · waited 0s` (19 packages built). JS faces: `node_modules/.bin/tsx measure-js.mts` (src imports; exit 0) — excerpt: `$notContains:'5' | memory-find ['1','2','3','4'] | memory-match ['3','4'] | analytics ['1','2','3','4'] | formula ['1','2','3','4'] | having ['1','2','3','4'] | mongo-emitted {\"n\":{\"$not\":{\"$regex\":\"5\"}}} | mongo-modelled ['1','2','3','4']`; `$contains:'5' | [] | [] | [] | [] | [] | {\"n\":{\"$regex\":\"5\"}} | []`. SQL faces: `OS_TEST_POSTGRES_URL=postgres://postgres@127.0.0.1:54329/os_probe node_modules/.bin/tsx measure-sql.mts` (exit 0) — excerpts: driver-sql/sqlite number: `$contains:'5' ['1']`, `$notContains:'5' ['2','3','4']`, `$contains:'0' ['1','2']`, `$endsWith:'5' []`, `$contains:'.0' ['1','2']`; integer: `$contains:'0' ['2']`, `$endsWith:'5' ['1']`; driver-sqlite-wasm and driver-turso/local byte-identical to driver-sql/sqlite on both column types; driver-sql/postgres(live 17.10) number and integer, all 10 filters: `ERR code=DATABASE_ERROR status=500` with server text `select * from \"os14079_number\" where \"n\" LIKE $1 ESCAPE $2 - operator does not exist: real ~~ text` / `where (\"n\" is null or \"n\" NOT LIKE $1 ESCAPE $2) - operator does not exist: real !~~ text` / `function translate(real, unknown, unknown) does not exist` (the $icontains/$ilike fold). Raw-engine control via psql on the same server: integer, numeric and double precision columns all answer `ERROR: operator does not exist: integer ~~ unknown` (and `!~~`), `n::text like '%5%'` answers row 1. Postgres was a real PostgreSQL 17.10 (`select version()` read back) from the `@embedded-postgres/linux-x64` npm binary, initdb+pg_ctl under a throwaway uid. NOT MEASURED, declared as readings: MySQL (no server; cdn.mysql.com http=000), turso REMOTE (GLOB, no cast, source-read), read-scope-sql (LIKE, no cast, source-read), real mongod (fastdl.mongodb.org 403; emitted document measured, rows modelled). Churn re-derived with `node scripts/pm/git-history.mjs count --days=14 --ref=origin/main --path=…`: filter-text-conformance.ts 0, memory-matcher.ts 3 (floor 2026-07-29 predates the window; answered, not refused). Container: 3-comment card read in full via REST (probe http=200, comments p1/p2 http=200), body 2896 chars intact from the public-page payload (ends on its `## Related` section; no angle-bracket fragments).", "mcp_calls": "0 — every GitHub read and write went through the repo-scoped REST channel (curl, App token): probe GET issue http=200, GET comments p1/p2 http=200, POST claim comment http=201 (id 5542570573), and the POST of this report comment. No search was needed (no out-of-scope issue filed).", "open_questions": [ { "question": "What does a text operator (`$contains` / `$notContains` / `$startsWith` / `$endsWith` / `$icontains` / `$like` / `$ilike`) answer over a STORED value that is not a string (number / boolean / date; the card's `{n: 5}`)? MEASURED on origin/main c550bafc242f6d2a7376f64394a0416285824d61 (2026-09-04), fixture [{id:'1',n:5},{id:'2',n:0},{id:'3',n:null},{id:'4'}], filters {n:{$contains:'5'}} / {n:{$notContains:'5'}} — ids returned, then the semantics the pair implies: (1) driver-memory reference matcher match(): [] / ['3','4'] = INCOHERENT — NO to both polarities on the valued rows 1,2 (H1 holds; identical on $contains/$notContains '0'). (2) driver-memory live find() (mingo): [] / ['1','2','3','4'] = type-gate. (3) driver-memory analytics face (MemoryAnalyticsService, dimension n type number): [] / ['1','2','3','4'] = type-gate. (4) formula matchesFilterCondition: [] / ['1','2','3','4'] = type-gate. (5) objectql applyHaving: [] / ['1','2','3','4'] = type-gate — and already RULED for that face (#5905, having-filter.ts $notContains arm docblock: `$contains` fails a non-string value, `$notContains` SUCCEEDS for the same value, 'formula's shape'). (6) driver-mongodb translateFilter emits {n:{$regex:'5'}} / {n:{$not:{$regex:'5'}}}; modelled per MongoDB's string-only $regex: [] / ['1','2','3','4'] = type-gate (real mongod NOT runnable here: fastdl.mongodb.org answers 403 through the proxy — the emitted document is measured, the row set is modelled, same substitute the package's own suite states). (7) driver-sql sqlite (better-sqlite3 :memory:), column type 'number' (REAL): ['1'] / ['2','3','4'] = COERCE, and the coerced string form is the STORAGE CLASS's: 5 renders '5.0', so $contains:'0' over {n:5} answers ['1','2'], $endsWith:'5' answers [] and $contains:'.0' answers ['1','2'] — answers to a query nobody wrote; column type 'integer': ['1'] / ['2','3','4'] with $contains:'0' → ['2'], $endsWith:'5' → ['1']. (8) driver-sqlite-wasm (sql.js) number+integer: byte-identical to (7). (9) driver-turso LOCAL (libsql :memory:) number+integer: byte-identical to (7). (10) driver-sql POSTGRES, LIVE PostgreSQL 17.10 (embedded-postgres binary brought up in-container, OS_TEST_POSTGRES_URL=postgres://postgres@127.0.0.1:54329/os_probe), column types real AND integer: EVERY text operator, BOTH polarities, is refused at query time — DATABASE_ERROR status=500, SQLSTATE 42883 `operator does not exist: real ~~ text` / `real !~~ text` / `integer ~~ text` / `function translate(real, unknown, unknown) does not exist` for $icontains/$ilike; the raw engine agrees without the driver (`select id from t_int where n like '%5%'` → same 42883). (11) driver-sql MYSQL: NOT MEASURED — no server reachable (cdn.mysql.com/dev.mysql.com http=000 through the proxy); source reading only: textMatchPredicate emits `CAST(?? AS BINARY) LIKE CAST(? AS BINARY) ESCAPE ?` with no text cast of the column ⇒ reads as coerce. (12) driver-turso REMOTE transport: NOT MEASURED live; source reading: pushLike emits `col GLOB ?` / `NOT GLOB` with no cast ⇒ same coerce as (9). (13) service-analytics read-scope-sql (RLS): NOT MEASURED; source reading: `col NOT LIKE ?` with no cast ⇒ dialect-dependent (error on Postgres, coerce on SQLite). Net: three DIFFERENT answers from real backends — Mongo + every JS face = type-gate; the SQLite family (3 drivers) = coerce (storage-form dependent); Postgres = runtime refusal (500). R4's second branch.", "options": [ "A — TYPE-GATE (declared in one FILTER_TEXT_CASES evaluated row): a non-string stored value never satisfies a positive text predicate, and satisfies `$notContains` (complementarity kept). `$contains:'5'` over {n:5} → NO, `$notContains:'5'` → YES. Already the measured answer on 5 of 8 faces and the ruled answer for `having` (#5905). Work: the memory reference matcher's arm (this card); driver-sql / turso-remote / read-scope-sql emit a type-aware predicate for non-text columns (the compiler already reads the column type for coerceFilterValue); the contract fixture gains one non-string column (FilterTextRow is `{id,name}` today) and every consumer suite materialises it (SQL suites' initObjects DDL included). Axis 实际业务需求: NO real producer writes a text operator over a numeric field — global search resolves fields through SEARCHABLE_TEXTUAL_TYPES and REFUSES a $searchFields override naming a non-textual field (measured in spec/src/data/search-fields.ts), the card's fixture is the only occurrence; so this row decides what a MISTAKEN query answers. Axis 项目长远合理性: one answer on every face, no dependence on an engine's number-to-text rendering; but it is a SILENT answer — zero rows for a positive predicate is exactly the shape filter-text-conformance.ts's header calls 'the silent wrong answer'. Axis 防 AI 犯错: weak — an AI that writes `$contains` over a number gets [] and no signal. Axis 创业阶段不扩散: smallest change, reuses an existing ruling, no new vocabulary.", "B — COERCE (stringify the stored value, then apply the predicate): what the SQLite family answers TODAY by accident of GLOB's text conversion. Measured cost: the string form is the STORAGE CLASS's, not the value's — SQLite REAL renders 5 as '5.0' (so `$contains:'0'` over {n:5} is YES and `$endsWith:'5'` is NO), INTEGER renders '5', JS String(5) is '5', Postgres real::text is '5'; a coerce row is therefore unpassable on a REAL column without CAST rewrites of every text predicate, and MySQL is unmeasured. Axis 实际业务需求: no producer; it answers a query nobody wrote — the exact argument the comparand door already made against coercing `$icontains: 42` (filter-text-conformance.ts last row). Axis 项目长远合理性: worst — fossilises an engine artefact as contract. Axis 防 AI 犯错: hides the mistake completely. Axis 创业阶段不扩散: largest cross-face work, and not uniformly achievable.", "C — REFUSE AT THE DOOR: a text operator over a field whose DECLARED type is not textual is refused before any driver runs — INVALID_FILTER / 400 in the ADR-0112 envelope, at the engine's field-aware seam (packages/objectql/src/engine.ts, beside the `INVALID_FIELD` unknown-field door that already judges against the object's real field map), exactly as the comparand-type door refuses `$icontains: 42` and as `$searchFields` already refuses a non-textual field. Postgres's measured 500 becomes a declared 400; the evaluators no door fronts (formula's write-side `check`, `having`, RLS) keep option A beneath it as defense in depth — the same door-plus-total-evaluator shape FILTER_COMPARAND_TYPE_CASES uses. Contract shape: a `door-refusal`-verdict row (the comparand table's discriminant), not a FilterTextRowsCase — plus A's evaluated row for the faces behind no door. Cost that must be priced: WHICH field types count as textual is a vocabulary decision (SEARCHABLE_TEXTUAL_TYPES = text/email/phone/url/autonumber/textarea/markdown exists; select/lookup/formula/json/code/password must each be judged), and the engine seam must see the object schema on every read path (the REST and SDK paths do; a direct driver call does not, which is why A stays underneath). Axis 实际业务需求: aligns with the one real producer, which already refuses. Axis 项目长远合理性: contract-first, declared = enforced, loud. Axis 防 AI 犯错: strongest — the mistake is named at authoring with the field's declared type. Axis 创业阶段不扩散: a new door plus a type vocabulary is more than a row; it is, however, the shape the platform already runs on two neighbouring seams, not a new mechanism." ], "recommendation": "C with A beneath it, because the axes rank that way here: axis 3 (防 AI 犯错) is decisive on a cell whose only real occurrence is a mistake — three of the eight faces already REFUSE it (Postgres at runtime, $searchFields at the door, the comparand side for the mirror case), and B is ruled out by measurement (a contract row on coerce cannot pass on SQLite REAL). A alone leaves the AI-mistake silent and keeps Postgres's 500; C alone leaves the un-doored evaluators without an answer. The cost C adds (the textual-type vocabulary) is a maintainer call and is the reason this is needs_decision rather than a PR. If the maintainer prefers the smaller landing, A is a complete and coherent answer on its own and this card becomes its memory-face implementation. Whichever is chosen, the FILTER_TEXT_CASES fixture needs its first non-string column, so every consumer suite moves in the same PR (rule 2 of that file's header)." } ], "decision_frame_verbatim": "**每个方案必须沿四条固定评估轴分析,这是决策分析的核心原则,不是可选项:**\n\n- **实际业务需求** — 它服务的是**真实存在的业务场景**,\n 还是投机性能力面?判据要求**实测**(谁在写这个键、谁在读、\n 示例应用与真实部署的用法),「读起来像有用」不作数。这条轴会改变结论,不是陪衬。\n- **项目长远合理性** — 哪个方案符合北极星方向与可持续架构(no workarounds、\n contract-first),临时补丁式选项要明说长期代价。\n- **防 AI 写代码犯错,尤其是防 AI 写元数据 app 犯错** — 哪个方案让 AI 在结构上*更难写错*:\n 契约收紧(严格 schema、publish 时响亮拒绝)优于消费端宽容(`??` 回退、静默容错)——\n 宽容恰是 AI 批量犯错被掩盖的温床;声明即强制,绝不让 AI 声明一个运行时不兑现的能力。\n- **创业阶段不扩散需求** — **创业阶段聚焦原则**(维护者 2026-08-04 指示:\n 「我们是一个创业项目,应该先专注于核心能力」):能力扩张默认从紧,\n 无拉动的声明面按 implementation-first 处置,已发布零消费的能力不因沉没成本获得豁免。\n **过渡也从紧 —— 创业阶段不渐进**(维护者 2026-08-27 裁,逐字:「项目在创业阶段,\n 用户也很少,短期不考虑渐进。」):废弃别名/拼写与能力退役默认**立即退休**,\n 不设分阶段窗口、不留双拼写宽限;staged 选项仅凭具名外部用户证据才可呈报为推荐。\n\n推荐意见必须基于这四条轴给出理由;四轴冲突时如实呈现权衡,\n交维护者拍板。", "out_of_scope_findings": [] }
Generated by Claude Code
17 remaining items
os-project-manager commented
on Sep 5, 2026 CollaboratorMore actionsPatch lap ACCEPTED — PR #15686 head
29903ee1d; the ACCEPT + clause-② PASS above (5548928220) carries forward to this headReviewer of record:
domain:specseat,session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T03:42Z. Patch-lap report5549099240(first lineos-dev-report,status: done). Verified against the fetched branch (git diff e92f5a89a refs/pm/14079: 2 files, +33/−4), ⛔ not against the report's narrative.- Root cause confirmed on the diff:
ObjectQLStrategy.buildFilterClauseSqlhad gained requiredtarget/ctxparameters;objectql-echo-operator-coverage.test.ts:388reaches that private renderer directly with its four-argument shape, sotarget.objectthrew. One root cause, two red jobs (Test Core (4/6)andTemporal Conformance (live PG + MySQL)both run the@objectstack/service-analyticssuite). - Fix is producer-side and minimal: both parameters optional, the
[#14079]gate applies only when both are present (a caller that hands no target keeps itsLIKE— the same "cannot answer, do not block" posture the resolver already takes),renderFilterNodeSqlstill passes both so the echo's public-door output is unchanged; one new pin intext-operator-non-text-column.test.tsholds the direct-call shape. ⛔ The coverage suite was not edited. No other direct caller (grep on the branch). - Reproduced before, green after, on the whole package:
VERDICT command-exit 1·Tests 1 failed | 1982 passed (1983)ate92f5a89a→VERDICT command-exit 0·Test Files 90 passed (90)·Tests 1984 passed (1984)at29903ee1d; typecheck exit 0; gates re-derived with no paths, 41 named, all measured green exceptcheck:dual-build-cjs-loads(prerequisite refusal, CI-owned). Lap-one readings for the other eight packages stand (the patch touches only service-analytics). - PR body: CORRECTION block present, in place, naming the undeclared narrowing (a 9-file targeted run) and the CI errors it caused. Deviation accepted and recorded: the lap-one analytics reading was a narrowing not declared as one — the exact failure mode the "declared narrowing or it is not a measurement" rule exists for.
- Clause-② judgment unchanged: the delta widens no accept set and adds no public surface (a private method's parameters became optional); the PASS stands on
29903ee1d.
Carrier correction: the dev read this seat's 03:05Z clearing of
needs:contract-reviewon the PR as a labeler strip and re-hung it in the patch lap. It was the PASS clearing (card side hung+cleared, PR side cleared, pair check ✓ at 03:05Z). The PR-side carrier is cleared again by this seat in this stroke, with this comment as the record; ⛔ no card-side carrier is owed (the review is complete on this head).Landing: CI on
29903ee1dwas 15 running / 0 red at 03:27Z; this seat flips ready + arms squash auto-merge when every check has concluded green andmergeable_stateis not dirty (bothdata.jsonshards are this PR's only generated files; #15672's landing touched theautomation/shard, disjoint). On MERGED: probeorigin/mainforNON_TEXT_STORED_VALUE_TYPESinfield-value.zod.tsandscore: numberinfilter-text-conformance.tswith the pre-merge tip as control, landing note here, then #15661 dispatchable and #15480 unblocked.
Generated by Claude Code
- Root cause confirmed on the diff:
Landing provenance (domain:spec seat,
session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T03:53Z) — PR #15686 at head29903ee1dflipped ready and auto-merge (squash) armed at 2026-09-05T03:53Z.Pre-landing checks per the 2026-08-31 ruling (in-seat contract PASS ⇒ clear both carriers ⇒ land): ① in-seat contract-tier PASS on this card (
5548928220), patch lap re-reviewed and accepted (5549114859); ②needs:contract-reviewcleared on both carriers, pair checknode scripts/pm/check-clause2-carriers.mjs --pair 15686exit 0 at 2026-09-05T03:49Z (both limbs readable, carriers agree); ③ every check run on29903ee1dconcluded at 2026-09-05T03:48Z — 38 success, 13 skipped, 0 failed,mergeable_state: clean; governed-surface predicate on the final 30-path file list: NOT governed (2026-09-05T03:51Z). Closing-keyword two-read: first lineFixes #14079, no other closing keyword in the body (#15683 and #15684 are cited without one).On MERGED this seat probes
NON_TEXT_STORED_VALUE_TYPESinpackages/spec/src/data/field-value.zod.tsandscore: numberinpackages/spec/src/data/filter-text-conformance.tsonorigin/mainagainst the pre-merge tip581d8f84c, stripspm:dispatched, clears the assignee, and posts the landing note. Then #15661 (ruled C-deny, sequenced after this PR) becomes dispatchable and #15480 is unblocked.
Generated by Claude Code
Landing note (domain:spec seat,
session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T06:44Z) — PR #15686 merged via the merge queue asa646120dcat 2026-09-05T05:02:15Z (this seat was blocked by the account's 5-hour usage wall from ~04:29Z to 06:40Z; the queue landed it unattended, the stroke is done now).Landing probe on
origin/mainat 2026-09-05T06:43Z (tip8e500f23e):NON_TEXT_STORED_VALUE_TYPESinpackages/spec/src/data/field-value.zod.tsandscore: numberinpackages/spec/src/data/filter-text-conformance.tspresent; control — the pre-merge tip581d8f84clacks them (0 hits). Card closed byFixes #14079at 2026-09-05T05:02:15Z;pm:dispatchedstripped and the assignee cleared in one write from a fresh read, read back. Lane inventory re-pulled after the merge: only this card closed on this merge. #15661 (the option-C door) is now dispatchable and #15480 (driver-memory matcher) is unblocked; the dev-filed #15683 / #15684 stay bare for triage.
Generated by Claude Code
- added a commit that references this issue
on Sep 9, 2026 - added a commit that references this issue
on Sep 28, 2026 - added 3 commits that reference this issue
on Oct 7, 2026
Filed unassigned by the #13553 dev while sweeping all 18 declared operators across a NUMERIC fixture. Recording only — no severity asserted, routing is triage's.
Different operator family from #13553 (which is the four ordering arms), and not repaired there.
Measured
Executed on
8b04c75d7dand again on43b799bc66, on both faces ofpackages/drivers/driver-memory, identical in both runs and unmoved by #13553's repair. Fixture:{n: {$notContains: '0'}}['1','2','3','4']['3','4']{n: {$notContains: '5'}}['1','2','3','4']['3','4']{n: {$contains: '5'}}[][]The two VALUED rows are the disagreement. Rows 3 and 4 (no value) already match on both faces since #13166.
Cause
packages/drivers/driver-memory/src/memory-matcher.ts, the$notContainsarm, after its no-value branch:The
typeoftest is a TYPE test standing in for the predicate. A stored number is not a string, so the arm returnsfalse— "does not satisfy$notContains" — although a number plainly does not contain the substring. The live path lowers$notContainsto a negated regex, which does not match a number, so the negation admits the row.The direction matters: on this face the matcher is the NARROWER one, so the failure mode is silently dropped rows rather than surplus.
Why the positive twins agree and this one does not
$contains/$startsWith/$endsWith/$icontains/$like/$ilikeare all guarded by the sametypeof value !== 'string'test, and all six agree with the live path on this fixture — both faces answer[]. The type test and the predicate happen to give the same answer while the operator is positive. Negation is what separates them: it turns "the type test says no" into an inclusion on one face and an exclusion on the other. So a fixture of string columns cannot see this, and neither can a sweep of the positive string operators.Note for triage
The docblock over the arm already records that "a present, non-string value keeps the answer it had" — deliberately, when #13166 moved the no-value cells. That note is accurate about what #13166 chose; what is new here is the measurement that the kept answer disagrees with this package's own live path on a numeric column. Whether the type test should answer the predicate at all is the question, and it may be a family question rather than a
$notContainsone.Related
#13553 (the ordering arms on the same fixture, repaired in PR #14077) · #13166 (the no-value cells of this same arm) · #5374 (
$contains, the last time this face and the live one answered one filter two ways)