onetaskgraph-github-projects 0.2.32

A onetaskgraph source over GitHub Projects.
Documentation
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
301
302
303
304
305
306
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
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
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
# What one session of the live journey costs

The GitHub Projects live journey reaches a rate-limited account that everything else this
repository does draws on too, and until this branch nobody had ever measured what it spends.
This is the measurement, and the before and after of the reduction taken against it.

## What these numbers are, and what they are not

Two quantities, both taken offline:

- **Requests** — how many HTTP requests one whole session sends. Exact: the source makes the
  same calls against the fixture board as against GitHub.
- **Node count** — the sum, over every GraphQL request, of the **worst-case number of nodes
  the document it sent may return** under the bindings that request really used. That is
  `nodeCount`, which GitHub limits **per query** and refuses a query above before executing
  it. It is arithmetic over the document's own text, computed by
  `github-graphql-node-count`.

**Neither of them is rate-limit points.** `cost` is metered by GitHub per call across
everything one credential does in an hour, and a document well under the node limit says
nothing about what GitHub charged for it — two numbers against two limits. What observes
what a **session** spends in points is the accounting in `src/accounting.rs`, which fills its
per-budget figures from the `x-ratelimit-*` headers a credentialed session's own responses
carry and prints them at the end of every run. That report comes from the live journey in
`tests/live.rs`, which runs in this repository's required check; it is not something this
file's figures can stand in for. **No figure in this file is a measurement of points**, and
that does not change below.

**What is computed offline in points is a per-document price, and it lives elsewhere.**
`worst_case_point_cost` prices one document from its own text under the largest page sizes
this source can be driven with, and `tests/point_cost.rs` pins every document in
`graphql::DOCUMENTS` at what it costs, so a shared fragment that gives a reduction back moves
a number somebody has to change. GitHub is the authority on its own pricing, and the
credentialed lane asks it: the `rateLimit(dryRun: true)` probe it already sends per read
document reports GitHub's own `cost`, and the reconciliation fails naming both figures when
they disagree. **None of that is what a session costs.** It is one document at a time, a
worst case rather than a bill, and the two quantities this file measures over a whole session
are still requests and worst-case nodes; what a whole session consumes of the hourly
allowance is still reported only by a credentialed run's own `x-ratelimit-*` headers.

One of the reductions recorded here **is** about points, and says so: *The board's own
`Labels` field* below argues from GitHub's published pricing rule — the one
`tests/journey/budget.rs` states in full — rather than from anything this file measured. Read
that section as arithmetic over a rule GitHub publishes, which is a different kind of claim
from the two quantities above and a weaker one than the accounting's observation; the
reductions recorded before it claim nothing about points at all. What that section argued by
hand is now a pinned figure: `tests/point_cost.rs` records the board read at **2 points**,
which is that argument's own "about 2" as the released `github-graphql-node-count` computes
it.

## How they are taken

`a_whole_session_of_the_live_journey_costs_what_the_record_beside_it_says`, in
`tests/plugin.rs`, drives the whole of `tests/journey` — the same code the credentialed
target drives — against this crate's loopback fixture board, with no credential and no
third-party API. The session it measures is the **whole** one: the schema verification, the
reconciliation of every read document's node count and price against GitHub's own, the board
and field lookups, every declared capability, this run's own cleanup and the end-of-run
orphan sweep, beside every request the source itself sends.
`tests/fixtures/session-cost.txt` is the checked-in record of the figures below, and that
test fails when a session stops costing them.

Two things differ between the two drives, and neither changes what is sent. One is where
the calls go — the fixture board rather than `api.github.com`, which is what makes the
measurement free. The other is pacing: the fixture drive turns off the interval this source
spaces its own content-creating mutations by, which changes how long a session takes and
nothing about how many requests it makes or what each carries.

## Before and after

|                | before  | after   |
| -------------- | ------: | ------: |
| **requests**   |     120 |      98 |
| **node count** | 1757401 | 1757301 |

Per call, before:

