Skip to content

fix(spec)!: refuse a dashboard globalFilters entry keyed dateRange beside a declared dateRange - #29370

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-0uv3au
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-0uv3au

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #29213

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — skills/objectstack-ui/rules/dashboards.md teaches dateRange as the reserved name of the built-in date range and no file under skills/** keys a globalFilters entry dateRange; every taught use stays true. content/docs/ui/dashboards.mdx gains one sentence naming the refusal.

维护者速读

  • 改了什么:仪表盘声明了内置日期范围 dateRange 时,globalFilters 里再写一个键为 dateRange 的筛选器(按 name,无 name 时按 field)现在会被拒收,报错落在该条目上并点名保留字。
  • 为什么改:以前这样写能保存,控制台和 lint 都让后写的定义覆盖内置日期范围,看板上日期选择器消失,所有绑定 dateRange 的组件被另一个字段过滤,只有浏览器控制台一条警告。
  • 风险与回滚:收紧契约,原本能通过的这类仪表盘会被拒;仓库内实测 0 个仪表盘这样写。回滚即还原本 PR 的两个提交。
  • 先看:packages/spec/src/ui/duplicate-global-filter-names.ts(新谓词与报错文案)、packages/spec/src/ui/dashboard.zod.ts(挂在仪表盘对象上的 refine)、packages/spec/dropped-refinements.baseline.json(+5 个站点)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/ui/dashboard-global-filter-reserved-date-range.test.ts,预期 11 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

  • packages/spec/src/ui/duplicate-global-filter-names.ts gains reservedDateRangeGlobalFilters(dateRange, globalFilters) beside duplicateGlobalFilterNames, sharing its filterKeyOf key derivation. I extended the module rather than adding a sibling because it is the same defect class: the built-in date range is a filter declared under the key dateRange, so an entry keyed dateRange is a second definition behind one key, exactly what the module's existing predicate refuses inside the array. The header no longer says dateRange "is not judged here".
  • packages/spec/src/ui/dashboard.zod.ts attaches refuseReservedDateRangeGlobalFilters to the dashboard OBJECT (strictObject(...).superRefine(...)), not to the array, because the verdict reads the sibling dateRange. One issue per entry, at globalFilters[i].name (or .field when unnamed), naming the reserved word and the built-in date range. The GlobalFilterSchema.name describe, the globalFilters describe and the filterBindings TSDoc now say the entry is refused, not only reserved.
  • The refusal fires only when dateRange is declared (any non-null object, the test both resolvers use). On a dashboard with no dateRange the key is free and an entry keyed dateRange stays legal (see Acceptance notes).
  • packages/spec/dropped-refinements.baseline.json: the build named five new sites (ui/Dashboard root and manifest.dashboards.element on the four installed-package projections); measured.droppedRefinementSites 988 → 993.
  • ADR-0087 semantic entry ui-dashboard-global-filter-reserved-date-range-refused (packages/spec/src/migrations/entries/semantic/), changeset .changeset/29213-dashboard-global-filter-reserved-date-range.md (minor, registered), regenerated content/docs/references/ui/dashboard.mdx, one sentence in content/docs/ui/dashboards.mdx.
  • Forced test edits: none. No existing test or fixture changed.

Why

Measured on main @ 5a2bdf9c before the change, with dateRange: { field: 'name' | 'close_date' } and globalFilters: [{ name: 'dateRange', field: … }]:

  • Parse: DashboardSchema.safeParse answered success: true for the named entry, the unnamed { field: 'dateRange' } entry and the undeclared case alike.
  • Save door: saveMetaItem (the PUT /api/v1/meta/dashboard handler, stub-engine harness of protocol.draft-publish-verdict.test.ts) answered success: true in draft (then publishMetaItem succeeded) and in publish; the row was stored active.
  • Console board: resolveDashboardFilterDefs (ui/packages/core) returned ONE def, {"name":"dateRange","field":"region","type":"text"}, plus the console warning duplicate filter name "dateRange" — the later definition wins. The date-range control was gone and every filterBindings.dateRange bound region. Lint's dashboardFilterDefs keeps the later definition the same way, and @objectstack/verify's window proof still proves the declared window the board no longer draws.
After the change (head commit)
  • Save door, draft and publish: INVALID_METADATA 422, one issue at globalFilters.0.name, nothing stored.
  • A row already stored with the pair still loads: _diagnostics.valid: false, one error at globalFilters.0.name carrying the refusal.
  • The probes were scratch tests, deleted after the run (no permanent file).

Tests

  • New packages/spec/src/ui/dashboard-global-filter-reserved-date-range.test.ts (11 cases): three controls (declared dateRange with another name; undeclared dateRange with an entry named dateRange; a named entry over a field called dateRange), the card reproduction located at .name with the message's first sentence, an empty dateRange: {}, an unnamed entry located at .field, two dateRange entries (one reserved issue each beside the duplicate-key issue), the refusal still answering when another key fails, and three predicate cases on stored-body shapes.
  • Ablation (committed fix first, scripts/ablation-replace.mjs, dashboard.zod.ts anchor }).superRefine(refuseReservedDateRangeGlobalFilters)); → }));): 5 failed | 6 passed (11), the five refusal cases red and the controls and predicate cases green; restored blob == HEAD, git diff HEAD empty. The spec suite imports ./dashboard.zod from source, so no dist was involved.
Package readings at head 6d7e238
  • pnpm --filter @objectstack/spec test: Test Files 792 passed (792), Tests 23402 passed | 1 todo.
  • pnpm --filter @objectstack/spec typecheck: exit 0.
  • pnpm --filter @objectstack/lint test: Test Files 189 passed (189), Tests 7830 passed.
  • pnpm --filter @objectstack/metadata-protocol test: Test Files 301 passed | 4 skipped (305), Tests 22127 passed | 26 skipped.
  • ui/: pnpm exec vitest run packages/core/src/utils packages/plugin-dashboard: Test Files 311 passed (311), Tests 3865 passed | 6 skipped.
  • Dogfood (full battery, after a turbo run build --filter=./packages/** closure build, under the lock): pnpm --filter @objectstack/dogfood test VERDICT command-exit 0, Test Files 391 passed | 3 skipped (394), Tests 3223 passed | 28 skipped (3251).

Acceptance notes

  • A globalFilters entry keyed dateRange on a dashboard that does NOT declare dateRange stays accepted. It is meaningful there: the console draws its own control under that name, filterBindings.dateRange binds it, lint's dashboard-filter-binding-unknown accepts the key against it, and verify's window proof skips the dashboard. Refusing it unconditionally would widen the refusal past the card; it is an open question to the seat.
  • Blast radius: zero authored dashboards key a global filter dateRange (git grep over examples, apps, packages, ui for name:/field: spelled dateRange: 5 hits, none a globalFilters entry; 115 files mention globalFilters).
  • Mirror reach: the rule sits on the dashboard object, so a schema that spreads only DashboardSchema.shape.globalFilters keeps the duplicate-key rule and not this one. Every in-repo door parses DashboardSchema whole (the dropped-refinements build names the four manifest projections that carry it).
  • The v18 stored-row listing (packages/spec/src/conversions/refused-shapes.ts) does not list this refusal; it is the sibling card spec(dashboard): the duplicate globalFilters key refusal (#29023) has no stored-row detector and no save-door pin #29212's surface and was not touched here.

Verification

Head 6d7e2382. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 106 families from 8 paths (331 changed lines); each ran after the last commit with its exit code recorded, and --ran answered: 106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN (exit 0).

Gate notes
  • node scripts/check-plugin-teardown-shape.mjs --self-test first answered exit 3 (PREREQUISITE NOT MET: its pinned positive-control commit was absent from the shallow clone); after git fetch --depth=1 origin 621a4876 it answered exit 0, 48 cases.
  • pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts up to date (after gen:docs regenerated content/docs/references/ui/dashboard.mdx).
  • node scripts/check-changeset-no-major.mjs --base 5a2bdf9c --event (this body, offline): exit 0, level axis no (narrowing) / narrowed, no patch grading. check-partof-closing-keyword.mjs on this body: exit 0.
  • Local-only node scripts/check-dev-prereqs.mjs: exit 1, "the workspace is not built", naming 35 ui/ workspace packages with no dist/ (only ./packages/** was built here). It judges this container's dev-server precondition, not the spec dist, so it is reported and not acted on.
  • CI families (Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, the UI jobs) are read from this PR's check-runs, not re-run here.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint is CI-owned and was not run locally.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…rd that declares dateRange

The built-in date range is declared under the reserved key `dateRange`; an
entry keyed `dateRange` beside it parsed and replaced the date range's
control and binding in the console and lint resolvers.

Claude-Session: https://claude.ai/code/session_01ADHkmNvZN4TxxGwnpRp9yG
Co-authored-by: Claude <noreply@anthropic.com>
…nces, baseline +5 sites

Semantic ADR-0087 entry ui-dashboard-global-filter-reserved-date-range-refused,
the changeset, the dropped-refinements baseline (the dashboard-level refine
adds ui/Dashboard root and four installed-package manifest projections) and
the regenerated dashboard reference; one docs sentence naming the refusal.

Claude-Session: https://claude.ai/code/session_01ADHkmNvZN4TxxGwnpRp9yG
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 10 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/dropped-refinements.baseline.json, packages/spec/export-origins/ui.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-api.mdx (via dateRange (literal, a string literal in RESERVED_DATE_RANGE_FILTER_NAME; a string literal in a comment in GlobalFilterSchema))
  • content/docs/data-modeling/analytics.mdx (via DashboardWidgetSchema (symbol, a top-level const))
  • content/docs/deployment/cli.mdx (via dateRange (literal, a string literal in RESERVED_DATE_RANGE_FILTER_NAME; a string literal in a comment in GlobalFilterSchema))
  • content/docs/deployment/validating-metadata.mdx (via dateRange (literal, a string literal in RESERVED_DATE_RANGE_FILTER_NAME; a string literal in a comment in GlobalFilterSchema), globalFilters (literal, a string literal in checkDashboardReservedDateRangeGlobalFilters))
  • content/docs/frontend/plugins/plugin-dashboard.mdx (via DashboardWidgetSchema (symbol, a top-level const))
  • content/docs/getting-started/quick-reference.mdx (via DashboardSchema (symbol, a top-level const))
  • content/docs/ui/dashboards.mdx (via dateRange (literal, a string literal in RESERVED_DATE_RANGE_FILTER_NAME; a string literal in a comment in GlobalFilterSchema), globalFilters (literal, a string literal in checkDashboardReservedDateRangeGlobalFilters))
  • content/docs/ui/reports.mdx (via dateRange (literal, a string literal in RESERVED_DATE_RANGE_FILTER_NAME; a string literal in a comment in GlobalFilterSchema))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via DashboardSchema (symbol, a top-level const), DashboardWidgetSchema (symbol, a top-level const), GlobalFilterSchema (symbol, a top-level const), dateRange (literal, a string literal in RESERVED_DATE_RANGE_FILTER_NAME; a string literal in a comment in GlobalFilterSchema))
  • content/docs/releases/v16.mdx (via DashboardWidgetSchema (symbol, a top-level const), dateRange (literal, a string literal in RESERVED_DATE_RANGE_FILTER_NAME; a string literal in a comment in GlobalFilterSchema), globalFilters (literal, a string literal in checkDashboardReservedDateRangeGlobalFilters))
  • content/docs/releases/v17/17-4.mdx (via dateRange (literal, a string literal in RESERVED_DATE_RANGE_FILTER_NAME; a string literal in a comment in GlobalFilterSchema))
  • content/docs/releases/v17/17-5.mdx (via dateRange (literal, a string literal in RESERVED_DATE_RANGE_FILTER_NAME; a string literal in a comment in GlobalFilterSchema))
  • content/docs/releases/v17/17-6.mdx (via globalFilters (literal, a string literal in checkDashboardReservedDateRangeGlobalFilters))
  • content/docs/releases/v17/17-7.mdx (via dateRange (literal, a string literal in RESERVED_DATE_RANGE_FILTER_NAME; a string literal in a comment in GlobalFilterSchema))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/dropped-refinements.baseline.json, packages/spec/export-origins/ui.json) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 7f277a86f670a956dcdf677937c1894706992bee → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e9688516cc2998ea7b4479b45755abd693452d36 — the merge of head 6b7710c9c0e97e31bc478da45b74fa5a3644acfd into base 7f277a86f670a956dcdf677937c1894706992bee, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e9688516cc2998ea7b4479b45755abd693452d36 && git checkout e9688516cc2998ea7b4479b45755abd693452d36
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7f277a86f670a956dcdf677937c1894706992bee 6b7710c9c0e97e31bc478da45b74fa5a3644acfd && git checkout -B drift-repro 7f277a86f670a956dcdf677937c1894706992bee && git merge --no-ff 6b7710c9c0e97e31bc478da45b74fa5a3644acfd

node scripts/docs-audit/affected-docs.mjs --json 7f277a86f670a956dcdf677937c1894706992bee

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 7f277a86f670a956dcdf677937c1894706992bee → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Contract-review-claim: session_01ADHkmSduNGC2SZXszyrNut · head 6d7e238 · 2026-11-02T13:24Z

The lane seat reviews this head itself at CONTRACT_REVIEW_TIER (triage ruling 6575141496 on #28741 returned contract review to the lane): a narrowing of a published schema (DashboardSchema refuses a globalFilters entry keyed dateRange).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6d7e23820072973d6a94ed50aea519010772cf1d
Local-runs: none
Scope: packages/spec/src/migrations/entries/semantic/18.ui-dashboard-global-filter-reserved-date-range-refused.ts, packages/spec/src/ui/dashboard.zod.ts, packages/spec/src/ui/duplicate-global-filter-names.ts + packages/spec/src/ui/dashboard-global-filter-reserved-date-range.test.ts, packages/spec/src/shared/strict-object.ts, packages/spec/src/shared/lazy-schema.ts, packages/spec/src/kernel/metadata-type-schemas.ts, packages/spec/src/stack.zod.ts, packages/spec/src/migrations/entries/semantic/18.ui-dashboard-global-filter-duplicate-name-refused.ts, packages/spec/src/migrations/types.ts, packages/spec/src/migrations/registry.ts.template, packages/spec/src/conversions/refused-shapes.ts, packages/spec/dropped-refinements.baseline.json, .changeset/29213-dashboard-global-filter-reserved-date-range.md, .changeset/29023-dashboard-global-filter-duplicate-name.md, content/docs/ui/dashboards.mdx, skills/objectstack-ui/rules/dashboards.md, packages/lint/src/validate-widget-bindings.ts, packages/lint/src/rule-explanations.ts, packages/verify/src/dashboard-window.ts, ui/packages/core/src/utils/dashboard-filters.ts, ui/packages/types/src/zod/complex.zod.ts, ui/packages/types/src/zod/imported-defaults.ts, ui/packages/types/src/zod/node-derivation.ts, ui/packages/types/src/zod/base.zod.ts, ui/packages/app-shell/src/views/metadata-admin/clientValidation.ts, ui/packages/app-shell/src/views/studio-design/metadataError.ts, scripts/check-adr-0087-registration.mjs

Inputs: card #29213 (body, triage 6574201278, claim 6576684400, os-dev-report 6578177651), PR #29370 (body, 8-file list, net diff against merge base 5a2bdf9c), the 62 check-runs on the head. Read-only: the cited code was read on the head tree with git show; nothing was built, run or re-run. The seat's rulings Q1 = A and Q2 = A and the amended claim surface (the ADR-0087 entry and the one dashboards.mdx sentence) are judged below as decisions, not adopted as conclusions.

Gate state on the head at review time: completed success for Lint & Repo Gates (and every Lint · and Type Check · source gates / debt ledger job under it, including Lint · PM dispatch-gates), PR-body guards, Check Changeset, Check PR Size, Governed Surface Queue Guard, Spec property liveness, Build Docs, Check Documentation Links, Dogfood Regression Gate, Live E2E UI, Build UI Deps, Plan Test Core shards, Auto Label, Flag docs affected by code changes, filter. Still in progress (PENDING, not read as verdicts): Build Core, Test Core (8 shards), Type Check · workspace, Type Check · consumer gates, Type Check UI, Test UI (16 shards), Build & E2E UI. Skipped by their own filters: Lint UI, UI Published Artifacts, the opt-in harnesses. The combined commit status is pending. The design is judged below regardless; the pending families are the dev's locally reported spec/lint/metadata-protocol/ui suites and the dogfood battery, which this record does not re-run.

① Derived judgments

  1. Accept-set narrowing of DashboardSchema — right. A dashboard whose dateRange is a declared object and whose globalFilters[i] is keyed dateRange (non-empty name, else non-empty field) now fails to parse, one custom issue per such entry at globalFilters[i].name or .field. Every in-repo door parses DashboardSchema whole: metadata-type-schemas.ts maps dashboard to it (the save door, draft and publish), stack.zod.ts uses it as the dashboards element (defineStack, os validate, os build), verify/dashboard-window.ts calls DashboardSchema.safeParse, and the console's clientValidation.ts loads the spec DashboardSchema for the dashboard editor. The changeset's "every door that parses it" is true of the tree.
  2. Conditional on a declared dateRange (Q1 = A) — right. The console's resolveDashboardFilterDefs seeds byName with the built-in only if (schema.dateRange), then inserts every globalFilters entry under f.name || f.field with its own type; the filter bar and the query side branch on def.type ('dateRange' vs 'date'/'text'), never on the key. Lint's dashboardFilterDefs seeds the built-in only when dateRange is a non-null object and otherwise keys entries by name else field, so filterBindings.dateRange is a declared key when such an entry exists. Verify marks a dashboard without dateRange not-windowed and skips it. So on an undeclared dashboard the entry is a working filter, and refusing it would take a capability away that no consumer breaks on. Adding dateRange later turns the pair into the refused shape, which is the loud failure the card asked for.
  3. Key derivation shared with the duplicate rule (Q2 = A) — right. reservedDateRangeGlobalFilters reuses the module-private filterKeyOf (non-empty name, else non-empty field), the same derivation the console (f.name || f.field) and lint use. Exact, case-sensitive match on 'dateRange' is what both resolvers do. Keeping it in duplicate-global-filter-names.ts makes the module's header true again (it no longer says dateRange "is not judged here"); the predicate is a second collision on the same key space, not a new class.
  4. "Declared" means any non-null object — right. Lint tests dateRange && typeof dateRange === 'object'; the console tests truthiness. A string dateRange is refused by the schema's own invalid_type before the object check runs, so the predicate's [] for non-objects is the "left to the schema" posture the sibling predicate takes, and on a stored body it cannot mask anything the schema does not already name.
  5. Attachment on the dashboard object, not the array — right, and its reach is named correctly. The verdict needs the sibling dateRange, so an array-level refine cannot carry it. The PR's acknowledged consequence holds in the tree: the UI mirror DashboardComponentSchema (ui/packages/types/src/zod/complex.zod.ts) excludes globalFilters from the spec fields it spreads and redefines it from the UI's own GlobalFilterSchema, so neither fix(spec)!: a dashboard that declares two global filters under one name is refused #29162's array rule nor this object rule reaches the SDUI validateTree path. That was already the posture after fix(spec)!: a dashboard that declares two global filters under one name is refused #29162; the save doors and the console editor parse the spec schema whole, so the refusal reaches every write.
  6. No published-export change. reservedDateRangeGlobalFilters, RESERVED_DATE_RANGE_FILTER_NAME and ReservedDateRangeGlobalFilter are new exports of duplicate-global-filter-names.ts, but that module is not re-exported from packages/spec/src/ui/index.ts (only dashboard.zod and dashboardForm are) and appears nowhere in api-surface/ or export-origins/. refuseReservedDateRangeGlobalFilters is module-private. The public surface change is the narrowing plus describe prose on GlobalFilterSchema.name and DashboardSchema.globalFilters, which the regenerated content/docs/references/ui/dashboard.mdx reflects at both sites where GlobalFilterSchema is printed.
  7. Dropped-refinements baseline +5 — right. "" is the baseline's spelling for a root-level object refinement (ui/ListView, automation/Flow, api/CreateFlowRequest all carry it), and manifest.dashboards.element on the four installed-package projections is where stack.zod.ts embeds the schema. 988 → 993 matches five sites. The two swapped translations lines are a sort-order normalisation the generator emitted; not a hits file, listed here only because the hunk is in the diff.
  8. ADR-0087 registration — right. The changeset carries adr-0087: registered ui-dashboard-global-filter-reserved-date-range-refused; the entry file 18.ui-dashboard-global-filter-reserved-date-range-refused.ts exports entry with that id; the registry is generated from entries/semantic/ by build-migration-registry.ts (no hand edit of the template is needed or made). Lint & Repo Gates is green on the head.
  9. refused-shapes.ts untouched — right for this claim. Neither this refusal nor fix(spec)!: a dashboard that declares two global filters under one name is refused #29162's duplicate-key refusal is listed there; the claim comment fences that file to sibling spec(dashboard): the duplicate globalFilters key refusal (#29023) has no stored-row detector and no save-door pin #29212, and the dev's out_of_scope_findings hands it to that carrier.

② Semver level

.changeset/29213-dashboard-global-filter-reserved-date-range.md bumps @objectstack/spec minor under fix(spec)!: with Clause-②: no (narrowing) and Metadata-protocol: narrowed; the PR body carries the same three lines verbatim. That is what the diff publishes: an accept-set narrowing of a published schema with no exported-symbol change, which check-changeset-no-major.mjs grades minor with the (narrowing) marker during the launch window (its header: a breaking change ships as minor; narrowed beside a line without (narrowing) is refused). The sibling #29162 changeset (29023-dashboard-global-filter-duplicate-name.md, still pending in .changeset/) uses the identical level axis, so the two refusals of one defect class publish at one level. The Skills-docs: none line is true of the tree: skills/objectstack-ui/rules/dashboards.md teaches dateRange only as the reserved built-in (lines 12, 162, 213, 396) and no skills/** file keys a globalFilters entry dateRange. Check Changeset is green on the head.

Clause-②: no (narrowing) — correct for this diff.

③ Boundary flags

Dev deviations (os-dev-report 6578177651):

  • Two files outside the original claim surface (the ADR-0087 semantic entry; one sentence in content/docs/ui/dashboards.mdx): the seat amended the claim to include both. The entry is required by the changeset's registered disposition and is judged under ④. The sentence is accurate against the code (refusal conditional on a declared dateRange, keyed by name else field, names the control and the filterBindings.dateRange binding it would replace); see should-fix 1 for a wording tension with the sentence above it. Accepted.
  • PR body lines 1–2 are the harness attribution block, Fixes #29213 on line 3: procedural, PR-body guards green. Not a contract matter.
  • No MCP writes, no PR assignee, no PR-activity subscription: the standing order; nothing to escalate.
  • Human Minimum item 3 (pnpm lint) unchecked, CI-owned: Lint · ESLint and Lint · docs & code guards are green on the head. Closed.

Open questions:

  • Q1 (undeclared-dateRange dashboard keeps an entry keyed dateRange): seat ruled A. Judged right under ① item 2; the alternative would widen the refusal past the card and past what any consumer breaks on.
  • Q2 (predicate placement in duplicate-global-filter-names.ts): seat ruled A. Judged right under ① item 3.

Out-of-scope note (refused-shapes.ts does not list this refusal): correctly routed to #29212, which holds that file.

Escalations: none.

④ Design judgment

packages/spec/src/ui/duplicate-global-filter-names.ts — against the card's scenario and both resolvers. Tried to break it: { name: 'dateRange', field: 'close_date' } is keyed by name and refused at .name (the card's shape); { field: 'dateRange' } unnamed is keyed by field and refused at .field with the parenthetical the console's fallback justifies; { name: 'period', field: 'dateRange' } is keyed period and passes, which is right because the console keys it period too; { name: '', field: 'dateRange' } falls to the field exactly as f.name || f.field does; targetWidgets on the entry does not rescue it, right because the filter-bar control key collides regardless of which widgets it targets; two dateRange entries answer one reserved issue each plus the array rule's one duplicate issue, so the author sees every site. dateRange: 'this_month' and null answer [], which the schema already refuses by type. The message carries no tracker number (the check:doc-authoring rule the test pins). The header now states the module's two predicates and their shared key space truthfully. No finding.

packages/spec/src/ui/dashboard.zod.ts — against its consumers. strictObject(...) returns a closedObject (a subclass instance); Zod 4's .superRefine clones through inst.constructor(def), the same construction the UI's cloneWithDef relies on, so the surface-naming error map and terminal unknown-key refusal survive the attachment, and the result is still a ZodObject: SpecDashboardSchema.shape.dateRange (ui/packages/core/src/utils/dashboard-filters.ts), DashboardSchema.shape.requiredPermissions and the UI mirror's stripImportedDefaults(SpecDashboardSchema).shape keep working, since stripImportedDefaults walks by _zod.def and clones (it never calls .extend()), and specFieldsExcept builds a fresh looseObject(...).partial() from the shape. I searched the whole tree for DashboardSchema.extend/omit/pick/partial/merge and found no site, so Zod 4's refusal to .extend() a refined object is not reachable from this change. The lazy proxy forwards the refined instance unchanged. z.input / z.output types are unchanged. The studio-design/metadataError.ts listItemHeldInput walk sees the new issue at globalFilters.i.name (or .field) with that input set, so it classifies the item as not held and the refusal reaches the author as an ordinary field error rather than a "fill this in" prompt, which is the right reading. One observation, not a finding: Zod 4 skips object-level checks after an aborting child issue (an invalid_type anywhere in the dashboard, or an unrecognized key on the zod 4.4 line), so the refusal does not accompany such failures in one parse; the new test's "another key fails" case uses columns: 99, a continuable max check, which is why it holds. This is the same posture as #29162's array refine and every superRefine in the package, and the author gets the reserved-name issue on the next parse once the type error is fixed. Describe prose on GlobalFilterSchema.name and globalFilters says what the code now does and is regenerated at both reference sites. No finding.

packages/spec/src/migrations/entries/semantic/18.ui-dashboard-global-filter-reserved-date-range-refused.ts — against its first consumer (the generated registry, os migrate meta, the upgrade guide) and the sibling entry. id matches the changeset marker and the file name; surface, replacement, reason and acceptanceCriteria are filled in the sibling's register; acceptanceCriteria quotes the message's first sentence exactly as the test asserts it, so the guide and the door agree; the stored-row account (loads with _diagnostics.valid: false, next save refused, nothing rewrites) matches the "no conversion" stance and the dev's measured save-door behaviour; conversionIds and relevantWhen are omitted as in the sibling (both optional; relevantWhen is a reviewed addition enumerated by semantic-relevance.test.ts, and neither dashboard entry carries it). Not convertible is the right call: which of the two definitions the author meant is not derivable. No finding.

Amended claim surface. content/docs/ui/dashboards.mdx sentence: accurate (see should-fix 1). The ADR-0087 entry: judged above.

Should-fix (non-blocking):

  1. content/docs/ui/dashboards.mdx keeps "the name dateRange is reserved for the built-in date range" unqualified immediately before the new sentence that says the key is refused only when dateRange is declared; the GlobalFilterSchema.name TSDoc says the key "is free" on a dashboard with no dateRange. A reader holds both. Qualifying the first sentence ("reserved for the built-in date range when the dashboard declares one") would make the prose say one thing. Prose only; the code is consistent.
  2. Follow-up outside this PR's surface: packages/lint/src/rule-explanations.ts (dashboard-filter-binding-unknown, around line 408) says a dateRange binding key on a dashboard that declares no dateRange "is reported the same way: there is no date range for it to bind". With Q1 = A now written into the spec, that sentence is false when a globalFilters entry named dateRange exists (lint's own dashboardFilterDefs accepts the key then). Lint's file, not this claim's; worth a card so the explanation matches the ruling.

Must-fix: none.

Implemented-by: claude/project-thread-0uv3au
Reviewed-by: session_01ADHkmSduNGC2SZXszyrNut

VERDICT: PASS

…es dateRange

Contract-review should-fix: the dashboards page and the GlobalFilterSchema.name
TSDoc now say one thing. The name is reserved, and an entry keyed dateRange
refused, only when the dashboard declares the built-in date range; otherwise
the name is free. Prose only, no behaviour change.

Claude-Session: https://claude.ai/code/session_01ADHkmNvZN4TxxGwnpRp9yG
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0aa87c6cf98d65a4fb8eb8634ff30cf03c2e0394
Local-runs: none

This is a DELTA record for the push after the PASS record 6578522383 (head 6d7e2382). The delta 6d7e2382...0aa87c6c touches two files, prose only. In content/docs/ui/dashboards.mdx the sentence "the name dateRange is reserved for the built-in date range" no longer stands unqualified: it now says the name is reserved on a dashboard that declares dateRange, and is free (an ordinary filter) on one that does not. In packages/spec/src/ui/dashboard.zod.ts the GlobalFilterSchema.name JSDoc is reworded the same way. That is Should-fix 1, done as asked. The .describe() string, the refinement, the ADR-0087 entry, the changeset and the tests are byte-identical, so every judgment in 6578522383 (semver, Clause-②: no (narrowing), Metadata-protocol: narrowed) carries to this head unchanged.

Implemented-by: claude/project-thread-0uv3au
Reviewed-by: session_01ADHkmSduNGC2SZXszyrNut

VERDICT: PASS

…rs and chain it on the console's dashboard mirror

The census in spec-object-refinements-7715 counts every object-level check
on DashboardSchema: rebuilding DashboardComponentSchema from the spec's
shape dropped the new reserved-dateRange check, so the console's authoring
door accepted a dashboard the spec refuses. Export it in the house check*
shape, chain it on DashboardComponentSchema, name it in the census row,
and add @object-ui/types to the changeset.

Claude-Session: https://claude.ai/code/session_01ADHkmNvZN4TxxGwnpRp9yG
Co-authored-by: Claude <noreply@anthropic.com>
…oardReservedDateRangeGlobalFilters

Claude-Session: https://claude.ai/code/session_01ADHkmNvZN4TxxGwnpRp9yG
Co-authored-by: Claude <noreply@anthropic.com>
…the import-boundary refinement list

Claude-Session: https://claude.ai/code/session_01ADHkmNvZN4TxxGwnpRp9yG
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

待裁:新公开名字 checkDashboardReservedDateRangeGlobalFilters(#29213)

  • 一句话问题:看板里一个全局筛选器占用 dateRange 这个 key 时,发布门会拒收,但 objectui 的编辑门原先放行,作者要到发布时才发现。修法是把这条检查导出成公开函数,让 objectui 镜像也挂上它。这个名字一旦发布就是公开契约。
  • 背景:ui 的 docs(qa): reconcile the i18n checklist items with the shipped contract (#7686, #7684) #7715 对等普查(Test UI)因为这条检查未导出而变红;导出并挂到 objectui DashboardComponentSchema 之后,两道门的结果一致。
  • 选项:
    • A 保留 checkDashboardReservedDateRangeGlobalFilters:零额外工作。与同文件 checkDashboardWidgetStageOrder、checkGlobalFilterDateDefaultValue 同一命名法(check + 对象 + 规则)。
    • B 改个更短的名字:开发线程改名、重新生成 api-surface/export-origins,ui 镜像和普查同步改,复审出 delta,约半小时。
  • 推荐:A。理由是 objectui 的普查按 check* 前缀读取 spec 的导出,这个名字与已挂上的三条检查同族。盲区:名字偏长。
  • 自检:只看①选 A;②③④ 是否翻转:否。
  • 落地:在此卡标签取下前,席位不挂自动合并。

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6b7710c9c0e97e31bc478da45b74fa5a3644acfd
Local-runs: none

This is a DELTA record for the pushes after the delta PASS 6578639267 (head 0aa87c6c). The delta 0aa87c6c...6b7710c9 touches 7 files. It fixes the real Test UI (6/16) red on 0aa87c6c: the objectui#7715 census at spec-object-refinements-7715.test.ts:488 counted one object-level check on DashboardSchema that no mirror site named.

  • packages/spec/src/ui/dashboard.zod.ts: the private refuseReservedDateRangeGlobalFilters is renamed and exported as checkDashboardReservedDateRangeGlobalFilters, in the file's own check* (value, ctx) idiom (checkDashboardWidgetStageOrder, checkGlobalFilterDateDefaultValue). The body, the predicate it calls (reservedDateRangeGlobalFilters), the message and the issue path are byte-identical. DashboardSchema chains the same function. The accept set is unchanged, so every judgment in 6578522383 holds.
  • Public surface: one new function export, recorded in api-surface/ui.json and export-origins/ui.json, with its origin at dashboard.zod.ts (no dual source). It makes the narrowing's own check re-attachable and widens no accept set, so Clause-②: no (narrowing) and Metadata-protocol: narrowed stand (claim 6576684400 amended). The name is a new public name and is with the maintainer (needs-user-decision, comment 6579980182).
  • ui/packages/types/src/zod/complex.zod.ts: DashboardComponentSchema chains .superRefine(checkDashboardReservedDateRangeGlobalFilters), so objectui's authoring door now refuses what the spec's publish door refuses. That is the objectui#7715 parity the census enforces. No caller in ui/** or packages/** derives from DashboardComponentSchema or DashboardSchema with .omit / .pick / .partial / .extend (grep on the head), so the refinement breaks no derivation. The changeset says the same for DashboardSchema.
  • The two ui pins: the census row attached: ['checkDashboardReservedDateRangeGlobalFilters'] and the imported-defaults-8317 REFINEMENT_EXCEPTIONS row are each the one-line registration that their harness asks of a re-attached spec check, as with checkDashboardWidgetChartMeasureArity.
  • Changeset: gains '@object-ui/types': minor (the mirror refuses more, beside the spec's breaking narrowing), and its prose names the export and the mirror. That is right.

Out of scope, already filed: the mirror redeclares globalFilters and so drops #29162's duplicate-name refine, which is #29284.

Check-runs on 6b7710c9: ci-failure.mjs GREEN, 65 check-runs, none failed (5 superseded rows not read).

Implemented-by: claude/project-thread-0uv3au
Reviewed-by: session_01ADHkmSduNGC2SZXszyrNut

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Head-sha: 6b7710c9c0e97e31bc478da45b74fa5a3644acfd · read 2026-11-02T14:46Z

维护者速读

  • 改了什么:看板声明了内置日期范围 dateRange 时,再有一个全局筛选器也叫 dateRange,现在每道门都会拒收(os validate、构建、保存接口 422,以及 objectui 的编辑门)。没声明 dateRange 的看板照常可以用这个名字。
  • 为什么改:之前这种看板能保存,控制台会用这个筛选器顶替内置日期范围,所有绑定 dateRange 的组件被静默改成按它过滤,只在浏览器控制台留一条警告。
  • 风险与回滚:这是收紧(BREAKING,已登记 ADR-0087 迁移条目和升级指南)。已存的这种看板仍能读出,标 valid: false,下次保存才被拒,不会被自动改写。仓库内无此用法。回滚就是 revert 本 PR。
  • 先看:packages/spec/src/ui/dashboard.zod.ts 的 checkDashboardReservedDateRangeGlobalFilters,以及 ui/packages/types/src/zod/complex.zod.ts 末尾的挂接。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/ui/dashboard-global-filter-reserved-date-range.test.ts,预期全部通过。
  • 需要你做的:定新公开名字 checkDashboardReservedDateRangeGlobalFilters(本 PR 已挂 needs-user-decision,分析见评论 6579980182,推荐保留)。定了之后席位落地。

席位意见:内容可落地,等名字裁定。契约复审 PASS(6578522383 @ 6d7e2382,delta 6578639267 @ 0aa87c6c,delta 在现 head),CI 全绿。

席位核对 — ACCEPT · domain:spec#2 · head 6b7710c9

  1. Shape: draft, base main, Fixes #29213. Clause-②: no (narrowing) and Metadata-protocol: narrowed sit at line start in the body and the changeset (@objectstack/spec minor with a ! subject and a BREAKING section, @object-ui/types minor). There are 13 files, +342/−20, all inside claim 6576684400 as amended twice on the seat's CI readings (the ADR-0087 entry, dashboards.mdx, the ui mirror and its two pins). No governed path.
  2. Premise measured: on origin/main after 38a278b7, a dashboard declaring dateRange with a globalFilters entry keyed dateRange parses, and the save door answers 200. On the head, the new test file refuses it at the entry's key in each door's form, and its CONTROL rows keep the undeclared case and a named entry with field: 'dateRange' legal. The dev probed objectui's DashboardComponentSchema: it refuses the pair at globalFilters.0.name with the spec message and accepts the undeclared case.
  3. Gates: ci-failure.mjs --sha 6b7710c9… GREEN: 65 check-runs, none failed. The earlier reds were the real Test UI (6/16) census at 0aa87c6c (fixed by the export) and check:export-origins at bb0e318e (fixed at fc71bedb). The rest were shards cancelled by a newer push.
  4. Accepted deviations: the forced ui edits (the mirror's .superRefine and its two harness rows) are the objectui#7715 parity rule's own requirement. The @object-ui/types changeset line follows from them.
  5. Out of scope: lint(dashboard-filter-binding-unknown): its explanation says a dateRange binding on a dashboard with no dateRange is always reported, but a globalFilters entry named dateRange is a declared filter there #29382 (the lint rule explanation's dateRange sentence) and ui(types): DashboardComponentSchema rebuilds globalFilters itself, so spec's duplicate-filter-key refusal (#29023) does not reach the published ui mirror #29284 (the mirror drops fix(spec)!: a dashboard that declares two global filters under one name is refused #29162's duplicate-name refine) are filed. The stored-row detector for this refusal (the spec(dashboard): the duplicate globalFilters key refusal (#29023) has no stored-row detector and no save-door pin #29212 shape) is filed after this lands, together with the dashboards: two globalFilters with the same name are accepted; the board draws one control and its value filters the other filter's field #29023 D3 acceptanceCriteria sentence. The Skills-docs: none reason holds: skills/objectstack-ui/rules/dashboards.md teaches dateRange only as the built-in's name.
  6. Landing: not Tier H and under 3000 lines. It waits for the needs-user-decision name ruling. Once that label is off with the name kept, the seat runs pr_ready + auto-merge (a rename means a new head and a fresh delta first). After merge, the seat writes a landing note on spec(dashboard): a globalFilters entry named dateRange parses and silently replaces the built-in date range's binding #29213, removes pm:dispatched and tells spec(dashboard): the duplicate globalFilters key refusal (#29023) has no stored-row detector and no save-door pin #29212's follow-up.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Withdrawn: the name question on this PR. The pm-dispatch skill carries no rule that a new public name needs the maintainer's approval; that line lived only in this seat's standard order. Under SKILL.md 〈升级与决策〉 a name escalates only when the options truly diverge on product semantics or contract shape and no existing norm decides; here the dev's name is fit and no fork exists, so it is the seat's call: the name stays as built. needs-user-decision is removed and the seat lands this PR on its ACCEPT.

Prompted by the maintainer's question at 2026-11-02T17:03Z.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 17:15
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 41489494232 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (2/8) — 失败步骤: Run this shard's tests

    @objectstack/spec:test:repo:  FAIL   repo  scripts/strictness-ledger-doc.test.ts > the ledger and the generated counts agree > is checked in current — every shard on disk equals a fresh render, and no
      ↳ 失败原因: @objectstack/spec:test:repo: AssertionError: expected Map{ …(14) } to deeply equal Map{ …(14) }
    @objectstack/spec:test:repo:  FAIL   repo  scripts/strictness-ledger-doc.test.ts > the ledger and the generated counts agree > the read-time totals equal the sums of the shards' own rows
      ↳ 失败原因: @objectstack/spec:test:repo: AssertionError: expected 'untriaged: 1293 object site(s) across…' to be 'untriaged: 1292 object site(s) across…' // Object.is equality
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 41491860190 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (2/8) — 失败步骤: Run this shard's tests

    @objectstack/spec:test:repo:  FAIL   repo  scripts/strictness-ledger-doc.test.ts > the ledger and the generated counts agree > is checked in current — every shard on disk equals a fresh render, and no
      ↳ 失败原因: @objectstack/spec:test:repo: AssertionError: expected Map{ …(14) } to deeply equal Map{ …(14) }
    @objectstack/spec:test:repo:  FAIL   repo  scripts/strictness-ledger-doc.test.ts > the ledger and the generated counts agree > the read-time totals equal the sums of the shards' own rows
      ↳ 失败原因: @objectstack/spec:test:repo: AssertionError: expected 'untriaged: 1297 object site(s) across…' to be 'untriaged: 1296 object site(s) across…' // Object.is equality
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 13 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit c268b72 Nov 2, 2026
74 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-0uv3au branch November 2, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(dashboard): a globalFilters entry named dateRange parses and silently replaces the built-in date range's binding

1 participant