Repository navigation
Commit 584b422
feat(spec): the draft read declares draftChanges, a pending draft's per-key provenance (#24383)
Fixes #24298
Clause-②: yes
The spec half of per-key draft provenance, for
objectstack-ai/objectui#12725 Target item 3: a field or a view setting
the AI copilot changed carries a 草稿 chip and a 来源 line, for example 「由
AI 副驾在 2 分钟前作为草稿加入」. Ruling A on #24324 (`6250704607`, item 3) keeps
that reader: "the spec key on the draft read". This PR declares the key
only. No server writes it yet and no reader draws it yet. The writer and
the reader are the follow-up cards named below; the seat files them when
this lands.
## What is declared
`GetMetaItemResponseSchema` (the body of `GET /api/v1/meta/:type/:name`)
gains one optional key beside `item`, `draftChanges`: one entry per
changed key path of the pending draft body.
| member | type | meaning |
|:--|:--|:--|
| `path` | `string[]`, each segment non-empty | The object keys from the
body root down to the deepest key the write changed, added or removed:
`["fields", "renewal_count"]` for an added field, `["fields",
"renewal_count", "label"]` for one property of it. An array value is one
leaf (`["kanban", "columns"]`, never an index). `[]` is the whole body,
for an item created as a draft. |
| `by` | `'copilot' \| 'studio' \| 'api'`, closed | The authoring
surface of the write that last changed this path. `copilot` is an AI
assistant's tool writing on a person's behalf, `studio` is a person
editing in Studio, `api` is any other caller of the save door. |
| `actor` | `string \| null` | The write's recorded identity (the save's
`actor`, the value its history event carries). `null` when the write
recorded none. |
| `at` | ISO-8601 instant, UTC `Z` form | When that write happened. The
parse refuses an offset or a local time; `toISOString()` produces the
admitted form. |
The meaning, as the `.describe()` text states it and the regenerated
reference page carries it. No server writes the key yet: until the
engine's draft save records it, every draft read is one whose saves
recorded none, and the key is absent. The rest is the contract a writer
must meet:
- It compares each draft save with the body that save replaced: the
previous draft, or the published body for the draft's first save.
- It keeps no two entries on one path, and replaces every entry at or
under a path a save changes. So the last write to change a key is the
entry whose `path` is the longest prefix of that key's path, and entries
under a key's path are changes inside it.
- It keeps the entries from the first recorded save until the draft is
published or discarded. Both remove the draft row, and the entries go
with it.
- The entries name which writes changed what. They are not a list of
every difference from the published body. Which elements of an array
differ is read by comparing the array with the published body's; the
array's entry answers who last changed the array.
- Served only on a `?state=draft` read that answers a stored draft row.
Absent on the active read, on the cached branch and on a
`?preview=draft` read. Absent means "not recorded", never "no changes".
`ListDraftsResponseSchema` (`GET /meta/_drafts`) is unchanged; its
docblock now points at the draft read.
The key carries ADR-0033's provenance item ("attribute each AI draft to
the acting user + mark it AI-sourced") at the grain of a key: `actor` is
the acting user, `by: 'copilot'` is the AI-sourced mark. They are two
members because one user makes both kinds of edit.
## Why this shape: the measurements behind each choice
Readings are at objectui `main` `76f91f80d` and at the console pin
`0707b4ae3712` (`.objectui-sha`) unless a line says otherwise; the line
numbers below are `main`'s, and the pin's differ only in
`StudioDesignSurface.tsx`.
1. **Where: on the draft read, not on the `_drafts` row.** The card
allowed either, and the reader decides. Studio's properties load the
item they edit through `readDraftForSave`
(`app-shell/src/views/metadata-admin/DraftConflictDialog.tsx:162`, both
refs), which is `client.getDraft`. That is `readItemResponse` with
`state=draft` (`data-objectstack/src/metadata-client.ts:1228` and
`:1141`, both refs), so `GET /meta/:type/:name?state=draft`, and it
returns the whole envelope (`res.json()`). Its callers are the loads
every pillar's properties edit: `StudioDesignSurface.tsx:3580` (app),
`:3771` (the current item), `:5698` (object), `:7382` (flow);
`ObjectListViewInspector.tsx:320` (view);
`PermissionMatrixEditor.tsx:644`; `ResourceEditPage.tsx:1141`, `:1945`;
`ObjectHooksPanel.tsx:238`. A key beside `item` reaches the reader
through the call it already makes. The `_drafts` list
(`usePendingDrafts`) is what the 变更 badge and the changes drawer read,
one row per item. Declaring the key on the read the body comes from also
means the marks and the body come from one stored row, read once, so a
mark can never describe a body the editor has not loaded.
2. **Beside `item`, never inside it.** It follows the same rule as
`registration`: it is a fact about the stored row the document cannot
carry. Inside the body it would be one more read decoration every GET,
edit and PUT round trip must strip (`METADATA_READ_DECORATIONS`). The
name is not `provenance`, because the ADR-0010 protection envelope
already uses that key on this schema. objectui's envelope test matches
by the presence of `type`, `name` and `item`, never by key count
(`isMetaItemEnvelope`, `metadata-client.ts:858`), so one more envelope
key changes no unwrapping at the pin.
3. **A path is an array of keys.** Keys contain dots:
`PermissionSetSchema.fields` is keyed `OBJECT.FIELD`
(`security/permission.zod.ts:956`, "entity_name.field_name"), so a
dotted string such as `fields.account.name.readable` has two readings.
An array has one.
4. **An array value is one leaf, and a path never carries an index.**
Studio's column selection is positional (`listViewColumnSelection`
builds `config.columns[N]`, `ObjectListViewInspector.tsx:452`). An index
moves when a column is inserted or reordered, so an index-keyed mark
would land on the wrong column. The card's own sketch names
`kanban.columns` as one key path. A writer computes this rule
generically, with no per-type knowledge of which member identifies an
array element.
5. **The ruled artboard marks array ELEMENTS, and this shape serves
it.** Re-checked this run against the maintainer's 「按「右栏二选一」定稿」
artboard, StudioRailProps (objectui `98aab506a`). With a kanban card
selected, the properties list the card's fields (图书, 读者, 应还日期, 状态), and
读者 and 应还日期 carry the 草稿 chip, under the 来源 line 「读者」「应还日期」两项由 AI 副驾在 2
分钟前作为草稿加入. Those card fields are `kanban.columns`
(`KanbanConfigSchema.columns`, a `string[]`, `ui/view.zod.ts:1973`).
Under this shape the entry `["kanban", "columns"]` (`by: 'copilot'`,
`at`) gives the who and the when; the reader finds which elements by
comparing the draft's array with the published one (读者 and 应还日期 are not
in it), and 「加入」 is the same comparison (a key absent from the published
body was added). That draws the artboard exactly. The limit, stated in
the describe text: attribution is per array. If a person also edits the
same array before publish, its entry becomes `studio`, and the reader
can no longer say the copilot added 读者. No artboard or ruling draws that
case. A per-element attribution later can be added as an optional member
of the entry without changing `path`.
6. **`by` is a closed set of authoring surfaces, not a reuse of an
existing field.** Three candidates were measured, and none answers the
question:
- the `_drafts` row's `updatedBy` is the acting user, the same person
for a copilot write and a hand edit;
- `sys_metadata.source` (`filesystem | database | api | migration`) is
the storage origin, and it is not projected;
- the history event's `source` (`HistoryMetaItemResponseSchema`) is an
open label of the code path that wrote the event (`main` now describes
it so, `ba5bfe96e`), recorded per write, not per key.
The three values are the card's and the triage's. Each one has a writer
named below.
7. **Optional, never defaulted to `[]`.** Absent means "not recorded": a
server with no writer, or a draft row saved before the writer landed. An
empty-array default would answer "nothing changed" for exactly those
rows. An older response with no key still parses, and the key stays
absent (pinned).
8. **The entry is a plain object, not `.strict()`.** The seat's earlier
suggested route asked for a strict entry; this PR does not do that. The
strictness ledger
(`docs/audits/2026-07-unknown-key-strictness-ledger.md`, the class
table) classes server responses as **wire**: "stay tolerant (`.strip` /
`.passthrough`); strictness here turns an upstream *addition* into our
parse crash". The value set is closed (`by`), and so is the path's
spelling (an array of non-empty strings). The key set stays tolerant,
like every other entry on this schema. The contract review judges this
one.
**Ruling A re-checked.** Nothing in A changes what the reader needs from
this key. A's item 3 names this key on the draft read. A's item 2, the
chat's Preview for any draft, is objectstack-ai/objectui#12968 (merged
on objectui `main` as `76f91f80d`). It reads the `_drafts` counts, and
its seat ruling `6251864146` item 2 declined per-key data on `_drafts`
rows. A `?preview=draft` read carries no `draftChanges`, and #12968 asks
for none.
## Declared is enforced: the writer and the reader
No liveness ledger covers response keys (`packages/spec/liveness/`
governs registered metadata types; `authorable-surface/api.json` records
only that the key exists). So these cards are the enforcement record.
The seat files them when this lands.
| card | lane and repo | what it does |
|:--|:--|:--|
| **W1, the writer** | `domain:engine`, objectstack,
`packages/metadata-protocol` (plus the request member's spec half) | A
draft save (`saveMetaItem` with `mode: 'draft'`, down to
`SysMetadataRepository.put(…, { state: 'draft' })`) computes the changed
paths in the transaction that already reads the head row
(`resolveWriteHead`). It compares the replaced body (the existing draft
row, else the active row for a first draft save, else nothing) with the
new one by the spec's path rule, and merges the paths into the draft
row's stored entries: each changed path replaces the entries at or under
it. `actor` is the save's `actor`; `at` is the write's own `now`, the
instant `updated_at` gets. Promote and discard already delete the row.
The strict draft read (`getMetaItem` with `state: 'draft'`, from a
stored draft row) serves the entries beside `item`, on the REST door and
on the dispatcher. **The surface value:** the door cannot tell Studio
from any other HTTP caller today (`writeFace` names the transport; the
history `source` is a code-path label). So W1 declares one save-request
input carrying the surface, on `SaveMetaItemRequestSchema` and its REST
spelling, with the same closed set, and moves the inline enum into one
exported schema both sides reference. An absent input stamps `api`.
Recommended: the HTTP doors refuse `copilot`, so only the in-process
copilot call can stamp it. Bulk paths to check: every `put` that writes
a draft row, and `migrate-stored`'s rewrite of draft rows, whose
conversions are not an authoring write and record no path. |
| **W2, the copilot stamp** | cloud, objectstack-ai/cloud,
`service-ai-studio` | The copilot's draft tools (`stageDraft` /
`applyDraft` in
`packages/service-ai-studio/src/tools/metadata-tools.ts`, as
objectui#12725's phase-5 report read them at cloud `f9e45145`) pass
`copilot` on their in-process `saveMetaItem` call, through the same
door, once cloud's framework pin carries W1. `Blocked-by:` W1. |
| **R1, the reader** | `domain:ui`, objectui, `packages/app-shell` |
Studio's properties read `draftChanges` off the envelope
`readDraftForSave` already returns. For the selected element (a field is
`["fields", NAME]`, a view setting is its key) they draw the 草稿 chip and
the 来源 line when the entry answering for it, or an entry under it, has
`by: 'copilot'`; the line's time comes from `at`. For an element of an
array setting (the artboard's card fields) the reader compares the
draft's array with the published body's: the view inspector reads the
published layer today only when no draft exists
(`ObjectListViewInspector.tsx`, the load at `:320`), so R1 adds that one
read on a draft load. The object designer already reads both
(`StudioDesignSurface.tsx:5697`). Studio's own draft saves declare
`studio` on W1's input, so a hand edit replaces a copilot mark.
`Blocked-by:` W1, plus objectui's framework pin. It completes
objectstack-ai/objectui#12725 item 3. |
**The W1 risk to watch.** If W1 finds no acceptable way for Studio's
saves to declare their surface, `studio` would be a declared value with
no writer. Then W1 comes back to this schema and narrows the set before
any writer ships; it must not invent a mapping.
## Verification
Every reading below is local, in this run's container, at branch head
`438e3570f` (`git rev-parse --short HEAD`) unless it names another
commit. Heavy runs went through `scripts/pm/os-verify-lock.sh`, and each
line quotes that run's own summary line. CI's readings are this PR's
check-runs.
- **The spec package.** `pnpm --filter @objectstack/spec typecheck` gave
`VERDICT command-exit 0` (`check:test-typecheck: OK`; `tsc -p
tsconfig.test.json --listFilesOnly` lists the new test file once). `pnpm
--filter @objectstack/spec exec vitest run --project local
--maxWorkers=2` gave `Test Files 695 passed (695)` and `Tests 21238
passed | 1 todo (21239)`.
- **The new tests**
(`packages/spec/src/api/meta-item-response-shapes.test.ts`, the file at
`22 passed (22)`): five runtime cases and five type-level pins.
- Runtime: every entry survives a parse (each surface, a root path, a
dotted key, a null actor). A response without the key parses, and the
key stays absent. A surface outside the set is refused at
`draftChanges.0.by` (`invalid_value`). A dotted-string path is refused
(`invalid_type`), and so is an empty segment (`too_small`). A missing
`at` is refused (`invalid_type`), and so is a non-ISO one
(`invalid_format`).
- Type level: `by` is the three-value union, `path` is `string[]`,
`actor` is `string | null`, the key may be absent, and the `_drafts` row
carries no `draftChanges`.
- **Generated artefacts.** After `pnpm --filter @objectstack/spec build`
at `438e3570f`, `check:generated` reads `✓ All 14 generated artifacts
are up to date`.
- **Reverse verification**, against the rebuilt declarations from a
consumer package. A scratch file in `packages/client/src` typed
`draftChanges: [{ …, by: 'ai', … }]` as `GetMetaItemResponse`. Compiled
with `tsc --ignoreConfig --noEmit --strict`, it exited 2 with `error
TS2322: Type '"ai"' is not assignable to type '"api" | "copilot" |
"studio"'`. With `by: 'copilot'` it exited 0. The file was then removed,
and `git status` was clean.
- **Ablation**, at `e98129b65`, through `scripts/ablation-replace.mjs`.
No build was needed, because the test imports the source. `by:
z.enum(['copilot', 'studio', 'api'])` became `by: z.string()` (anchor x1
→ x0, blob `fe32063f1f92` → `7dd4e62cf796`). The result was `Tests 1
failed | 21 passed (22)`, and the failing case was `` refuses a surface
outside the closed set, at the entry's `by` ``. The tool then reported
`` ok restored: blob == HEAD (fe32063) and `git diff HEAD` is empty
``. From `e98129b65` to `438e3570f` the only change is one docblock
paragraph.
- **Consumers.** I ran `git grep` repo-wide for the packages that import
`GetMetaItemResponse` / `GetMetaItemResponseSchema` by name. These are
downstream consumers of `@objectstack/spec`: `@objectstack/client` (the
type, re-exported), `@objectstack/client-react`,
`@objectstack/service-automation` (a type import in an integration
test), `@objectstack/rest` (the route ledger's schema name) and
`@objectstack/runtime` (comments).
- The three typechecks (`Scope: 3 of 85 workspace projects`) were green.
- `@objectstack/client` vitest: `52 passed (52)` files, `671 passed
(671)` tests.
- The four `@objectstack/rest` `meta-item-*` route tests: `52 passed
(52)`.
- `@objectstack/runtime`'s `domains/meta-item-absent-404.test.ts`: `6
passed (6)`.
- All of these ran at `438e3570f` (`VERDICT command-exit 0`).
- objectui at the pin `0707b4ae3712` and on `main` `76f91f80d` imports
the schema in one code file, `DraftConflictDialog.tsx:84`, and reads
only `.shape.version` there (`:138`). Its envelope test is key-presence
(`isMetaItemEnvelope`), so one more optional envelope key changes
nothing there.
- **The full build** (`turbo run build --filter=!@objectstack/docs
--concurrency=2`): `Tasks: 76 successful, 76 total`. The gates that read
other packages' `dist/` need it.
- **The derived gate union**, run after the final commit:
- `node scripts/pm/dispatch-gates.mjs --commands` (no paths; the change
set is 6 paths against merge base `6a49efb9c`) derived 110 commands at
`438e3570f`. All 110 exited 0, each exit code captured before any pipe.
- `--ran` reconciliation: `110 derived, 110 run, 0 NOT-MEASURED, 0
UNRUN` (`a DERIVED zero — all 110 recorded an exit code and none of them
is 3`).
- One standing line inside a green family: `check:entry-nameability`
prints `NOT MEASURED: no callable export on
@objectstack/spec/api-assembled, @objectstack/spec/qa`. That covers two
entries this diff does not touch; the `api` entry is measured.
- **The local-only families:** `pnpm check:dev-prereqs` exit 0
(`@objectstack/spec … built from the sources on disk`), and `pnpm
check:skill-frame-freshness` exit 0.
- **Declared to CI:** the whole `check:*` farm, `pnpm lint`,
`test:repo`, and every type-check lane outside the four packages
typechecked above (spec, client, client-react, service-automation). The
reconciliation counts these outside the derived total.
- **Patch round 1** (contract review `6253694122`, item ① 9), at head
`9e98be435`. Only describe text changed: the schema-level `draftChanges`
describe now states the writer as a contract and says once that no
server writes the key yet, and the `at` describe names the one ISO-8601
form the parse admits. `gen:docs` moved only
`content/docs/references/api/protocol.mdx`. After `pnpm --filter
@objectstack/spec build`, `check:generated` reads `✓ All 14 generated
artifacts are up to date`. `pnpm --filter @objectstack/spec typecheck`
passed (`check:test-typecheck: OK`), and
`meta-item-response-shapes.test.ts` gave `22 passed (22)` (`VERDICT
command-exit 0`). `pnpm check:doc-authoring` exited 0 (`19054
customer-facing string(s) … no internal issue-id references`). The full
build gave `Tasks: 76 successful, 76 total`. Re-derived with no paths,
`dispatch-gates --commands` gave the same 110 commands, and all 110
exited 0. `--ran` read `110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN`.
The derivation warned that the tree is behind `origin/main` and that two
gate-source files changed there
(`scripts/check-single-authz-resolver.mjs`,
`scripts/engine-double-contract.pinned.json`). Both are data rows for
`main`'s own new files, and neither touches this diff's paths. CI runs
both on the merge ref.
## Acceptance notes
- **Two departures from the seat's earlier suggested route, each with
its reason above:** the key sits on the draft read rather than the
`_drafts` row (choice 1), and the entry is not `.strict()` (choice 8).
- **Resumed run.** The first run's branch (`82d1987ce`) was kept through
the hold on #24324. This run merged `origin/main` `6a49efb9c` through
`scripts/pm/os-regen-merge.sh` (`b5e73b9e1`): the os-regen driver had
kept the branch's side of `content/docs/references/api/protocol.mdx`,
and the regeneration (`125f6cb10`) carries both `main`'s history
`source` describe (`ba5bfe96e`) and this key's rows. This run then added
one sentence to the `path` describe and two docblock paragraphs (the
array-element reading of choice 5, and the ADR-0033 item), and
regenerated the reference page (`e98129b65`).
- **Generated artefacts, regenerated only where `check:generated` proved
them stale:** `authorable-surface/api.json` (the build writes one line,
`api/GetMetaItemResponse:draftChanges`);
`content/docs/references/api/protocol.mdx` (`gen:docs`);
`docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md`
(`gen:strictness-ledger`: the `api/` site total 444 → 445).
- **The first run's note on the history `source` describe is
withdrawn:** `main` rewrote that describe in `ba5bfe96e` (#24334), and
the merged tree carries it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 1eae21f commit 584b422
6 files changed
Lines changed: 180 additions & 1 deletion
File tree
- .changeset
- content/docs/references/api
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- packages/spec
- authorable-surface
- src/api
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1457 | 1457 | | |
1458 | 1458 | | |
1459 | 1459 | | |
| 1460 | + | |
1460 | 1461 | | |
1461 | 1462 | | |
1462 | 1463 | | |
| |||
1483 | 1484 | | |
1484 | 1485 | | |
1485 | 1486 | | |
| 1487 | + | |
| 1488 | + | |
| 1489 | + | |
| 1490 | + | |
| 1491 | + | |
| 1492 | + | |
| 1493 | + | |
| 1494 | + | |
| 1495 | + | |
| 1496 | + | |
| 1497 | + | |
1486 | 1498 | | |
1487 | 1499 | | |
1488 | 1500 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
789 | 789 | | |
790 | 790 | | |
791 | 791 | | |
| 792 | + | |
792 | 793 | | |
793 | 794 | | |
794 | 795 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| 35 | + | |
35 | 36 | | |
36 | 37 | | |
37 | 38 | | |
| |||
305 | 306 | | |
306 | 307 | | |
307 | 308 | | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
546 | 546 | | |
547 | 547 | | |
548 | 548 | | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
549 | 592 | | |
550 | 593 | | |
551 | 594 | | |
| |||
588 | 631 | | |
589 | 632 | | |
590 | 633 | | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
591 | 677 | | |
592 | 678 | | |
593 | 679 | | |
| |||
1962 | 2048 | | |
1963 | 2049 | | |
1964 | 2050 | | |
| 2051 | + | |
| 2052 | + | |
| 2053 | + | |
| 2054 | + | |
| 2055 | + | |
| 2056 | + | |
1965 | 2057 | | |
1966 | 2058 | | |
1967 | 2059 | | |
| |||
0 commit comments