```
    1       0  DELETE /repos/{owner}/{repo}/labels/{name}
    2       0  GET /repos/{owner}/{repo}/labels
    1       0  POST /repos/{owner}/{repo}/labels
    1       0  account allowance after the node-count reconciliation
    1       0  account allowance before the node-count reconciliation
    6       0  adding an issue to the board
    2     100  board page size probe
    6       0  creating an issue
    6       0  deleting an issue
    3       0  filing an issue under its project
    6       0  live artifact cleanup
    3     300  live artifact lookup
    1       0  live label attachment
    1       0  live origin field cleanup
    1       0  live origin field creation
    1       0  mutation contract introspection
   28       0  mutation type introspection
    1   56100  node-count reconciliation while reading a project's tasks
    1     200  node-count reconciliation while reading an issue's dependencies
    1     560  node-count reconciliation while reading one issue
    1  260150  node-count reconciliation while reading the board
    1       0  node-count reconciliation while reading the destination repository
    1   56100  node-count reconciliation while searching this board's issues
    1       0  nominated board lookup
    2  112200  reading a project's tasks
    9    1400  reading an issue's dependencies
    4    2240  reading one issue
    5  1043251  reading the board
    2       0  reading the destination repository
    2       0  recording a dependency
    4  224400  searching this board's issues
    1       0  updating an issue
    4     400  writable field discovery
   10       0  writing a board field
```

After: `tests/fixtures/session-cost.txt`, which the test above holds the session to.

## What the budget precondition added afterwards

The reduction's two figures above are a comparison of the reduction, and they stand. What
`tests/fixtures/session-cost.txt` records **now** is one request more — **99 requests, node
count 1757301** — because the budget precondition that landed after it makes one:
`GET /rate_limit`, the account's allowance, before the session does any of the work it
exists to do. That read is deliberately in the record rather than outside it, so what the
gate itself costs is measured beside everything else instead of assumed; the node count is
untouched, because a REST call sends no document.

Nothing about the reduction moved. The rows are the reduction's rows plus one, and every
figure in the table above is still what those two changes were worth.

## The board's own `Labels` field, and what dropping it moved

`graphql::BOARD` was the last document selecting
`... on ProjectV2ItemFieldLabelValue{labels(…)}` — the shared `board_issue!` fragment had
already stopped, which is what took the three issue reads under GitHub's node limit. It is
now out of the board read too, and an item's labels come from its content's own `labels`
connection on every path.

In the two quantities this file measures offline, again in the record's own frame:

|                | before  | after  |
| -------------- | ------: | -----: |
| **requests**   |      99 |     99 |
| **node count** | 1757301 | 504801 |

**Not one request either way** — the selection was a field of a document already being
sent — and **1,252,500 worst-case nodes gone, 71% of the session's whole total.** The whole
of it lands in the two rows that send that document: `reading the board` goes from 1043251
nodes over 5 requests to 40751 over the same 5, and the one-request
`node-count reconciliation while reading the board` from 260150 to 10150. Every other row of
the record is byte-for-byte what it was, and `tests/node_count.rs` pins the document itself
at **260,150 → 10,150** nodes.

**This one is about points, which nothing here measures.** The published rule
`tests/journey/budget.rs` states in full is that a call costs `max(1, round(A / 100))`,
where `A` sums, over the call's connections, the product of the page sizes strictly above
each. That label connection sat under `fieldValues(first: 50)` under `items(first: 100)`, so
GitHub resolved it **5,000 times** for one page of board items — against roughly 202 for the
whole of the rest of that document. So `A` for a board read falls from about 5,202 to about
202, and `round(A / 100)` from about **52 points to about 2**. That is arithmetic over
GitHub's own rule rather than an observation: what observes points is still the accounting,
from the `x-ratelimit-*` headers a credentialed run's own responses carry, and this file
measures requests and worst-case nodes and nothing else.

What is given up is nothing. GitHub derives that field from the item's content: for `Issue`
content it *is* the issue's own labels, which the same document selects one level up, and a
`DraftIssue` exposes no `labels` field and cannot carry a value of the board field either —
`LABELS` is absent from `ProjectV2CustomFieldType`, so no project can create such a field,
and `ProjectV2FieldValue`, the whole of what `updateProjectV2ItemFieldValue` accepts, offers
no label member, so no item type's value is writable. A draft therefore reports no labels,
which is what it reported before this change too.

## Making a missed board membership recoverable, and what that document costs

