Repository navigation
Commit 92a1638
fix(service-analytics): the draft preview groups a bucketed time dimension and reads a qualified filter member as the live faces do; ObjectQL serves a backed RELATION.id dimension (#28742)
domain:server#3 · os-project-manager · session_01FEkQ5kbUJTTWAxjV9nqr1P
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XB1hsVtuncZ5F5C4zpiftHC)_
Fixes #28690
Fixes #28691
Fixes #28692
Clause-②: yes (widening)
Metadata-protocol: none
Skills-docs: none — no file under `skills/**` or `content/docs`
describes how the draft preview groups a time dimension or reads a
qualified filter member, or says the ObjectQL strategy refuses a
`RELATION.id` dimension.
Card thread: `session_01FEkQ5kbUJTTWAxjV9nqr1P`.
## 维护者速读
- **改了什么**:数据集查询三条执行面(原生 SQL、ObjectQL、草稿预览)的三处不一致收齐。① 草稿预览对带粒度的
`timeDimensions` 按桶分组,不再要求它同时出现在 `dimensions` 里,`fields` 里也带上这一列。②
草稿预览遇到 `对象名.region`、`数据集名.region`、或声明在 `region` 上的维度 `zone`
这类筛选成员,改为读它真正对应的列(按该列的声明类型比较),不再返回 `[]`。③ ObjectQL 策略对 `account.id`
维度、以及同一关系上的两个维度(`account.id` + `account.name`,或 `account` +
`account.name`),groupBy 只写一次那一列,不再被引擎以「重复列」拒绝。
- **为什么改**:卡 #28690、#28691、#28692(p3),是 #28555 开发时在真 SQLite
上测到的同族缺口:作者在草稿里看到一个总数,发布后变成按月分组;草稿里一个能用的筛选什么都不返回;同一个查询原生面能答、ObjectQL 面
400。
- **风险与回滚**:只放宽,不收窄:之前给错数或 `[]` 的现在给线上同样的答案,之前 `INVALID_QUERY`
的现在服务。引擎的重复 groupBy 拒绝原样保留(改在策略里,不碰
`packages/objectql`)。预览仍拒绝经关系路径读的成员(#28555)。回滚即 revert。
- **先看**:`packages/services/service-analytics/src/preview-evaluator.ts`
分组段(改用共享的 `projectedDimensions`)与新的 `whereColumns`
参数;`analytics-service.ts` 新的
`previewWhereColumns`;`strategies/objectql-strategy.ts` 的
`executeCrossObject` 与 `resolveFkAttr` 两处 groupBy。
- **两分钟验证**:`pnpm --filter @objectstack/service-analytics exec vitest
run src/__tests__/preview-time-filter-fk-parity.test.ts`,预期 17 passed。
- **需要你做的**:无,席位落地。
## What changes
- **#28690 — `preview-evaluator.ts`.** The grouping set is now
`projectedDimensions(query)` from `order-key-door.ts`, the one
definition the ObjectQL strategy's grouping, row mapping and
`buildFieldMeta` already read: every `dimensions` entry, then every
`timeDimensions` entry with a granularity that is not already one of
them. The evaluator's `fields` list reads the same set, so the bucket
column carries the cube dimension's type (`time` for a `date` dataset
dimension), as on the live faces. A `timeDimensions` entry without a
granularity still only contributes its window.
- **#28691 — resolve, not refuse.** The order allowed either. Both live
strategies serve these members (measured below), so the preview now
resolves them. `previewWhereColumns(query, cube)` (new, module-level in
`analytics-service.ts`) reads the `where` leaves with the reader the
strategies compile with (`normalizeAnalyticsFilterTree` +
`collectFilterLeaves`, as `relationshipMembers` reads them) and resolves
each with `declaredMemberEntry(cube, member, 'any')`, the strategies'
filter-member lookup that `assertWhereFields` and the dimension face's
`previewEvaluationCube` already use. Each member spelled other than its
bare column (`OBJECT.region`, `CUBE.region`, `zone` over `region`) maps
to that column. The evaluator takes the map as a new optional last
argument `whereColumns`: the `where` match reads each listed member from
its column, and the declared-type reader used by the `where` lowering
and the storage-form comparison reads the column's declaration. No
`where` tree is rewritten or walked a second time; the time window and
grouping read columns as before. A member that resolves to no declared
entry, an expression or a relationship path is not listed (the latter is
refused earlier by #28555's gates).
- **#28692 — `strategies/objectql-strategy.ts`, where the duplicate
enters.** Two places built a `groupBy` naming one column twice, and the
engine's guard refused it (`groupBy.1 names the column '…', which
groupBy.0 already names`, `INVALID_QUERY`): `resolveFkAttr` grouped the
related object by `['id', attr]`, which is `['id', 'id']` for
`RELATION.id`; and `executeCrossObject` pushed a cross-object
dimension's FK column once per dimension, so `account.id` beside
`account.name` (both FK `account`) pushed `account` twice, and a base
dimension over the FK column (`account` beside `account.name`) did the
same. Now `resolveFkAttr` groups `['id']` when `attr` is `id`, and the
cross-object `groupBy` names an FK column once (a base dimension over a
column already grouped as an FK reuses it). The re-bucket reads the one
column for each dimension, as before. Nothing in `packages/objectql`
changes, so its duplicate-groupBy refusal is unchanged and there are no
other callers of a changed function to measure.
- Tests: new `src/__tests__/preview-time-filter-fk-parity.test.ts` (17
cases, real SQLite, the #28201 / #28258 / #28555 harness).
- Changeset
`.changeset/28690-preview-time-buckets-filter-columns-fk-id.md`,
`@objectstack/service-analytics` patch.
## Why
Measured on `origin/main` `d62d9737` (real SQLite, every face), with a
probe printing each face's answer per selection. Dataset over a deal
object (`region`, `amount`, `closed_at` datetime, `account` lookup),
rows amer/2026-03-01/100/a1, amer/2026-04-01/50/a2,
emea/2026-04-02/30/a1; dimensions `region`, `closed_at` (date), `zone`
(field `region`).
<details><summary>Before / after per selection</summary>
| selection | native | ObjectQL | preview before | preview / ObjectQL
after |
|:--|:--|:--|:--|:--|
| td `closed_at` month, no dims | 2026-03:100, 2026-04:80 | same | one
row, total 180 | preview = live |
| dim `region` + td `closed_at` month | (amer,03,100) (amer,04,50)
(emea,04,30) | same | grouped by region only | preview = live |
| td `OBJECT.closed_at` month | keyed `OBJECT.closed_at`, 03:100, 04:80
| same | one row, total 180 | preview = live |
| td `closed_at` year + April window | `2026`:80 | same | total 80, no
key | preview = live |
| td in dims too; td window only; `dateGranularity` | agree | agree |
agreed already | unchanged |
| filter `OBJECT.region` / `CUBE.region` / `zone` = emea | emea 30 |
same | `[]` | preview = live |
| `$or` [`OBJECT.region` $in emea, amount 50] | amer 50, emea 30 | same
| amer 50 only | preview = live |
| `OBJECT.closed_at` $gte 2026-04-01 | amer 50, emea 30 | same | `[]` |
preview = live |
| backed dim `account.id` | a1:130, a2:50 | `INVALID_QUERY` (groupBy
'id' twice on the account object) | refused (#28555) | ObjectQL =
native; preview still refuses |
| backed `account.id` + `account.name` | served | `INVALID_QUERY`
(groupBy 'account' twice) | refused | ObjectQL = native |
| backed `account` + `account.name` (either order) | served |
`INVALID_QUERY` | refused | ObjectQL = native |
</details>
## Tests
<details><summary>Red first, green, ablations</summary>
- Red on base: the three production files restored to `d62d9737` (`git
checkout d62d973 -- …`), the new test at HEAD: `Tests 15 failed | 2
passed (17)`. The two green are the controls (a window-only time
dimension, the preview's #28555 refusal). Each #28690/#28691 case fails
on the preview leg, each #28692 case on the ObjectQL leg with the
engine's duplicate-column message. Restored with `git checkout HEAD --`,
`git status` clean.
- Green: `17 passed (17)`.
- Ablations at `de8aeea7` (`node scripts/ablation-replace.mjs`, fix
committed first, each anchor 1 → 0, restore blob == HEAD and `git diff
HEAD` empty; the suite imports `src`, no build between):
- A1 grouping set back to `query.dimensions ?? []`: `5 failed | 12
passed` (the four bucket cases and the `zone` + bucket case).
- A2 `--delete` the `previewWhereColumns(q, cube)` argument: `7 failed |
10 passed` (the six filter cases and the `zone` + bucket case).
- A2b the `where` type reader back to `declaredType` (members read from
their columns but typed as written): `1 failed | 16 passed` (the
`OBJECT.closed_at` instant comparison with an offset comparand).
- A3a `resolveFkAttr` back to `['id', attr]`: `2 failed | 15 passed`
(`account.id`, `account.id` + `account.name`).
- A3b the FK column pushed once per cross dimension again: `2 failed |
15 passed` (`account.id` + `account.name`, `account` before
`account.name`).
- A3c the base-dimension reuse of an FK column off: `1 failed | 16
passed` (`account` after `account.name`).
- `pnpm --filter @objectstack/service-analytics exec vitest run` at
`de8aeea7`: `Test Files 203 passed (203)`, `Tests 4856 passed | 282
skipped`.
- `pnpm --filter @objectstack/rest exec vitest run src/analytics` (after
`--filter '@objectstack/rest^...' --filter
@objectstack/service-analytics` build): `Test Files 23 passed (23)`,
`Tests 298 passed | 22 skipped`.
- `pnpm --filter @objectstack/service-analytics typecheck` (includes
`__tests__`, after a full build) exit 0; `eslint` on the four touched
source and test files exit 0.
</details>
## Acceptance notes
- #28691 went the "resolve" way: both live strategies serve every
measured spelling, so a refusal would have narrowed the preview below
them. The resolution is `declaredMemberEntry`, the lookup the strategies
mirror and the preview's dimension face already uses; the leaves come
from the strategies' own reader. No second walker: the `where` tree is
not rewritten, the evaluator reads a listed member from its column at
the match.
- #28692's fix is in the strategy, where the duplicate enters.
`packages/objectql` is not touched, so its duplicate-groupBy refusal
stands and no other caller moves.
- Out of scope, measured on `400154ac` (real SQLite), not fixed: two
dataset dimensions over one base column (`region` and `zone` over
`region`) on the ObjectQL strategy's direct path answer `INVALID_QUERY`
/ 400 (`groupBy.1 names the column 'region'`), while native and the
preview serve amer 150 / emea 30 keyed by both names. Same root as
#28692 on the non-cross-object path; reported to the seat.
## Verification
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 62 families derived on `de8aeea7` (5 paths
vs merge base `4bf48a8cc`); each run with stdin from `/dev/null`, exit
captured before any pipe, after a full `turbo run build
--filter=!@objectstack/docs --concurrency=2` (lock VERDICT command-exit
0). `--ran`: **62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN**, every one
`exit 0`.
- Hand gates: `node scripts/check-platform-checklist.mjs --changed-from
origin/main` exit 0 (1 anchor judged, 0 lost); `node
scripts/check-commit-card-trailers.mjs --range origin/main..HEAD` exit
0.
- Dogfood battery: not run. Clause-② is widening only, with no `!` and
no narrowing arm.
- Head is `400154ac`: `de8aeea7` with the changeset's two placeholder
spellings changed (`RELATION.id`), no code change. On `400154ac`, the
six changeset- and citation-reading gates
(`check-adr-0087-registration`, `check-changeset-no-major`,
`check-empty-changeset` with `--base origin/main`,
`check:pm-changeset-deadline-census`, `check:issue-citations`,
`check-closing-keyword-parity`) were run again, each exit 0.
- CI families: read from this PR's check-runs.
<details><summary>The 62 derived families and their exits</summary>
- `node scripts/check-adr-0087-registration.mjs --base origin/main` ::
exit 0
- `node scripts/check-adr-0087-registration.mjs --self-test` :: exit 0
- `node scripts/check-changeset-no-major.mjs --base origin/main` :: exit
0
- `node scripts/check-closing-keyword-parity.mjs` :: exit 0
- `node scripts/check-closing-keyword-parity.mjs --self-test` :: exit 0
- `node scripts/check-comment-mask-adoption.mjs` :: exit 0
- `node scripts/check-comment-mask-adoption.mjs --self-test` :: exit 0
- `node scripts/check-comment-mask-corpus.mjs` :: exit 0
- `node scripts/check-dts-emitted.mjs --self-test` :: exit 0
- `node scripts/check-empty-changeset.mjs --base origin/main` :: exit 0
- `node scripts/check-empty-changeset.mjs --self-test` :: exit 0
- `node scripts/check-issue-citations.mjs` :: exit 0
- `node scripts/check-plugin-teardown-shape.mjs` :: exit 0
- `node scripts/check-plugin-teardown-shape.mjs --self-test` :: exit 0
- `node scripts/check-tenant-audit-census.mjs` :: exit 0
- `node scripts/check-tenant-audit-census.mjs --self-test` :: exit 0
- `node scripts/ci/assert-turbo-remote-cache.mjs --self-test` :: exit 0
- `node scripts/ci/plan-test-core-shards.mjs --self-test` :: exit 0
- `node scripts/ci/prune-turbo-cache.mjs --self-test` :: exit 0
- `node scripts/docs-audit/check-affected-docs.mjs` :: exit 0
- `node scripts/docs-audit/check-drift-comment.mjs` :: exit 0
- `node scripts/pm/release-rehearsal-clone.mjs --self-test` :: exit 0
- `node scripts/release-pending-publish.mjs --self-test` :: exit 0
- `pnpm --filter @objectstack/spec run check:duration-unit-keys` :: exit
0
- `pnpm check:changeset-gate-self-tests` :: exit 0
- `pnpm check:ci-filter-parity` :: exit 0
- `pnpm check:closed-ceremony-text` :: exit 0
- `pnpm check:cross-package-test-inputs` :: exit 0
- `pnpm check:dispatcher-error-vocabulary` :: exit 0
- `pnpm check:doc-authoring` :: exit 0
- `pnpm check:driver-memory-census` :: exit 0
- `pnpm check:dts-closure` :: exit 0
- `pnpm check:dual-build-cjs-loads` :: exit 0
- `pnpm check:engine-double-contract` :: exit 0
- `pnpm check:error-status-conformance` :: exit 0
- `pnpm check:gitlink-declared` :: exit 0
- `pnpm check:issue-citations` :: exit 0
- `pnpm check:keyed-text-bounds` :: exit 0
- `pnpm check:lean-entry-closure` :: exit 0
- `pnpm check:logger-receiver-detach` :: exit 0
- `pnpm check:nul-bytes` :: exit 0
- `pnpm check:objectql-double-limit` :: exit 0
- `pnpm check:org-identifier` :: exit 0
- `pnpm check:page-declaration-shape` :: exit 0
- `pnpm check:platform-object-tenancy-census` :: exit 0
- `pnpm check:pm-changeset-deadline-census` :: exit 0
- `pnpm check:published-files` :: exit 0
- `pnpm check:query-options-erasure` :: exit 0
- `pnpm check:refd-timer-probe` :: exit 0
- `pnpm check:registry-log-declared` :: exit 0
- `pnpm check:rest-log-spy-declared` :: exit 0
- `pnpm check:slot-lookup` :: exit 0
- `pnpm check:sourcemap-no-sources-content` :: exit 0
- `pnpm check:swallow-census-controls` :: exit 0
- `pnpm check:system-context-census` :: exit 0
- `pnpm check:test-source-alias` :: exit 0
- `pnpm check:tier-file-adoption` :: exit 0
- `pnpm check:type-check-coverage` :: exit 0
- `pnpm check:type-check-debt` :: exit 0
- `pnpm check:undeclared-dep-imports` :: exit 0
- `pnpm check:watch-hint-literal` :: exit 0
- `pnpm check:where-matcher` :: exit 0
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No `packages/spec` change.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes (scoped run above; the
repo-wide run is CI's).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request. (Three cards, one PR, by the dispatch order.)
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: not touched.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block is the
dispatch contract's).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5kbUJTTWAxjV9nqr1P)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01FEkQ5kbUJTTWAxjV9nqr1P
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7e6e8a4 commit 92a1638
5 files changed
Lines changed: 403 additions & 8 deletions
File tree
- .changeset
- packages/services/service-analytics/src
- __tests__
- strategies
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2895 | 2895 | | |
2896 | 2896 | | |
2897 | 2897 | | |
| 2898 | + | |
| 2899 | + | |
| 2900 | + | |
2898 | 2901 | | |
2899 | 2902 | | |
2900 | 2903 | | |
| |||
4777 | 4780 | | |
4778 | 4781 | | |
4779 | 4782 | | |
| 4783 | + | |
| 4784 | + | |
| 4785 | + | |
| 4786 | + | |
| 4787 | + | |
| 4788 | + | |
| 4789 | + | |
| 4790 | + | |
| 4791 | + | |
| 4792 | + | |
| 4793 | + | |
| 4794 | + | |
| 4795 | + | |
| 4796 | + | |
| 4797 | + | |
| 4798 | + | |
| 4799 | + | |
| 4800 | + | |
| 4801 | + | |
| 4802 | + | |
| 4803 | + | |
| 4804 | + | |
| 4805 | + | |
| 4806 | + | |
| 4807 | + | |
| 4808 | + | |
| 4809 | + | |
| 4810 | + | |
| 4811 | + | |
| 4812 | + | |
| 4813 | + | |
| 4814 | + | |
| 4815 | + | |
| 4816 | + | |
| 4817 | + | |
| 4818 | + | |
| 4819 | + | |
4780 | 4820 | | |
4781 | 4821 | | |
4782 | 4822 | | |
| |||
0 commit comments