Repository navigation
Commit b373596
fix(objectql): a delete refused by a traversing rule on its reference cleanup names the delete, the reference and the repair (#20021)
Fixes #20006
Clause-②: no
## What this changes
Deleting a record clears every `set_null` reference to it (the default
for an optional `lookup`). `ObjectQL.cascadeDeleteRelations` does this
with an UPDATE of each referencing record. That cleanup resolves no
related record for a validation rule, by design (#18682: resolving one
would let a rule's verdict leak one bit of a related record to a deleter
who cannot read it). So a `script` / `cross_field` rule on the
referencing object that reads through a reference faults there. It
refuses the cleanup, and the delete with it.
The refusal stays. #18682 ruled it fail-closed, and this PR does not
touch it. Only the text changes, per round-11 Q1 option D,
「让外键清空的拒绝文字指对对象」.
**Before**, measured at the merge base `fc6ddb87a`: the deleter got the
generic fault text about the rule's own object:
```text
Validation rule 'closed_account_frozen' could not be evaluated (runtime: No such key: status) — write rejected.
The predicate reads 'status', which this object does not declare — fix the rule's condition, or declare the field.
```
The person who deleted the record did not write that rule. Following the
advice adds a bogus `status` column to `crm_deal`, the wrong object.
**After**:
```text
Cannot delete crm_account (acc_1): the delete clears `account` on the crm_deal records that reference it,
and validation rule 'closed_account_frozen' on crm_deal could not be evaluated on that write — it reads
'status' through `account`, and a rule is given no related record while a delete clears references.
Guard the rule on `account` being set: make it the `then` of a `conditional` rule whose `when` is
`record.account != null`. Or change `deleteBehavior` on crm_deal.account: 'cascade' deletes those records
with the crm_account, 'restrict' refuses the delete while they exist.
```
It names all four things the triage asked for: the object and rule, the
reference being cleared, the parent delete that is blocked, and the
repairs. Each repair was measured to work, and each is pinned:
- **The `conditional` guard** makes the same delete succeed and clears
the reference. It is offered only to a rule that reads through the
reference the cleanup empties. Such a rule already refuses every write
that leaves that reference empty (`no single related record`), so the
guard only lets those writes through. The guarded rule is still judged
on an ordinary write while the reference is set, and a non-vacuity pin
shows it refusing with the authored message. On a multi-value reference
the other members stay, so the guard would still run the rule (measured:
still refused). There the text offers only `deleteBehavior`.
- **A rule that reads only through another reference** (the #18682
plugin-security fixture's own shape) is offered only `deleteBehavior`. A
guard on the cleared reference would stop judging that rule on every
record whose cleared reference is empty, on every insert and update
(patch round 1, F2).
- **`deleteBehavior`** is a real, authorable `FieldSchema` key
(`'set_null' | 'cascade' | 'restrict'`). With `'cascade'` the delete
succeeds and the referencing records are deleted with it; measured on a
single-value and a multi-value lookup. With `'restrict'` the delete is
refused up front: `DELETE_RESTRICTED`, 409, nothing changed.
**Unchanged, byte for byte:** the error (`VALIDATION_FAILED`), the field
error (`rule_violation`, `_record`), the whole `constraint` (`reason:
'unevaluable'`, `fault`, `missingKey`), and the set of deletes refused.
The new text is chosen only AFTER the predicate has already faulted. A
rule that has a verdict without reading through the reference still lets
the delete through, as before. That is pinned with `record.amount > 100
&& record.account.status == 'closed'`; CEL's `false && FAULT` is
`false`. Any other refusal keeps today's text, and two controls pin it
byte for byte:
- a rule that reads through no reference, on the same cleanup;
- the same fault on an ordinary update.
## How
- **`rule-validator.ts`.** On a CEL fault, `checkPredicate` asks
`referentialClearRefusal` whether this is the cleanup. The answer is yes
only when three things hold:
- the `related` binding was minted by `referentialClearBinding`;
- the fault's missing key is a column the rule reads through a reference
field;
- `readsAnOwnColumnItLacks` finds no own read of a key its holder does
not hold. Optional spellings (`has(record.K)`, `record.?K`) count too,
although they do not fault, so such a rule keeps the generic text: a
conservative miss that changes no verdict.
That last check covers EVERY own read, not only the key CEL reports;
when both operands of `&&` / `||` fault, CEL reports the right-hand key.
The own reads are:
- a bare `record.K` the record does not hold;
- a `record.F.K` through a field that is not a reference, whose value
lacks `K`;
- a bare `previous.K` the previous row does not hold;
- a `previous.F.K` through ANY field, a reference included, whose value
lacks `K`. `previous` is never hydrated, so a hop through a reference
there meets its bare id.
A declared column is materialised on `record` and `previous`, is always
held, and never counts. If any own read faults, or the missing key is
not read through a reference, the generic text stands byte for byte.
`referenceGuardRepair(field)` holds the guard wording once.
- **`engine.ts`, `cascadeDeleteRelations`.** Beside
`__referentialFieldClear`, the cleanup UPDATE's context gets
`__referentialFieldClearCause`: `{ object, id, referencingObject,
referencingId, field }`. It uses the operation-private `__` prefix, so
every consumer that forwards a context already strips it. It authorizes
nothing, and the referencing row's id never reaches the text.
- **`engine.ts`, `resolvePredicateRelated`.** This is the
`__referentialFieldClear` short-circuit, the `⛔ A referential FK clear
resolves NOTHING` block. It still resolves nothing. For the one record
the cleanup was issued for (same object, same row id), it now hands back
an EMPTY binding that carries the cause. A write that only inherits the
envelope keeps today's text. That is a hook's write to another object or
another row during the cleanup, and it is pinned.
- **No public surface moves.** `RelatedRecordBinding`,
`RelatedFieldBinding`, `RelatedUnavailableReason` and
`EvaluateRulesOptions` are unchanged. The cause rides a module-private
`WeakMap` keyed by the minted binding. Measured in the rebuilt
`dist/*.d.ts`, `referentialClearBinding` and `ReferentialClearCause`
have 0 hits in `index.d.ts`, `core.d.ts` and the `util` chunk; the
control `RelatedRecordBinding` has 1, 1 and 3 hits. There is no new
error code and no ledger change. Neither `OperationContext` nor
`update()` is touched (draft PR #20012), and nor are `packages/formula`
or `packages/spec`.
## For #20007
#20007 rewrites the neighbouring `no single related record`
prescription. It should reuse this sentence, with its own reference
name:
> Guard the rule on `REF` being set: make it the `then` of a
`conditional` rule whose `when` is `record.REF != null`.
In `rule-validator.ts` the clause after the colon is
`referenceGuardRepair(field)`, a module-local function, so #20007 can
call it rather than respell it. This PR does not edit #20007's
prescriptions. #20007 is not addressed here.
## Mechanism hypotheses (dispatch §2), measured at `fc6ddb87a`
- **H1 holds.** The cleanup context is `{ ...(context ?? {}),
__referentialFieldClear: true }`, and `resolvePredicateRelated` returns
`unbound` for it.
- **H2: the CEL-fault arm fires, not the `no single related record`
arm.** When the rule reads the cleared reference, `null.status` gives
`runtime: No such key: status`. When it reads another reference, the
bare id `'reg_1'.kind` gives `No such key: kind`. Both fall to
`unevaluableRuleError`. The `no-reference` arm fires only on an ordinary
write that empties the reference, and that text is unchanged here.
- **H3.** The validator could not learn which reference and which delete
from a boolean. The cause is carried on the cleanup UPDATE's own context
object only, not on the `OperationContext` interface, and there is no
public option.
- **H4 holds.** The `conditional` guard makes the delete succeed, and
`deleteBehavior` is a real key. Both are named exactly, with what each
does.
## Deviations, declared
- **Option D's written mechanism was not taken, only its direction.**
Option D sketched "an explicit unavailable binding with its own reason
... a new `RelatedUnavailableReason` member". That would refuse BEFORE
evaluation, which grows the refused set. Measured at the merge base, two
rules pass the cleanup today: `record.amount > 100 &&
record.account.status == 'closed'` with amount 50, and
`has(record.account.status) && ...`. A pre-evaluation refusal would
refuse both deletes. That is a narrowing, against the claim's `Clause-②:
no`, and the new union member is a public type the dispatch rules out.
So the text is chosen on the fault path, and the refused set does not
move (pinned).
- **The `engine.ts` edit spans two methods.** Besides
`cascadeDeleteRelations` (the stamp), it edits the
`__referentialFieldClear` short-circuit in `resolvePredicateRelated`
(the read), plus one named import. There is no other seam where the
cleanup's context reaches the validator without editing `update()`.
Neither hunk overlaps #20012's (`OperationContext` ~2235–2257,
`update()` ~13420–13669).
## Patch round 2 (`b22174cd4`): contract review 5822275989 (FAIL at
`9a31fc992`), item F3
- **F3, attribution keyed on the one key CEL reports.** When both
operands of `&&` / `||` fault, CEL reports the right-hand key. So at
`9a31fc992`, `record.kind == 'x' && record.account.status == 'closed'`
on a `crm_deal` declaring no `kind` got the cleanup text with the guard,
although the rule is broken on every write. So did its `||` form and
`previous.kind == 'x' && …`.
- **The fix:** `readsKeyOffTheTraversal` is replaced by the
key-independent `readsAnOwnColumnItLacks`, which checks EVERY own read
(see "How"). Those three shapes now keep today's generic text byte for
byte, which is base's text for them. The round-1 refinement stands: a
declared column never counts.
- **Pins:** red at `7a57a1092` (3 failed / 67 passed, the three F3
cleanup legs), then green at `b22174cd4` (70/70). The ordinary-write
control, where the rule refuses on its own `kind`, is green at both
heads.
- **Ablations:** each restore ended blob == HEAD.
- G1, the bare-read loop removed: 5 red.
- G2, the `previous` root unchecked: 2 red.
- **Refused set and envelope, re-measured:** a base-revert probe over 22
engine-level shapes moved 0 verdicts and 0 envelopes. The message
differs from base only in cleanup shapes where only the traversal
faults: 5 in the author's 22-shape probe, and 18 in the contract
review's 78-shape probe (5822996409).
- **Suites and checks:**
- the two pin files: 70/70;
- the plugin-security fixture: 18/18;
- objectql `--project local`: 311 files / 5262 tests;
- `typecheck` exits 0, with the debt unchanged;
- narrowed eslint: 0 / 0;
- the module-local names have 0 hits in the rebuilt d.ts;
- `dispatch-gates` gives the same 65 families (63 exit 0, 2 NOT
MEASURED, dist-reading families re-run after the rebuild);
- `check-issue-citations` exits 0.
- **`engine.ts`** is untouched this round.
- **Changeset:** only the "Unchanged" bullet is reworded, to the four
reads the code checks.
- **Accepted as is: a read through a reference into a column the related
object does not declare** (`record.account.nope`, with no `nope` on
crm_account).
- It gets the cleanup text with the guard. Every sentence of that text
is true, and the guard does let the delete through.
- The rule's breakage stays loud on every ordinary linked write
("'crm_account' declares no 'nope'").
- The generic text would be worse there: it would say the rule's own
object does not declare `nope`.
## Patch round 1 (`9a31fc992`): contract review 5821350148 (FAIL at
`b1de69f91`), items F1 and F2
- **F1, attribution by key name.** At `b1de69f91`, `record.status == 'x'
&& record.account.status == 'closed'` on a `crm_deal` that declares no
`status` got the cleanup text, although that rule is broken on every
write. The attribution now also requires that the key cannot fault on
the rule's own columns (`readsKeyOffTheTraversal`, module-local). That
shape keeps today's generic text byte for byte, and so does a
`previous.KEY` collision.
- The review's literal rule, "a key also read bare at the root keeps the
generic text", was measured and not taken. On an object that DOES
declare `status`, `record.status == 'x' || record.account.status ==
'closed'` would then say "which this object does not declare", which is
false: the declared column reads null and cannot fault.
- A bare read therefore counts only when the record does not hold the
key. That case is pinned, and so is its ablation (F1lit).
- **F2, the guard's cost: option (b).** The `conditional` guard is
offered only to a rule that reads through the reference the cleanup
empties. That rule already refuses every write leaving the reference
empty (`no single related record`), so the guard gives up nothing it
enforced (pinned).
- A rule that reads only through ANOTHER reference is offered
`deleteBehavior` alone. A guard there would stop judging it on every
record whose cleared reference is empty.
- Pinned: an account-less secret-region insert is refused with the
authored message both unguarded and under `deleteBehavior` 'restrict' /
'cascade'.
- **Pins:** red at `98cb79454` (4 failed / 61 passed, the new F1/F2 pins
against round 0's implementation), then green at `9a31fc992` (66/66).
- **Ablations:** each restore ended blob == HEAD.
- F1, the off-traversal check disabled: 2 red.
- F1lit, the bare read counted whether or not the record holds the key:
1 red, the declared-`status` leg.
- F2, the guard gated on emptiness only: 2 red.
- **Suites and checks:**
- the two pin files: 66/66;
- plugin-security `delete-reference-cleanup-system-identity.test.ts`:
18/18;
- objectql `vitest run --project local`: 311 files / 5258 tests;
- `typecheck` exits 0, with the debt unchanged;
- narrowed eslint: 0 / 0;
- the new module-local names have 0 hits in the shipped d.ts;
- `dispatch-gates` derives the same 65 families: 63 exit 0, and 2 are
NOT MEASURED (the workspace dist was not built; CI's Lint & Repo Gates
and Type Check are green at this head);
- `check-issue-citations --base fc6ddb8` exits 0.
- **`engine.ts`** is untouched this round.
- **The changeset:** the guard bullet and the "Unchanged" bullet are
rewritten to match, and an other-reference bullet is added. The example
block and the multi-value bullet are unchanged.
The "Tests" section below is round 0's readings at `b1de69f91`.
## Tests (all at `b1de69f91`)
- **Failing probe first.** `636550185` added the pins before any fix. At
that commit, `engine-predicate-relationship.test.ts` gave 3 failed / 28
passed, and the failures were exactly the new-text pins. Each received
today's generic text quoted above; the guard, cascade and control pins
were green, as they measure existing behaviour.
- **Two test files:**
- `pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2
src/engine-predicate-relationship.test.ts
src/validation/rule-relationship-traversal.test.ts` gives 63/63 passed.
- The new blocks add 12 end-to-end cases (real engine, in-file driver
double, no new double) and 5 evaluator cases.
- **`@objectstack/objectql`:**
- `vitest run --project local --maxWorkers=2`: 311 files / 5255 tests
passed.
- `test:repo`: 1 file / 5 tests passed.
- `typecheck` exit 0, including `check:test-typecheck`. The debt is
unchanged at 40 files / 234 errors / 65 pinned.
- **plugin-security, end to end on the real `SecurityPlugin`.**
`delete-reference-cleanup-system-identity.test.ts` stays green
unchanged, 18/18. It includes THE CONTRACT: a secret and a public
related row give identical outcomes, the message included, and zero
reads. A scratch copy that only printed the message (deleted after the
run) shows the new text arriving through the security middleware, so the
cause key survives that path:
```text
Cannot delete os_ehr_product (r_2): the delete clears `product` on the
os_ehr_inspection records that reference it,
and validation rule 'no_secret_line' on os_ehr_inspection could not be
evaluated on that write — it reads 'kind'
through `line`, ...
```
- **eslint.** `eslint --no-inline-config --format json` over the 4
touched TS files: 4 files, 0 errors, 0 warnings. The config has no
type-aware linting: `parserOptions` is `{ ecmaVersion, sourceType }`
only, with no `project`. So this diff cannot move the verdict on any
untouched file. The repo-wide run is CI's.
### Ablations
Each ablation went through `scripts/ablation-replace.mjs` in WRAP mode,
running the two test files. Every mutation was verified on disk (anchor
1 → 0, blob changed). Every restore ended with blob == HEAD and `git
diff HEAD` empty, and porcelain was 0 after the set.
| Ablation | Mutation | Red | Tests that went red |
|---|---|---|---|
| A | Engine binding dropped (`return unbound`) | 3 / 63 | the two
new-text pins, and the inherited-marker control leg |
| B | Validator branch dropped (`return unevaluable`) | 6 / 63 | those
3, plus the evaluator guard / multi / decides-nothing cases |
| C1 | Row-id match dropped | 1 / 63 | the inherited-marker pin |
| C2 | Object match dropped | 1 / 63 | the inherited-marker pin |
| D | Attribution check dropped | 2 / 63 | the byte-for-byte clear
control, and the own-columns evaluator case |
| E | Guard offered unconditionally | 1 / 63 | the multi-value case |
- My first try at A was refused by the tool: the replacement was a
substring of the anchor, so the count could not rise. Nothing ran. It
was re-run with a distinct marker; the result above is from that re-run.
### Gates
- **Derivation.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `b1de69f91` (merge base `fc6ddb87a`, 5
paths) derived 65 commands, and all 65 were run: 63 exit 0; 2 exit 3
(PREREQUISITE NOT MET, workspace `dist/` not built in this worktree),
NOT MEASURED: `check:dual-build-cjs-loads`, `check:type-check-debt`.
- `--ran` reconciliation: 65 derived, 63 run, 2 NOT-MEASURED, 0 UNRUN.
- As a supplementary reading, not the gate:
`require('./packages/objectql/dist/index.js')` loads.
- **Citations.** `node scripts/check-issue-citations.mjs --base
fc6ddb8`: exit 0. 7 citations were
judged across 2 files, and all resolve.
## Acceptance notes
- **Out of scope, class (a), not filed here** (the seat files it). The
`ConditionalValidationSchema` TSDoc examples in
`packages/spec/src/data/validation.zod.ts` (use cases 1–3) fail when
copied: `when: 'account_type = "enterprise"'`, `condition:
'approval_status = null'` and `'shipping_address = null OR
shipping_address = ""'` each give `parse: Unexpected character: =`.
`'order_total > 10000'` gives `Unknown variable: order_total`, because
it has no `record.` root. Those were measured through
`ExpressionEngine.evaluate`. The examples ship in the spec's published
`.d.ts` (`object.zod-*.d.ts`).
- **Changeset.** `.changeset/20006-cascade-fk-clear-refusal-text.md`,
`@objectstack/objectql: patch`, `Clause-②: no`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7766b62 commit b373596
5 files changed
Lines changed: 650 additions & 4 deletions
File tree
- .changeset
- packages/objectql/src
- validation
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
428 | 428 | | |
429 | 429 | | |
430 | 430 | | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 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 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 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 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
0 commit comments