Every document that reaches an issue carries a *page* of `Issue.projectItems` — the board
half of that issue — and this board's own entry can sit past it. That used to be refused,
naming the connection, because with no way to read the rest of it an unreached entry could
not be told from an issue this board really does not hold. `graphql::ISSUE_BOARD_ITEMS` is
the read that tells them apart: one issue's memberships and nothing else, resumed from the
page's own cursor and walked to exhaustion.

In the two quantities this file measures offline, in the record's own frame:

|                | before | after  |
| -------------- | -----: | -----: |
| **requests**   |     99 |    100 |
| **node count** | 504801 | 509901 |

**One request more, and 5,100 worst-case nodes.** Both are the same one thing, and it is
not a read of the board at all: the node-count reconciliation asks GitHub about every query
document this source sends, so a seventh document is a seventh probe, and 5,100 is that
document's own worst case as `tests/node_count.rs` pins it. **The session makes no recovery
read**, and that is the finding rather than an omission — every item on the fixture board
sits on one board, so its memberships arrive exhausted and there is nothing to recover.
That is what this costs a deployment whose issues sit on one board: nothing. Every other
row of the record is byte-for-byte what it was.

## The board memberships a read carries, and what shrinking them moved

`BOARD_ITEMS_PAGE_SIZE` — the page of `Issue.projectItems` that rides along on every
document reaching an issue — was ten and is now **three**. It sits under a page of a
hundred issues, so it multiplies through `SEARCH_ISSUES`, `SUB_ISSUES` and `ISSUE`, and
every point of it is paid whether or not any issue is on a second board. What had kept it
generous was the refusal above; with a miss recoverable, a constant that had to be generous
can be small.

In the two quantities this file measures offline, again in the record's own frame:

|                | before | after  |
| -------------- | -----: | -----: |
| **requests**   |    100 |    100 |
| **node count** | 509901 | 222516 |

**Not one request either way** — a page size is a bound on what a document may return
rather than on how many are sent — and **287,385 worst-case nodes gone, 56% of the
session's whole total.** The whole of it lands in the six rows that carry the fragment or
ask about it: `searching this board's issues` goes from 224400 nodes over 4 requests to
81600 over the same 4, `reading a project's tasks` from 112200 to 40800 over 2, and
`reading one issue` from 2240 to 812 over 4; the three matching reconciliation rows move
with them, 56100 → 20400, 56100 → 20400 and 560 → 203. Every other row of the record is
byte-for-byte what it was, and `tests/node_count.rs` pins the documents themselves at
**56,100 → 20,400** for both the search and the sub-issue read and **560 → 203** for the
issue read.

**Three is chosen against the recovery read's cost, which is a property of the product
rather than of this instrument.** At one, a deployment whose issues commonly sit on two or
more boards would pay that further request *per issue* — order N, against the one page read
per hundred issues a board-scoped read costs today. At three it is reached only by an issue
on four or more boards at once, which keeps the recovery path exceptional rather than
routine for a plausible deployment.

**One observation that is deliberately not a reason.** The estimate in
`tests/journey/budget.rs` divides by the smallest page size this source binds, which was ten
and is now three, so shrinking this constant loosens that bound: the estimate rises from 702
points to 934 even as the session's worst-case nodes fall by more than half. That is real,
and it is recorded here as an observation. It is **not** what chose the value — an estimate
deliberately sized high, whose job is to refuse a run rather than to describe one, must not
be what picks a production constant.

## Comments on a task, and what the five documents behind them moved

A task's comments are its issue's comments, so this source now sends five documents it did
not: `graphql::ISSUE_COMMENTS` and `graphql::COMMENT_ISSUE`, which are queries, and
`ADD_COMMENT`, `UPDATE_COMMENT` and `DELETE_COMMENT`, which are mutations. The journey drives
none of them against the board — nothing it proves reaches a comment — so what moved is the
two things the journey does for **every** document this source sends.

In the two quantities this file measures offline, in the record's own frame:

|                | before | after  |
| -------------- | -----: | -----: |
| **requests**   |    100 |    103 |
| **node count** | 222516 | 222616 |

