Repository navigation
[finding] os explain query's example teaches two keys QuerySchema SILENTLY DROPS — filters/sort are where/orderBy, and the #14811 sweep is green on it #16925
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentation
on Sep 8, 2026 - addedpriority:p2Medium: important, M3Medium: important, M3
on Sep 9, 2026 分诊
documentationfindingdomain:clipriority:p2pm:queue· typeBug· 未指派卡片的事实我逐行复核过,成立,并且我测到了两条它没有的读数。
复核(读的是行,不是行号)
packages/spec/src/data/query.zod.ts:514——const BaseQuerySchema = z.object({,不是strictObject。其真实键读下来是object/fields/where/search/searchFields/orderBy/limit/offset/top/aggregations/groupBy/having,外加四个retiredKey墓碑(cursor、joins、windowFunctions、distinct)。filters与sort既不在键里,也没有墓碑 —— 所以既不报错、也不给处方,纯静默剥离。卡片给的解析读数与此一致。packages/cli/src/commands/explain.ts:215-216(optional 表两行)与:223-224(example 两行)确是这两个拼写。⭐ 读数一:这不是漏网,是已声明为故意、且已有归属的缺口
query.zod.ts:61-62逐字写着:// Deliberately NOT taken here:BaseQuerySchema's own top level stays
// non-strict. That is #4001's to schedule.同一文件
:20-21、:74-79记录了SortNodeSchema被关闭的原因,而那段病历描述的正是本卡的失效形态:「direction被剥离,order回落到asc默认,降序请求以升序返回 —— 与limit同时出现时,那不是重排的一页,而是一批不同的行,响应里没有任何信号」。⇒ 类根(顶层非严格)在
packages/spec,归 #4001,⛔ 不是本卡的。 本卡在packages/cli内可闭合的是两半,都落在packages/cli⇒ 车道domain:cli(按落地包判定,非按标题词汇)。⭐ 读数二:类的规模是 1 / 9,不是「下一个非严格 schema 会复现」
把
commands.test.ts:222-232的 9 个BOUND条目逐个读回 spec:条目 schema 顶层 view / flow / agent / app / dashboard / action / field strictObject(action经actionObject()action.zod.ts:840)严格 object ObjectSchemaBase,其 docblock 逐字写 「No silent strip (ADR-0032 / #1535): unknown top-level keys … are rejected」严格 query BaseQuerySchema.extend(...)=z.object开放 ⇒
query是 9 个绑定条目里唯一会静默剥离的那个。 这修正了卡片「下一个非严格 schema 会以同样方式复现且扫描全绿」的措辞:今天类的规模是 1,且方向是收敛的(#4001 在关,不在开)。键保留断言因此不是"补一个大洞",而是棘轮:防止将来某个绑定重新落到开放顶层时无声复现。这不降低它的价值,只把它的理由说对。⭐ 读数三:扫描还有一面它自己从没看过 ——
optional表commands.test.ts里evaluate(key)只取catalog[key].example,从不读optional/required表。本卡的两个错拼写同时住在这两面,而扫描只能看见其中一面。这正是扫描自己抬头警告的形状(「A guard reporting green over the entries it never looked at is this card's own defect, one layer up」),只是又高一层。顺带(⛔ 未测到判决,不属本卡):
view条目:93-94的 optional 表也列着filters/sort,而ViewSchema:3676是容器、strictObject。view的 example 已由 #15171 承接(响亮失败),但它的表同样在扫描视野之外。若接手者做键保留断言,建议顺手确认这一面的归属,⛔ 不要在本卡里替 #15171 改 view。定级
priority:p2。理由:害处是成功响应下的错误行(无 filter、无 ordering,配limit: 50),不是难读的文档;而且仓库为此建的补偿控制(#14811 扫描)在它上面是绿的。不是 p1:没有运行时行为出错,爆炸半径止于抄这一个示例的作者。关于「reach 未测 ⇒ p3」:此处不适用。未测的是「有多少人抄过这个示例」,而缺陷在制品本身 ——
os explain存在的唯一目的就是被抄,它输出的键表与它所记录的契约相矛盾,其正确性不以受众规模为条件。我把 reach 明确记为 ⛔ 未测量,并给出双向触发器。type
Bug:explain目录把filters: Filter[]声明为 Query 的键,而 Query 的已声明契约没有此键 —— 输出与所记录的契约相违背。⛔ 非 Feature(不扩大任何接受集)。重新定级触发器(双向)
- 升 p1:若测到任一已落地的源(仓内示例、模板、脚手架、文档站正文)照抄了这个示例形状 —— 即静默剥离已经越过"作者可能抄"进入"已经抄了"。
- 降 p3:若接手者测到
os explain的 example 面在别处已有等价的正确性门禁覆盖(即 [finding]os explain's 11 other catalog entries are hand-maintained against no schema — theflowentry's sample was unparseable and nothing said so #14811 不是唯一控制),则「假绿」这一半消失,只剩单条文档更正。
接手者提示
- 第一半(改
:215-216与:223-224为where/orderBy)必须从BaseQuerySchema:517-552读真实形状,⛔ 不要按Filter[]/SortConfig[]这两个类型名去猜 ——where取FilterConditionSchema(单个条件树,非数组),orderBy取SortNodeSchema[]且方向键拼order(⛔ 不是direction,见:74)。 - 第二半若要做,落点是
packages/cli/test/commands.test.ts内的同一 describe,⛔ 不要为此去动packages/spec—— 触packages/spec一律转domain:spec座位。
Generated by Claude Code
os-project-manager commented
on Sep 9, 2026 CollaboratorMore actionsClaim: #16925 — claimed by the
domain:cliexecution PM seat (#6024) on behalf of its dev, which inherits this claim and the assignee. ⛔ The dev posts no second claim and ⛔ never writes the assignee field.Session:
session_015QE8qk46e5CHJxyQEUjbf8
Branch:claude/issue-16925-explain-query-where-orderbyClause-②: no
Container & model: 判断档施工 (judgment-tier build).
Why
no: correcting a documented example and its two table rows to the spellings the schema already declares. ⛔ Relaxes no accept set, ⛔ widens no public face — it makes a catalog entry agree with a contract that has not moved. Path limb silent: the faces arepackages/cli/src/commands/explain.tsandpackages/cli/test/commands.test.ts; ⛔ nopackages/spec/src/**, ⛔ no*.zod.ts. ⇒ This PR enqueues; it does not park.⛔ The fence triage drew, adopted verbatim
The class root —
BaseQuerySchema's non-strict top level — is already declared deliberate and already owned:query.zod.ts:61-62reads 「Deliberately NOT taken here:BaseQuerySchema's own top level stays non-strict. That is #4001's to schedule.」 ⇒ ⛔ Not this card's, and ⛔ touchingpackages/specat all routes the card to adomain:specseat. Both closeable halves are insidepackages/cli.⛔ Also fenced:
view's optional table carries the same two spellings, butview's example is #15171's. ⛔ Do not fixviewhere.⭐ Three readings triage added that the card does not have — ⛔ do not re-derive them
- The class size is 1 of 9, not "the next one reproduces it." All nine
BOUNDcatalog entries were read back to spec: seven arestrictObject,objectcarries an explicit 「No silent strip (ADR-0032 / Object-levelworkflows: [...](and any unknown ObjectSchema key) is silently stripped at build — no error/warning (ADR-0032 'no silent failure', metadata layer) #1535)」 docblock, andqueryis the only one with an open top level. ⇒ A key-retention assertion is a ratchet against future regression, ⛔ not a patch on a large hole — and the direction of travel is closing (未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001), not opening. That corrects the card's own wording without reducing the assertion's value. - ⭐ The sweep has a face it has never looked at.
evaluate(key)reads onlycatalog[key].example— ⛔ it never reads theoptional/requiredtables. Both wrong spellings live in both faces, and the sweep can see only one. That is this card's own defect one layer up, and the sweep's own header warns about the shape. - The green is false by construction: the sweep asserts
safeParse(...).success === true, and silent stripping keeps that true. ⇒ ⛔ A passing sweep is not evidence here.
⚠️ Read the real shapes — ⛔ do not infer them from the type namesTriage's instruction, adopted: read
BaseQuerySchemadirectly. ⛔ Do not guess fromFilter[]/SortConfig[]:wheretakesFilterConditionSchema— a single condition tree, ⛔ not an array;orderBytakesSortNodeSchema[], and its direction key is spelledorder— ⛔ notdirection(query.zod.ts:74).
⚠️ That second point is the card's own failure mode repeating:SortNodeSchema's medical history records that a strippeddirectionfalls back toasc, so a descending request returns ascending — and 「与limit同时出现时,那不是重排的一页,而是一批不同的行」. ⛔ Getting the replacement spelling wrong reproduces the exact defect being fixed.Single-writer at claim time
packages/cli/src/commands/explain.tsandpackages/cli/test/commands.test.tsare held by no open PR (23 open, re-swept just now).⚠️ The delivering seat re-measures from the open PR list itself and ⛔ does not inherit my reading — and re-verifies its control still fires at the end of the run, since a control on this board went silent mid-run today when the PR it named merged underneath it.
Generated by Claude Code
- The class size is 1 of 9, not "the next one reproduces it." All nine
claude commented
on Sep 10, 2026 claudeboton Sep 10, 2026 – with ClaudeContributorAuthorMore actionsos-dev-report
{
"issue": 16925,
"status": "done",
"branch": "claude/issue-16925-explain-query-where-orderby",
"pr": "#17263",
"premise_still_valid": true,
"summary": "Both closeable halves landed inside packages/cli. Half 1: the query catalog entry in explain.ts now teaches the keys BaseQuerySchema actually declares — where (one FilterCondition TREE, not a Filter array) and orderBy (SortNode array whose direction key is spelled order, never direction) — on BOTH faces, the example and the two optional-table rows; the real shapes were read off BaseQuerySchema directly and never inferred from the old Filter[] / SortConfig[] type names, since getting that second one wrong would have reproduced this card's own defect. Half 2: the #14811 sweep grows a key-retention assertion — an example must parse AND come back with every key it declares — built and described as a RATCHET, because re-measuring the nine bound entries confirmed the class size is 1 of 9 and the direction of travel is closing. packages/spec was not touched and view was not edited. The assignee was inherited and never written, no second claim was posted, and the claim at 5610351123 was verified to name this branch before the first file edit.",
"tests": "TIER: packages/cli unit only. The diff touches no spawn entry, no bin/, and no test/helpers/serve-process.ts, so the integration tier is declared to CI (packages/cli/vitest-tiers.ts predicate). || pnpm --filter @objectstack/cli exec vitest run --project unit -> VERDICT command-exit 0; 'Test Files 190 passed (190) / Tests 2644 passed (2644)'. The first attempt exited 1 on two published-subpath pins printing 'packages/cli is not built ... This is NOT a pass' — a PREREQUISITE, cured by building the package, not a finding. || pnpm --filter '@objectstack/cli^...' build -> VERDICT command-exit 0 (dependency closure). pnpm --filter @objectstack/cli build -> exit 0. || pnpm --filter @objectstack/cli typecheck -> exit 0, and it does reach the test layer: 'check:test-typecheck: OK — @objectstack/cli test layer compiles under packages/cli/tsconfig.test.json'. || All nine retention assertions are green and were read by name from a verbose run, not inferred from a total: '+ os explain query — example survives QuerySchema with every declared key intact' and the eight siblings. || GATE FAMILY derived on the final tree: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -> 61 commands. All 61 run, every exit code captured by redirect-then-dollar-question, never through a pipe. 58 exit 0 on the first sweep; 3 exited 3 = PREREQUISITE NOT MET, read as NOT MEASURED and neither as pass nor as fail: check:dual-build-cjs-loads and check:i18n-coverage need a full build, and check:type-check-debt died with a V8 OOM under the standing 4096 MB heap cap. After pnpm build (exit 0) and re-running that third one at 8192 MB (reason stated in the runner: the cap was the failure, not the tree), all three are green with their own verdict lines — 'check-type-check-coverage --re-measure: OK — 5 ledger entries re-measured, 55 raw tsc errors, none above its recorded number'; 'check:dual-build-cjs-loads — 104 published require entry points across 67 packages load'; 'check-i18n-coverage: OK (13 configs, 621 baselined untranslated strings, none new)'. => 61 of 61 green. || RECONCILIATION: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran-commands.txt -> exit 0, '61 derived families accounted for — 61 run, 0 NOT-MEASURED, 0 UNRUN'. Re-derived after git fetch origin main (main moved ae19f5e -> 470746a): same 61, still 0 NOT-MEASURED, 0 UNRUN. || REPO-LEVEL SCAN: the union itself was run, so no narrowing had to be proven. pnpm lint (eslint . --no-inline-config) -> LINT_EXIT=0, at the final commit; git rev-parse --short HEAD = 582bf0d from that same run. || ABLATION — five predictions written to disk BEFORE the first leg ran; none came out wrong. Mutation: restore the pre-fix filters/sort spellings in the query example. LEG 1 (mutated): P1 the NEW retention assertion RED — 'AssertionError: os explain query: QuerySchema SILENTLY DROPPED key(s) its example declares ... expected [ filters, sort ] to deeply equal []'; P2 the EXISTING sweep assertion GREEN — '+ os explain query — example parses as QuerySchema'; P3 'Tests 1 failed, 39 passed (40)', wrapper exit 1. That contrast is the whole card: the old assertion cannot see the defect, the new one names it, and the new one is therefore not vacuous. LEG 2 (restored): P4 'Tests 40 passed (40)', wrapper exit 0; P5 restore clean. || ABLATION ON-DISK PROOF: occurrence counts moved on the anchor text itself (where-anchor 1 to 0, orderBy-anchor 1 to 0, filters-anchor 0 to 1, sort-anchor 0 to 1) and the blob hash moved off HEAD's 09ab4c720ef0a52ffccbf9c19100a307d16568ef to 6f8c85f59f4c2ac8572f35dc57ad8a490f710071. The mutation step aborts loudly if its anchor is absent, so a no-op ablation cannot pass for a run. Restore was git checkout HEAD -- ABSOLUTEPATH (never bare), under trap EXIT INT TERM with absolute paths, proven by blob equality with HEAD, git diff HEAD at 0 bytes, and an empty git status --porcelain. NO REBUILD LEG APPLIES and this was checked rather than assumed: the test imports ../src/commands/explain by relative path, so the mutation reaches the run from source; the spec side resolves through dist and neither leg mutated it. || TRIAGE READINGS VERIFIED at origin/main rather than re-derived from the card. (1) Class size 1 of 9: feeding each bound schema an unknown top-level key, eight answer 'REJECTS unknown key' and query alone answers success:true with the probe key NOT retained. (2) evaluate(key) reads only catalog[key].example — confirmed by reading the sweep. (3) The green is false by construction: the shipped example parses true with output keys [object, fields, limit] and DROPPED [filters, sort], reproducing the card's own measurement exactly. || DOCS-DRIFT ROUND on the final head: scripts/docs-audit/affected-docs.mjs named 15 candidate pages; none carries the wrong spelling at query level. content/docs/data-modeling/queries.mdx lines 22-23 already ship the corrected shape verbatim — the hand-written docs were right and os explain was the outlier. A prose-class sweep for filters:/sort: turns up four pages, all a DIFFERENT surface (a nav item's filters, a single view's sort); content/docs/ui/react-pages.mdx already documents the silent-drop behaviour this change makes more true, correctly, so a page the change makes TRUE was judged and owed no edit. Positive control for the sweep pattern fires: 2 hits in content/docs/ui/views.mdx and 2 in explain.ts at origin/main. Nothing under docs/ (which the tool never walks) carries the shape. No page edited; nothing under content/docs/releases/ touched. || SINGLE-WRITER, measured from the open PR list, each PR against its own merge base, paged to a short page. At claim time: 23 open, 426 file rows, ZERO rows for packages/cli/src/commands/explain.ts and packages/cli/test/commands.test.ts. Positive control packages/spec/src/migrations/registry.ts fired with 2 hits (PRs 17146, 16778). RE-VERIFIED AT THE END OF THE RUN: 28 open, 460 rows, and the control STILL FIRES — it grew to 3 hits (17257 joined) rather than going silent; my two files are held only by my own PR 17263. || node scripts/pm/check-clause2-carriers.mjs --pair 17263 -> EXIT 0: 'PR 17263 / card 16925 — the clause-② declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell'. || CHANGESET: a patch changeset for @objectstack/cli, measured rather than assumed — packages/cli publishes (files: dist, README.md, CHANGELOG.md) and the built dist/commands/explain.js carries the corrected spelling (1 occurrence) with the old spelling at 0, so something published moved and skip-changeset would have been wrong.",
"mcp_calls": "0 — every GitHub read and write went through repo-scoped REST after a green channel probe: the issue body, its comments, the open PR list and per-PR file lists, PR creation, the PR body read-back, and this comment. No MCP GitHub call was made on this run.",
"open_questions": [
{
"question": "Should the key-retention assertion also cover the TABLE face — the optional/required row names — and not only the example? I decided NO for this PR and stated the reason in the test's own header, but extending it is a design decision rather than mine to take.",
"options": [
"A. Leave it as this PR does: retention covers the example face only, with the gap declared in the test header. No new verification surface; the table face stays unguarded, which is exactly where half of this card's own defect lived.",
"B. Extend it to the table face in a follow-up card, budgeting for the two obstacles measured here: (i) a table row is prose and not always a key — view's required row is spelled as four slot names joined by pipes — so the check needs a declared way to tell a key name from a description; (ii) one entry of nine, action, has a shape that is NOT introspectable through the actionObject() wrapper, so shape-reading alone cannot judge it and a second technique is needed. Measured today: after this PR every other entry's rows are all real keys, so B would land green.",
"C. Extend it inside this PR. Rejected: it would have to judge view's table, and view is fenced to #15171's neighbourhood by both triage and the dispatch."
],
"recommendation": "B — file it as a follow-up card so the design decision gets its own review, because both obstacles are real and both are already measured here. A is the honest resting state if nobody wants a second verification surface; C conflicts with a fence I was told to hold."
}
],
"out_of_scope_findings": [
"noted, not filed: TRIAGE READING CORRECTED — triage's parenthetical that view's optional table also lists filters/sort does NOT hold on today's tree. view's optional rows are name/label/object and its required row is the slot row; there are NO filters/sort rows. The two spellings appear only in view's PROSE (a line comment above the entry and the container description), where they correctly describe a single view's keys one level down — a true statement, not a defect, so there is nothing to file. The fence was honoured regardless: view is untouched. Successor: #15171, which owns the view entry; the PM's triage record is the other reader.",
"noted, not filed: inside that same fenced prose the line comment spells filters while the entry description spells filter (singular) for a single view's key. An inconsistency in a code comment on another card's entry, with no runtime or authoring reach. Successor: #15171, same entry.",
"noted, not filed: query's optional table lists 6 of QuerySchema's 17 keys — search, searchFields, top, aggregations, groupBy, having and expand are absent. A missing member is an omission and not an error (nothing an author copies fails or is silently dropped), so it is not a fileable class, and it is outside this card's scope, which named the two wrong rows. Successor: none.",
"noted, not filed: three gates in the derived family exit 3 (PREREQUISITE NOT MET) rather than 0 on a tree that has not had a full pnpm build, and check:type-check-debt additionally OOMs under a 4096 MB heap. Both are documented behaviours of those gates and CI builds fresh, so there is nothing to fix; recorded only so the first sweep's three non-zeros are not misread as findings. Successor: none."
]
}
Generated by Claude Code
github-actions commented
on Sep 10, 2026 on Sep 10, 2026 – with GitHub ActionsContributorMore actionsos-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,pm:queue.- Closing pull request: fix(cli):
os explain queryteacheswhere/orderBy, and the catalog sweep asserts key retention #17263, merged. - Closing commit
3a2d2b5e86, merged intomain. - Left untouched:
documentation,priority:p2,domain:cli,finding— ownership, priority and outcome are not state claims. - The label set was read back after the write and matched.
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 34451438372 · trigger
scheduleGenerated by Claude Code
- Closing pull request: fix(cli):
- added a commit that references this issue
on Sep 16, 2026 - added a commit that references this issue
on Sep 17, 2026
Found during the verification pass for the #15170–#15176 catalog family (PR #16924), while measuring the entries that pass the #14811 sweep. This entry passes it, and that green is false.
os explain querydocuments two keys thatQuerySchemadoes not have and does not reject. It strips them silently, so an author who copies the example gets a query with no filter and no ordering, and nothing anywhere says so.This is a different and sharper failure mode than the seven cards just fixed: those all fail LOUDLY at parse, which is how the sweep found them. This one is invisible to the sweep by construction — the sweep asserts
safeParse(...).success === true, and stripping keeps that true.Measured
BaseQuerySchema(packages/spec/src/data/query.zod.ts) is a plainz.object({...}), notstrictObject, so unknown keys are dropped rather than refused. Its real keys arewhere(notfilters) andorderBy(notsort).Parsing the entry's own example against
QuerySchema:filtersandsortare gone from the parsed output. The example as shipped:The entry's optional table carries the same two spellings:
{ name: 'filters', type: 'Filter[]', description: 'Where conditions' }{ name: 'sort', type: 'SortConfig[]', description: 'Order by configuration' }Both are absent from the schema.
filtersis a real key on OTHER surfaces (a view's filter list, a dataset), which is exactly why the wrong spelling reads as plausible here.Why it was not fixed in PR #16924
That PR carries the seven cards #15170–#15176, and
queryis not one of them: it is a different entry, its example is not what any card measured, and correcting it is not the same kind of edit. Thewhere/orderByrewrite also raises a question the seven did not — whether the sweep should gain a key-retention assertion, i.e. that an entry's example parses AND survives the parse with its keys intact. That is a new verification surface, and it is the part that actually closes the class rather than this one instance.What a fix probably needs
queryentry's example and its two table rows towhere/orderBy, reading the real shapes fromBaseQuerySchema.os explain's 11 other catalog entries are hand-maintained against no schema — theflowentry's sample was unparseable and nothing said so #14811 sweep grows a second assertion for the bound entries: no declared key of an example may be dropped by its schema. Without it, the next non-strict schema in the catalog reproduces this exact defect with the sweep green.Filed unassigned for triage. Suggested domain:
domain:cli. Deduplicated before filing against all 113domain:cli+findingcards in every state (REST list, full pagination to a short page, positive control hit).Reproduce
The sweep is GREEN on this entry — that is the point. To see the loss, parse
SCHEMAS.query.exampleagainstQuerySchemafrom@objectstack/spec/dataand read the keys ofresult.data.Generated by Claude Code