**Three requests more, and 100 worst-case nodes.** Two of the requests are the node-count and
point-cost reconciliation asking GitHub about the two new query documents —
`reading a task's comments` at 100 nodes, which is its one `comments(first:)` connection and
nothing multiplied through it, and `reading which issue a comment is on` at none. The third is
the mutation schema introspection: three mutations bring three input and three payload types,
thirty-four types in all, and at GitHub's cap of two capped selections a document that is
**nine** documents rather than eight. The three mutations are not reconciled, because
`rateLimit` cannot be asked about a mutation; `tests/point_cost.rs` pins each of the five at
one point. Every other row of the record is byte-for-byte what it was.

The estimate in `tests/journey/budget.rs` moves with the record, as it is built to: **934
points to 941** against the GraphQL budget, and the REST estimate unchanged at 5 requests.

## The estimate the gate is sized from, and what it is not

`tests/journey/budget.rs` derives what this session will cost each of GitHub's two budgets
from the record above and a cost model it states in one place: **702 points** against the
GraphQL budget and **5 requests** against the REST one — 1955 points before the board read
stopped selecting the board `Labels` field. That is an *estimate*, deliberately
high — it is what refuses a run rather than what a run spends, and an estimate that is too
low is the thing that exhausts a shared budget.

**It is still not a measurement of points, and neither is anything else in this file.** What
observes points is the accounting, from the `x-ratelimit-*` headers a credentialed
session's own responses carry. What the gate adds is that the session report now prints the
estimate beside those figures, so a run says how far the model was from GitHub's own numbers
rather than asking anybody to trust it.

## What was kept, and what each change measured

Both changes landed in one commit, so read each summary's arithmetic off the per-call rows
rather than off the totals: subtracting the two totals gives their sum, not either one. The
intermediate figures below are measured rather than derived — the same session, driven with
only the first change applied, costs 100 requests and 1757401 nodes, which is 99 in the
table's frame. Every request figure in this section is in that frame, the reduction's own:
it sets the budget precondition's one `GET /rate_limit` aside, because that read landed
after the reduction and is no part of what either change was worth. The node counts need no
such reading — a REST call sends no document.

**Eight introspections instead of twenty-nine.** The schema verification asked GitHub for
the `Mutation` type and then for each of twenty-eight input and payload types, one request
each — the `mutation contract introspection` (1) and `mutation type introspection` (28) rows
of the before record, twenty-nine requests in all. GitHub allows any number of aliased root
fields on one query and `__type` is not a connection, so the contract folds into documents
that add nothing to the node count. What bounds the fold is a separate limit: **GitHub caps
how many times one document may select a given introspection field, at two.** A first
version of this change put all twenty-nine roots in one document and GitHub refused it
outright — `INTROSPECTION_LIMIT_EXCEEDED`, *"__Type.fields (14), __Type.inputFields (15)"* —
which cost the whole request rather than part of the answer, and which only a credentialed
run meets, because the loopback board answers whatever it is asked. So the fold is batched
to that cap: fifteen `inputFields` selections and fourteen `fields` ones, two of each per
document, is **eight** documents. Every name, input, payload, member and type signature the
checks held GitHub to is still asked for, from the same two tables. Those twenty-nine
requests become the eight `mutation schema introspection` rows of the after record: **29
replaced by 8, a net reduction of 21.**
**120 → 99 requests; node count unchanged at 1757401.**

`no_introspection_document_selects_a_capped_field_more_often_than_github_allows`, in
`tests/plugin.rs`, holds the batch to GitHub's own stated number and to asking about every
type exactly once, so a later fold cannot buy requests back by re-tripping that cap or by
dropping a type.

**One walk of the board's field connection instead of two.** `ensure_origin_field` and
`live_write_status` each read the project's fields for themselves, and one read answers
both — nothing the first creates can change what the second reads, because the `Status`
field was on the board before either ran. It is the `writable field discovery` row, which
falls from 4 requests and 400 nodes to 3 and 300: the last of the 22 requests the two
changes remove between them, and the whole of the node-count move.
**99 → 98 requests; 1757401 → 1757301 nodes.**

## The three places this step was told to look

Every figure in this section was measured **before** the board read stopped selecting the
board's own `Labels` field, so its node counts are in the frame of the 1757301-node session
above rather than the 504801-node one. None of the three findings turns on the size of that
number — each is a comparison between two sessions measured the same way, and all three came
out *no change kept* — so they are left as they were recorded rather than re-run.

**The lane's own setup, residue sweep and cleanup.** This is where both kept changes came
from, above. What is left there is not slack: the two allowance reads either side of the
node-count reconciliation are the observation that asking is free, the two board page-size
probes are the assertion that `max_page_size` is GitHub's own connection maximum and not a
guess, and each artifact-lookup and label-listing request is either a sweep or the
confirming re-read that says the sweep worked. The six documents of the node-count
reconciliation cannot be folded into one: `rateLimit(dryRun: true)` reports the count for the
whole operation it sits in, so merging six documents would answer their sum and reconcile
none of them.

One further candidate was examined and **rejected**: the cleanup deletes each artifact's
board item and then its issue, and on GitHub deleting an issue is understood to remove the
project items whose content it was, which would make the first of those two redundant and
save six requests a session. It is not kept, because this instrument cannot settle it — a
fixture board modelling that behaviour would only be answering back the assumption that was
put into it, and the property is GitHub's to demonstrate.

**The source resolving the same board and repository repeatedly.** Measured directly, as a
pair. Adding one more source instance to the journey and listing tasks through it costs
**one more request and 260150 more worst-case nodes**, and the whole of that lands in one
row, `reading the board`, which goes from 5 requests and 1043251 nodes to 6 and 1303401.
Making that same extra read through a source the journey already has costs **nothing at
all** — not one request and not one node — because a source holds its board and its
destination repository for its own lifetime and shares neither with the next one. So what this session pays really does scale
with the number of commands the journey stands in for rather than with how much it reads.
**No change is kept.** Every source this journey builds is load-bearing: the read-configured
one proves that a source configured with no `status_mapping` reads the board, each rebuild
inside `await_on_board` is what makes GitHub's own view visible rather than the writing
source's record of itself, and the rebuild the fixture settling loop makes per attempt is
what makes a change nothing here wrote visible at all — the label attached out of band, and
each further item GitHub has not reported yet. That loop settles on the first attempt
against this fixture and so builds exactly one source here, which is why the row above is
what it is; against GitHub it builds one more per attempt it has to make, because a source
answers every board question from one read and asking the same one twice asks GitHub once.
Collapsing any of them would buy one request by deleting a proof.

**The page sizes the source asks for.** Measured by driving the journey with its page limit
at 100 and at 5, against 50 as it stands. All three are whole sessions as this branch now
sends them, so each includes the budget precondition's one request:

| journey page limit | requests | node count |
| ------------------ | -------: | ---------: |
| 100                |       99 |    1757701 |
| **50 (kept)**      |   **99** | **1757301** |
| 5                  |       99 |    1756941 |

The finding is that **a caller's limit reaches the wire in exactly one document** — the
dependency read — and nowhere else: this source filters before it pages, so every other read
binds `MAX_PAGE_SIZE` whatever the caller asked for, and the other two sessions above differ
from the kept one in that row alone. `reading an issue's dependencies` carries 1800 nodes at
100, 1400 at 50 and 1040 at 5, over 9 requests in all three, which is both the whole 760-node
spread in the table — 400 nodes above the kept limit and 360 below it — and why every row of
it reads the same request count. **No change is kept.** Raising the limit costs nodes for nothing, and
lowering it looks free here only because this fixture board holds fewer rows than a page:
the round trip a smaller page buys appears on a board with more rows than the limit, which
is the half of the trade this instrument cannot see. `MAX_PAGE_SIZE` itself is out of
scope — making the source read less is a different decision from making it affordable.

## What a project copy costs, and what moved it

The live journey above is one kind of session. The one that spends this account's GraphQL
allowance day to day is different: an engine projecting a run onto a board copies the
project again on every node's status change, and until this change every such copy read
the whole project to find out what had changed. What a copy costs now has a record of its
own, `tests/fixtures/copy-cost.txt`, in the same two quantities and the same per-call shape
as this file's — requests, and each GraphQL request's worst-case node count under the
variables it really sent — **and no figure in it is points either.**

It is taken by `a_project_copy_into_a_board_costs_what_the_record_beside_the_session_record_says`
in `crates/onetaskgraph/tests/e2e/copy_cost.rs`, which drives the compiled binary against
the loopback fixture board the copy journeys use, with pacing off. It lives in the binary's
crate rather than beside its record because a copy is the engine's and no plugin crate may
depend on the engine; every request the board served is named and counted through this
crate's own `accounting`, so the two records cannot count one document two ways. The four
copies it measures are:

- **(a)** a whole copy of a project of 10 tasks the board does not hold yet;
- **(b)** the same whole copy again, unchanged, with every item recording the destination
  item it landed on at `onetaskgraph.origin` — the shape a write-back's shadow project has;
- **(c)** a member copy of that project naming one task whose status changed;
- **(d)** the same one-member copy of a project of 3 tasks.

|                         | before (a) | after (a) | before (b) | after (b) | (c)  | (d)  |
| ----------------------- | ---------: | --------: | ---------: | --------: | ---: | ---: |
| **requests**            |         69 |        68 |         47 |        23 |    9 |    9 |
| **node count**          |      53150 |     32750 |      19452 |     14583 | 1209 | 1209 |

Per call, before, measured on the same harness against the engine as it stood at 0.2.28:

```
(a)
   11       0  adding an issue to the board
   11       0  creating an issue
   10       0  filing an issue under its project
   11    2200  reading an issue's dependencies
    1   10150  reading the board
    1       0  reading the destination repository
    2   40800  searching this board's issues
   22       0  writing a board field

(b)
   12    2400  reading an issue's dependencies
   34    6902  reading one issue
    1   10150  reading the board
```

After: `tests/fixtures/copy-cost.txt`, which the test above holds every copy to.

**(a) 69 → 68, and (b) 47 → 23: each item's target resolved once.** A project copy planned
the project and every member to learn their destination ids, then planned each of them again
to land it, and compared each against a third read of the same destination item — and read
the project a fourth time to decide whether it had settled. Now every item is read and its
target decided once, before anything is written, and the read that found the target is the
read the comparison uses. In (b) that is the whole move: `reading one issue` falls from 34
to 11, one per item, and `reading an issue's dependencies` from 12 to 11. In (a), where no
item has a target yet, it is the second board-scoped search for the project's counterpart.
The one `reading the board` left in (b) is the walk for items the source no longer holds,
which a whole copy keeps because it was not told which members it carries.

**(c) and (d): 9 requests each, and the same 9.** A member copy reads, compares and writes
the project and the members it names and nothing else, runs no walk for orphans, and so does
not grow with the members it leaves alone. Its 9 are all reads and writes of one issue:
the project's issue and the member's issue, each read once for its target and its edges;
the member's issue read once more by the write, which takes the board's id and the
definitions of the `Status` and origin fields from that issue's own board entry instead of
from every page of the board; the member's blockers read before they are reconciled; the
update; and its two field writes. The test holds that by the documents sent — none is the
board read, the board-scoped search or a project's sub-issue walk — and by the node ids its
issue reads carry.

Two changes in this crate are what took the board out of (c) and (d), and both cost nothing
in the quantities above: neither adds a request or a node to any document.

- **`graphql::ISSUE_DEPENDENCIES` selects the issue's own `body`.** An issue's edges into
  other sources are recorded in its body, and reading them used to walk the board for that
  one field of one item. They now come out of the dependency read already made. A draft,
  whose body that read cannot carry, still reads the board.
- **The board entry every issue read carries names the board's `id`.** With it and the
  field definitions each value names, an update of an item this board holds reads that item
  rather than the board. An item that cannot say enough — one with no board entry naming
  the board, no value of the origin field, or no `Status` value when the write carries a
  status — is not guessed at: the write reads the board exactly as before. **That fallback
  costs one request more than it did** when the command has not already read the board —
  the item read that could not describe it — and nothing more once it has, because a board
  this command read is used as it is. A copy whose items were written by a copy, as a
  projection's are, does not take it.

What stays as it was: a member the copy names that records no origin is still looked for
by origin before it is created — that is correctness, not slack — and creating an item
still reads the board, which a create needs for the repository it files the issue in.