@issuegraph/editor 0.4.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +65 -9
  2. package/dist/a11y/baseline.d.ts +182 -0
  3. package/dist/a11y/baseline.d.ts.map +1 -0
  4. package/dist/a11y/baseline.js +434 -0
  5. package/dist/a11y/baseline.js.map +1 -0
  6. package/dist/audit/findings.d.ts +107 -1
  7. package/dist/audit/findings.d.ts.map +1 -1
  8. package/dist/audit/findings.js +113 -6
  9. package/dist/audit/findings.js.map +1 -1
  10. package/dist/audit/panel.d.ts +277 -0
  11. package/dist/audit/panel.d.ts.map +1 -0
  12. package/dist/audit/panel.js +462 -0
  13. package/dist/audit/panel.js.map +1 -0
  14. package/dist/audit/refused.d.ts +99 -0
  15. package/dist/audit/refused.d.ts.map +1 -0
  16. package/dist/audit/refused.js +276 -0
  17. package/dist/audit/refused.js.map +1 -0
  18. package/dist/audit/styles.d.ts +1 -1
  19. package/dist/audit/styles.d.ts.map +1 -1
  20. package/dist/audit/styles.js +429 -1
  21. package/dist/audit/styles.js.map +1 -1
  22. package/dist/audit/surface.d.ts +54 -12
  23. package/dist/audit/surface.d.ts.map +1 -1
  24. package/dist/audit/surface.js +53 -13
  25. package/dist/audit/surface.js.map +1 -1
  26. package/dist/create/draft.d.ts +36 -0
  27. package/dist/create/draft.d.ts.map +1 -1
  28. package/dist/create/draft.js +40 -0
  29. package/dist/create/draft.js.map +1 -1
  30. package/dist/create/keys.d.ts +134 -9
  31. package/dist/create/keys.d.ts.map +1 -1
  32. package/dist/create/keys.js +119 -20
  33. package/dist/create/keys.js.map +1 -1
  34. package/dist/firstpass/candidates.d.ts +17 -1
  35. package/dist/firstpass/candidates.d.ts.map +1 -1
  36. package/dist/index.d.ts +42 -17
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +45 -14
  39. package/dist/index.js.map +1 -1
  40. package/dist/overlay/render.d.ts +11 -3
  41. package/dist/overlay/render.d.ts.map +1 -1
  42. package/dist/overlay/render.js +76 -20
  43. package/dist/overlay/render.js.map +1 -1
  44. package/dist/overlay/request.d.ts +71 -0
  45. package/dist/overlay/request.d.ts.map +1 -0
  46. package/dist/overlay/request.js +129 -0
  47. package/dist/overlay/request.js.map +1 -0
  48. package/dist/overlay/styles.d.ts +1 -1
  49. package/dist/overlay/styles.d.ts.map +1 -1
  50. package/dist/overlay/styles.js +78 -7
  51. package/dist/overlay/styles.js.map +1 -1
  52. package/dist/picker/render.d.ts +26 -16
  53. package/dist/picker/render.d.ts.map +1 -1
  54. package/dist/picker/render.js +25 -36
  55. package/dist/picker/render.js.map +1 -1
  56. package/dist/picker/styles.d.ts +7 -7
  57. package/dist/picker/styles.d.ts.map +1 -1
  58. package/dist/picker/styles.js +7 -36
  59. package/dist/picker/styles.js.map +1 -1
  60. package/dist/picker/view.d.ts +8 -4
  61. package/dist/picker/view.d.ts.map +1 -1
  62. package/dist/picker/view.js +4 -3
  63. package/dist/picker/view.js.map +1 -1
  64. package/dist/picker/words.d.ts +12 -10
  65. package/dist/picker/words.d.ts.map +1 -1
  66. package/dist/picker/words.js +12 -8
  67. package/dist/picker/words.js.map +1 -1
  68. package/dist/reevaluate/parts.d.ts +130 -0
  69. package/dist/reevaluate/parts.d.ts.map +1 -0
  70. package/dist/reevaluate/parts.js +260 -0
  71. package/dist/reevaluate/parts.js.map +1 -0
  72. package/dist/reevaluate/render.d.ts +18 -11
  73. package/dist/reevaluate/render.d.ts.map +1 -1
  74. package/dist/reevaluate/render.js +21 -135
  75. package/dist/reevaluate/render.js.map +1 -1
  76. package/dist/reevaluate/styles.d.ts +1 -1
  77. package/dist/reevaluate/styles.d.ts.map +1 -1
  78. package/dist/reevaluate/styles.js +26 -0
  79. package/dist/reevaluate/styles.js.map +1 -1
  80. package/dist/reevaluate/view.d.ts +16 -1
  81. package/dist/reevaluate/view.d.ts.map +1 -1
  82. package/dist/reevaluate/view.js +21 -1
  83. package/dist/reevaluate/view.js.map +1 -1
  84. package/dist/reevaluate/words.d.ts +7 -0
  85. package/dist/reevaluate/words.d.ts.map +1 -1
  86. package/dist/scale/ladder.d.ts +62 -6
  87. package/dist/scale/ladder.d.ts.map +1 -1
  88. package/dist/scale/ladder.js +129 -11
  89. package/dist/scale/ladder.js.map +1 -1
  90. package/dist/scale/render.d.ts +44 -0
  91. package/dist/scale/render.d.ts.map +1 -1
  92. package/dist/scale/render.js +206 -28
  93. package/dist/scale/render.js.map +1 -1
  94. package/dist/scale/styles.d.ts +11 -5
  95. package/dist/scale/styles.d.ts.map +1 -1
  96. package/dist/scale/styles.js +133 -4
  97. package/dist/scale/styles.js.map +1 -1
  98. package/dist/workspace/chrome.d.ts +1 -1
  99. package/dist/workspace/chrome.d.ts.map +1 -1
  100. package/dist/workspace/chrome.js +48 -0
  101. package/dist/workspace/chrome.js.map +1 -1
  102. package/dist/workspace/firstpass.d.ts +166 -0
  103. package/dist/workspace/firstpass.d.ts.map +1 -0
  104. package/dist/workspace/firstpass.js +151 -0
  105. package/dist/workspace/firstpass.js.map +1 -0
  106. package/dist/workspace/host.d.ts +283 -11
  107. package/dist/workspace/host.d.ts.map +1 -1
  108. package/dist/workspace/host.js +614 -43
  109. package/dist/workspace/host.js.map +1 -1
  110. package/dist/workspace/inspector.d.ts +72 -2
  111. package/dist/workspace/inspector.d.ts.map +1 -1
  112. package/dist/workspace/inspector.js +26 -0
  113. package/dist/workspace/inspector.js.map +1 -1
  114. package/dist/workspace/mount.d.ts +75 -6
  115. package/dist/workspace/mount.d.ts.map +1 -1
  116. package/dist/workspace/mount.js +1851 -60
  117. package/dist/workspace/mount.js.map +1 -1
  118. package/dist/workspace/rail.d.ts +5 -2
  119. package/dist/workspace/rail.d.ts.map +1 -1
  120. package/dist/workspace/rail.js.map +1 -1
  121. package/dist/workspace/recovery.d.ts +155 -0
  122. package/dist/workspace/recovery.d.ts.map +1 -0
  123. package/dist/workspace/recovery.js +156 -0
  124. package/dist/workspace/recovery.js.map +1 -0
  125. package/dist/workspace/render.d.ts +779 -2
  126. package/dist/workspace/render.d.ts.map +1 -1
  127. package/dist/workspace/render.js +1727 -61
  128. package/dist/workspace/render.js.map +1 -1
  129. package/dist/workspace/selection.d.ts +11 -0
  130. package/dist/workspace/selection.d.ts.map +1 -1
  131. package/dist/workspace/selection.js +10 -0
  132. package/dist/workspace/selection.js.map +1 -1
  133. package/dist/workspace/styles.d.ts +1 -1
  134. package/dist/workspace/styles.d.ts.map +1 -1
  135. package/dist/workspace/styles.js +1405 -42
  136. package/dist/workspace/styles.js.map +1 -1
  137. package/package.json +4 -4
@@ -9,7 +9,7 @@
9
9
  * |-----------|-------------------------------------------------------|
10
10
  * | header | `auditOverlay` + `renderAuditHeader` |
11
11
  * | rail | `renderViewer(…, { projection: 'linear' })`, windowed |
12
- * | canvas | `renderScaleLadder` |
12
+ * | canvas | `canvasToolbar` + `renderScaleLadder` |
13
13
  * | inspector | `inspectorView`, rendered here |
14
14
  *
15
15
  * ## The audit left-bar is applied to the rail's SPEC, never to its markup
@@ -49,17 +49,83 @@
49
49
  * `styles.ts` writes structure against its custom properties.
50
50
  */
51
51
  import { type Theme, type ViewerDocument } from '@issuegraph/viewer';
52
- import type { ProjectedEdge } from '@issuegraph/store';
52
+ import type { EdgeId, InvalidCode, MutationId, OrderChange, OrderStatus, ProjectedEdge } from '@issuegraph/store';
53
53
  import type { AuditInput } from '../audit/findings.ts';
54
+ import { type AuditWords } from '../audit/panel.ts';
55
+ import { type EncodingRefusedBlockOptions } from '../audit/refused.ts';
56
+ import { type CreateDraft } from '../create/draft.ts';
57
+ import type { Point } from '../create/placement.ts';
58
+ import type { ChangeWords } from '../reevaluate/words.ts';
54
59
  import { type AuditOverlay } from '../audit/surface.ts';
55
60
  import { type ScaleState } from '../scale/commands.ts';
56
61
  import { type InspectorView } from './inspector.ts';
57
62
  import { type RailWindow, type RailWindowOptions } from './rail.ts';
63
+ import { type ConflictDiff } from './recovery.ts';
58
64
  import { type WorkspaceSelection } from './selection.ts';
59
65
  /** The four fixed positions. A closed union, which is what keeps `zone` safe. */
60
66
  export declare const ZONES: readonly ["header", "rail", "canvas", "inspector"];
61
67
  export type Zone = (typeof ZONES)[number];
62
68
  export interface WorkspaceWords {
69
+ /**
70
+ * §17c's vocabulary — the change summary's facets, its dismiss control and
71
+ * its direction words.
72
+ *
73
+ * OPTIONAL, AND ITS ABSENCE DRAWS NOTHING rather than drawing a default.
74
+ * `ChangeWords` states the doctrine and this obeys it exactly: the store
75
+ * ships the change as COUNTS so a host writes the sentence in its own
76
+ * language, and a default supplied here would take that choice back one layer
77
+ * down. So a host that has not supplied these gets no summary and no chips —
78
+ * not an English summary it cannot translate.
79
+ *
80
+ * It is the one shape that also keeps this a non-breaking addition: every
81
+ * host built against 0.12 renders exactly as it did.
82
+ */
83
+ readonly change?: ChangeWords | undefined;
84
+ /**
85
+ * Reads the freshness stamp in §17a's header — the frame's `as of 14:32 ↻`,
86
+ * minus the clock.
87
+ *
88
+ * THE NUMBER IS APPENDED, NOT INTERPOLATED, exactly as {@link whyRank}'s is
89
+ * and for the reason stated there: a `{n}` template makes every host
90
+ * reimplement the substitution, which is how a package that refuses to invent
91
+ * English ships a tiny template language instead. `Freshness.asOf` and
92
+ * `Freshness.age` are the host's own strings and are printed verbatim.
93
+ *
94
+ * THE STALE FLAG CARRIES NO WORD, unlike layer 1's stamp, which appends a
95
+ * literal ` · stale`. §17a's stamp is `as of 14:32 ↻` and says nothing more,
96
+ * so staleness rides on `data-stale` for the stylesheet to treat. A word here
97
+ * would be a fourth thing to translate for a state the frame draws silently.
98
+ */
99
+ readonly asOf: string;
100
+ /**
101
+ * The noun after the backlog's total — frame 17a's `312 open`.
102
+ *
103
+ * IT WORDS `Adoption.counts.total`, WHICH IS NOT `OrderCounts`. Two different
104
+ * pairs reach this surface and only one of them is §17a's header content: the
105
+ * adoption pair says how much of the backlog is encoded, and the order pair
106
+ * (`ranked` / `readyNow` / `held`) says how the order splits. The second stays
107
+ * in the rail, where it belongs to the order it describes.
108
+ */
109
+ readonly open: string;
110
+ /**
111
+ * The noun after the encoded count — frame 17a's `64 encoded`, wording
112
+ * `Adoption.counts.declaring`.
113
+ *
114
+ * SEPARATE FROM {@link open} RATHER THAN ONE PHRASE, because the two numbers
115
+ * are the package's and the two nouns are the host's; a single string would
116
+ * have to carry both figures and would be a template again.
117
+ */
118
+ readonly encoded: string;
119
+ /**
120
+ * The panel's own heading — frame 17a's `INSPECTOR`.
121
+ *
122
+ * UNCONDITIONAL, and that is what it is for: the zone used to begin with
123
+ * whatever the selection happened to resolve to, so a reader who had selected
124
+ * nothing met a bare sentence in an unnamed column. The caps are the
125
+ * stylesheet's, as they already are for `WHY RANK` and `RELATIONSHIPS`, so
126
+ * the word supplied here reads as a word.
127
+ */
128
+ readonly inspector: string;
63
129
  /** Shown in the inspector when nothing is selected. */
64
130
  readonly nothingSelected: string;
65
131
  /**
@@ -72,8 +138,532 @@ export interface WorkspaceWords {
72
138
  * command it publishes is `clear`, and that is the whole of its behaviour.
73
139
  */
74
140
  readonly clearSelection: string;
141
+ /**
142
+ * §17k's strip control while the canvas is collapsed — "open the canvas".
143
+ *
144
+ * REQUIRED, UNLIKE {@link CanvasWords}, AND THAT IS THE WHOLE REASON IT IS
145
+ * NOT IN IT. The canvas caption is optional because a surface without it
146
+ * still draws every fact; this control is the only way back out of §17k's
147
+ * strip, and a host that omitted it would ship a layout with a zone the
148
+ * reader cannot reach below `1120`. An unreachable zone is not a degraded
149
+ * caption.
150
+ *
151
+ * IT NAMES THE ZONE, NOT A DIRECTION. The strip is drawn at the canvas's own
152
+ * track, so "open" and "show" both read as acting on what is beside the word;
153
+ * the fixture and the demo both say which zone out loud, because a strip four
154
+ * characters wide, turned on its side, carries no other label.
155
+ */
156
+ readonly showCanvas: string;
157
+ /**
158
+ * The same control while the canvas is open.
159
+ *
160
+ * ITS OWN MEMBER RATHER THAN REUSING {@link showCanvas}, on
161
+ * {@link RailWords.hide}'s reasoning exactly: the control carries
162
+ * `aria-expanded`, which flips, and a label that did not flip with it would
163
+ * tell a screen-reader user the canvas is open and offer to open it in one
164
+ * breath.
165
+ */
166
+ readonly hideCanvas: string;
75
167
  /** Names the relationships list. */
76
168
  readonly relationships: string;
169
+ /**
170
+ * §17a's `WHY RANK n` heading, without the number.
171
+ *
172
+ * THE NUMBER IS APPENDED, NOT INTERPOLATED INTO A HOST TEMPLATE. A rank is
173
+ * the one part of this heading the package knows and the host does not, and
174
+ * a `{n}` placeholder would make every host reimplement the substitution —
175
+ * which is how a package that refuses to invent English ends up shipping a
176
+ * tiny template language instead.
177
+ */
178
+ readonly whyRank: string;
179
+ /**
180
+ * The same heading for a HELD slot, which has no rank to name.
181
+ *
182
+ * A held slot's rank is `null` by construction (`@issuegraph/derive` assigns
183
+ * `ready ? (rank += 1) : null`), so "why rank —" would be a heading about a
184
+ * position that does not exist. The design's own §16d ruling is the same one:
185
+ * a held unit prints the em dash rather than a number.
186
+ */
187
+ readonly whyHeld: string;
188
+ /**
189
+ * What the rail says when the audit filter is on and NOTHING matched it:
190
+ * §17's *"No flagged rows match."*
191
+ *
192
+ * A SEPARATE STRING FROM EVERY OTHER EMPTY SENTENCE, because it is about a
193
+ * different world. The others say the order is short; this one says the order
194
+ * is intact and the view is narrow.
195
+ */
196
+ readonly filteredEmpty: string;
197
+ /**
198
+ * The count that makes the sentence above unmisreadable: *"8 issues are
199
+ * ranked"*.
200
+ *
201
+ * A FUNCTION, WHICH IS THE ONE PLACE THIS FILE TAKES ONE, and the reason is
202
+ * the reason `whyRank` refuses to: there the number is APPENDED to a fixed
203
+ * phrase and a host can write the phrase; here the number sits INSIDE the
204
+ * sentence and its grammar moves with it — *"1 issue is ranked"* against
205
+ * *"8 issues are ranked"*. A fixed string plus a number cannot say both, and
206
+ * a `{n}` placeholder would make every host implement the substitution. So
207
+ * the host is handed the count and writes its own sentence, in its own
208
+ * language, with its own plural rule.
209
+ */
210
+ readonly filteredDenominator: (ranked: number) => string;
211
+ /** The way back out, which §17d requires be one click: *"clear the filter"*. */
212
+ readonly filteredClear: string;
213
+ /**
214
+ * What the rail says while the filter is on AND some rows still match.
215
+ *
216
+ * A SECOND HEADLINE RATHER THAN A REUSE OF `filteredEmpty`, because they are
217
+ * opposite statements: that one says nothing matched, this one says the list
218
+ * is narrowed. Saying "no flagged rows match" above three flagged rows is the
219
+ * false statement §2 wrote its ruling about.
220
+ */
221
+ readonly filteredActive: string;
222
+ /**
223
+ * Joins the rest of a together unit: §17a ends *"then worked with #514 as one
224
+ * unit"*, and this is that phrase minus the members, which the package names.
225
+ */
226
+ readonly workedAsOneUnit: string;
227
+ /**
228
+ * Stated when the subject has no relationships at all.
229
+ *
230
+ * A STATED EMPTY IS NOT THE SAME AS AN ABSENT LIST. The panel used to draw
231
+ * the `RELATIONSHIPS` heading and then nothing under it, which reads as a
232
+ * list that failed to load rather than as an issue that is genuinely
233
+ * unrelated to everything — and "this issue is connected to nothing" is one
234
+ * of the more actionable things a grooming surface can say.
235
+ */
236
+ readonly noRelationships: string;
237
+ /**
238
+ * The control that begins a relationship from the selected issue — frame
239
+ * 17a's `+ add`, publishing `add`.
240
+ *
241
+ * MOVED HERE FROM `MountWords`, where it was while the mount drew the button.
242
+ * `renderWorkspace` is the published surface and the panel is now what draws
243
+ * it, so a host rendering the markup without mounting gets the control rather
244
+ * than a panel that can only be read. `MountWords` extends this, so the mount
245
+ * still reads the same field and no host supplies two.
246
+ */
247
+ readonly addRelationship: string;
248
+ /**
249
+ * §17a's `ADD RELATIONSHIP` heading, over the numbered kinds.
250
+ *
251
+ * NOT {@link WorkspaceWords.addRelationship}, AND THE FRAME IS WHY. That
252
+ * field is the CONTROL's label — frame 17a writes it `+ add`, on the
253
+ * relationships header row — and this is the HEADING over the five numbered
254
+ * choices, which the frame writes in full. They are two strings in the frame
255
+ * and reusing one for both would put a control's label where a section name
256
+ * belongs, in a panel whose other two headings are already section names.
257
+ *
258
+ * REQUIRED, on the same rule the three header nouns took. An optional field
259
+ * would draw the heading only for a host that opted in, leaving every other
260
+ * host with the frame's two moved placements and not its third — a panel that
261
+ * half-applies the pass this exists to complete. The usual argument for
262
+ * optional, that a host built against the previous version renders
263
+ * byte-identically, has nothing to preserve here: this release moves `+ add`
264
+ * and the kind digits for every host regardless of what it passes.
265
+ */
266
+ readonly addRelationshipHeading: string;
267
+ /**
268
+ * The control that abandons a draft — frame 17b's escape hatch beside the
269
+ * numbered kind list, publishing `cancel`.
270
+ *
271
+ * MOVED HERE FROM `MountWords` WITH THE STEP IT BELONGS TO. The kind list and
272
+ * the control that withdraws from it are one affordance; leaving the cancel
273
+ * in host chrome while the list moved into the package would have split a
274
+ * control group across two layers, and a draft the reader cannot abandon with
275
+ * a pointer is the failure `create/keys.ts` names for the keyboard. The
276
+ * mount's target-search step still reads this field, through the extension.
277
+ */
278
+ readonly cancel: string;
279
+ /**
280
+ * Names the issue a live draft was begun from, when that is not the panel's
281
+ * subject — "relating from", read before the reference the package supplies.
282
+ *
283
+ * IT EXISTS BECAUSE THE TWO CAN DIVERGE AND THE DRAFT MUST STILL BE
284
+ * CANCELLABLE. See {@link createStep}: a draft begun by `R` on a together
285
+ * unit's non-lead member, or one outlived by a change of selection, leaves
286
+ * the kind step live under a panel headed by a different issue. The step is
287
+ * drawn anyway — it carries the only pointer cancel there is — so it has to
288
+ * say whose it is, and the package invents that phrase no more readily than
289
+ * it invents {@link WorkspaceWords.workedAsOneUnit}. The REFERENCE is the
290
+ * package's, appended as its own element, so a host writes a phrase and never
291
+ * a template.
292
+ */
293
+ readonly relatingFrom: string;
294
+ /**
295
+ * The accessible name of a row's remove control — the `✕` in frame 17a's
296
+ * right-hand slot.
297
+ *
298
+ * ROW-INDEPENDENT WORDING. Which relationship it removes is in the markup
299
+ * (`data-ig-target`) and in the row the button sits on, not in this string, so
300
+ * a host writing "remove #488" here would be wrong on every other row.
301
+ *
302
+ * DISTINCT FROM `MountWords.deleteRelationship`, which names a different
303
+ * control: that is the mount's labelled button for the one SELECTED edge, and
304
+ * this is a glyph button repeated once per row. A glyph carries no name of its
305
+ * own — see {@link relationshipSpec} — so the two cannot share one string
306
+ * without one of them reading wrongly.
307
+ */
308
+ readonly remove: string;
309
+ /**
310
+ * The right-hand marker on an incoming row — frame 17a's `inbound`.
311
+ *
312
+ * IT IS A STATED FACT, NOT A MISSING CONTROL. An inbound edge's field lives in
313
+ * the OTHER issue's body, so this panel's subject cannot declare it away; the
314
+ * slot says which relationship this is rather than leaving a gap where the
315
+ * other rows have a `✕`.
316
+ */
317
+ readonly inbound: string;
318
+ /**
319
+ * The control that reverses a directed relationship — frame 17b's `⇅ flip`.
320
+ *
321
+ * MOVED HERE FROM `PickerWords` WITH THE CONTROL ITSELF, on the same rule
322
+ * that brought `addRelationship` and `cancel` down from `MountWords`: the
323
+ * surface that draws a control owns the word for it, so a host rendering the
324
+ * markup without mounting gets the affordance rather than a panel it can only
325
+ * read. §17b calls direction “the single most common encoding mistake”, which
326
+ * made this the worst control to have reachable from one rendering path only.
327
+ *
328
+ * IT IS NOT #144's MOVE, AND THE DIFFERENCE IS VISIBLE TO EVERY HOST.
329
+ * `addRelationship` travelled between two interfaces in one `extends` chain,
330
+ * so `words.addRelationship` read the same before and after. `MountWords`
331
+ * holds the picker's vocabulary in a NESTED field, so this word's path
332
+ * changed: `words.picker.flip` became `words.flip`.
333
+ *
334
+ * ROW-INDEPENDENT WORDING, exactly as {@link WorkspaceWords.remove} is. Which
335
+ * relationship it reverses is the selected row it sits in, never this string.
336
+ */
337
+ readonly flip: string;
338
+ /**
339
+ * Why the store refused an edit, keyed by its code.
340
+ *
341
+ * TOTAL OVER `InvalidCode`, so a code the store adds is a compile error in
342
+ * every host rather than a capsule that renders blank on the one refusal
343
+ * nobody anticipated. The store's own `InvalidReason.message` is deliberately
344
+ * not used: it is one sentence in one language chosen by the layer that
345
+ * detected the refusal, and `overlay/grammar.ts` already records that an
346
+ * invalid edge's sentence is the host's, keyed off the code.
347
+ */
348
+ readonly refusals: Readonly<Record<InvalidCode, string>>;
349
+ /**
350
+ * §17b's recovery cards, in the host's own words.
351
+ *
352
+ * `retry` AND `retryOnLatest` ARE TWO WORDS BECAUSE THEY ARE TWO CALLS. §17b
353
+ * names the failed affordance "retry" and the conflict affordance "retry on
354
+ * latest", and they reach `store.retry` and `store.retryOnLatest`
355
+ * respectively — the second of which re-reads and ADOPTS the upstream
356
+ * document as the new base before re-dispatching. One string for both would
357
+ * make one card lie about what pressing it does, and it would be the card
358
+ * whose consequence most needs stating.
359
+ */
360
+ readonly recovery: RecoveryWords;
361
+ /**
362
+ * §17d's findings panel, in the host's own words.
363
+ *
364
+ * NESTED, like `recovery` and `refusals` beside it, because it is a record
365
+ * over a closed class table rather than a loose string — flattening four
366
+ * chips and four titles into this interface would put eight members here
367
+ * whose relationship to each other is only legible from their names.
368
+ */
369
+ readonly audit: AuditWords;
370
+ /**
371
+ * The canvas toolbar's words — frame 17a's caption over the graph.
372
+ *
373
+ * OPTIONAL, AND ITS ABSENCE DRAWS NO TOOLBAR, on `change`'s reasoning
374
+ * exactly: a required member here would break every host built against the
375
+ * current version, and a default supplied here would be this package writing
376
+ * an English caption for a host that may not speak English. So a host that
377
+ * has not worded the row gets the canvas it already had.
378
+ */
379
+ readonly canvas?: CanvasWords | undefined;
380
+ /**
381
+ * The order rail's footer — frame 17a's `248 with no relationships · show`.
382
+ *
383
+ * OPTIONAL, AND ITS ABSENCE DRAWS NO FOOTER, on `canvas`'s reasoning exactly:
384
+ * a required member here would break every host built against the current
385
+ * version, and a default supplied here would be this package writing English
386
+ * for a host that may not speak it.
387
+ *
388
+ * IT IS ALSO THE ONE WORDS MEMBER WHOSE PRESENCE MOVES SOMETHING IN ANOTHER
389
+ * ZONE, and that is worth stating where a host reads it. Supplying this draws
390
+ * the count and its control at the foot of the rail and turns the canvas
391
+ * ladder's own chip off, so the surface offers one control rather than two.
392
+ * The list the control opens is drawn in the CANVAS either way — the rail is
393
+ * virtualized and cannot hold it; `railFooter` records the arithmetic. The
394
+ * `isolatedChip` argument at the ladder call records why the two conditions
395
+ * are safely the same.
396
+ */
397
+ readonly rail?: RailWords | undefined;
398
+ }
399
+ /**
400
+ * Frame 17a's canvas toolbar reads `focus: #512 · 1 hop · 6 of 312 shown`, and
401
+ * every word in it is the host's.
402
+ *
403
+ * NUMBERS ARE APPENDED, NEVER INTERPOLATED, which is why this is four words
404
+ * rather than one sentence with two `{n}` holes in it. {@link
405
+ * WorkspaceWords.whyRank} states the doctrine and the reason: a template makes
406
+ * every host reimplement the substitution. The row is assembled as spans —
407
+ * `<focus> <key> · <n> <of> <total> <shown>` — so a host whose language orders
408
+ * those differently is no worse off than it is with `whyRank`, and no host has
409
+ * to parse a format string.
410
+ *
411
+ * THE DOCTRINE IS THIS INTERFACE'S, NOT THE WHOLE PACKAGE'S, and saying
412
+ * otherwise would be refuted by the markup directly beneath this row. The scale
413
+ * module writes English with numbers in it — the refusal sentence
414
+ * (`scale/ladder.ts`), the isolated chip's label, and a literal button label in
415
+ * `scale/render.ts`. That is a pre-existing inconsistency; the row follows
416
+ * `WorkspaceWords`' rule because the row is composed in the workspace, and it
417
+ * neither resolves nor extends the other one.
418
+ *
419
+ * THE FRAME'S `1 hop` IS NOT HERE, and its absence is a decision rather than an
420
+ * omission. `scaleLadder` narrows the canvas to a whole CONNECTED COMPONENT,
421
+ * not to an n-hop ball around the focus: there is no hop radius in
422
+ * `ScaleState`, in `ScaleLadder`, or in the viewer's graph projection, and a
423
+ * component can be ten hops deep. Printing `1 hop` would state a bound the
424
+ * canvas does not honour — the same defect as a rank the rail is not sure of —
425
+ * so the row prints what the ladder computed and nothing else.
426
+ */
427
+ export interface CanvasWords {
428
+ /**
429
+ * Leads the focus clause — the frame's `focus:`, minus the colon, which is
430
+ * punctuation this package draws rather than a word a host translates.
431
+ */
432
+ readonly focus: string;
433
+ /**
434
+ * Sits between the two counts: the frame's `6 OF 312`.
435
+ *
436
+ * ITS OWN MEMBER RATHER THAN PART OF `shown`, because the two numbers
437
+ * straddle it. Folding it into either neighbour would put a number in the
438
+ * middle of a host's string, which is the interpolation this interface's
439
+ * whole shape exists to avoid.
440
+ */
441
+ readonly of: string;
442
+ /** Trails the counts — the frame's `6 of 312 SHOWN`. */
443
+ readonly shown: string;
444
+ /**
445
+ * The trailing pill — the frame's `edit mode`.
446
+ *
447
+ * A STATE, NOT A CONTROL. The workspace is the editing surface and there is
448
+ * no read-only workspace to toggle into, so this names what the reader is
449
+ * looking at rather than offering to change it. It is drawn as text, never as
450
+ * a button, because a pill that looked pressable and did nothing is the dead
451
+ * control `headerControls` refuses to draw.
452
+ *
453
+ * OPTIONAL AND SEPARATELY SO, on `headerControls`' own reasoning rather than
454
+ * on this group's. `renderWorkspace` returns markup, and markup can be served
455
+ * without `mountWorkspace` — nothing on such a surface is editable, so a pill
456
+ * asserting that it is would be a claim the render layer cannot honour. The
457
+ * host knows which it built, so the host says: supply this only where the
458
+ * surface was mounted. Absent, the caption still draws and the pill does not.
459
+ */
460
+ readonly editMode?: string | undefined;
461
+ }
462
+ /**
463
+ * Frame 17a's rail footer reads `248 with no relationships · show`, and every
464
+ * word in it is the host's.
465
+ *
466
+ * NUMBERS ARE APPENDED, NEVER INTERPOLATED — {@link CanvasWords} states the
467
+ * doctrine and {@link WorkspaceWords.whyRank} the reason. The row is assembled
468
+ * as spans, `<count> <isolated>`, so a host is never handed a format string.
469
+ *
470
+ * `IsolatedChip.label` IS NOT REUSED, and the reason is the one `CanvasWords`
471
+ * already records: the scale module writes English with numbers in it, that is
472
+ * a pre-existing inconsistency, and a row composed in the workspace follows the
473
+ * workspace's rule rather than inheriting it. Reusing the label here would have
474
+ * been the cheaper line and would have imported the defect.
475
+ */
476
+ export interface RailWords {
477
+ /** Trails the count — the frame's `248 WITH NO RELATIONSHIPS`. */
478
+ readonly isolated: string;
479
+ /** The toggle while the list is closed — the frame's `show`. */
480
+ readonly show: string;
481
+ /**
482
+ * The toggle while the list is open.
483
+ *
484
+ * ITS OWN MEMBER RATHER THAN REUSING `show`. The control carries
485
+ * `aria-expanded`, which flips; a label that did not flip with it would tell
486
+ * a screen-reader user the list is open and offer to open it in one breath.
487
+ */
488
+ readonly hide: string;
489
+ }
490
+ export interface RecoveryWords {
491
+ /** Names the state on a failed card. */
492
+ readonly failed: string;
493
+ /** Names the state on a conflicted card. */
494
+ readonly conflict: string;
495
+ readonly viewDiff: string;
496
+ /** `failed` → `store.retry`. */
497
+ readonly retry: string;
498
+ /** `conflict` → `store.retryOnLatest`, which adopts upstream as the new base. */
499
+ readonly retryOnLatest: string;
500
+ readonly discardMine: string;
501
+ /** Heads the edges the held document has and the reader's does not. */
502
+ readonly upstreamOnly: string;
503
+ /** Heads the reader's own unlanded edit, where it ADDS a relationship. */
504
+ readonly mineOnly: string;
505
+ /** Heads the reader's own unlanded edit, where it REMOVES one. */
506
+ readonly mineRemoved: string;
507
+ /** Heads relationships both sides hold pointing opposite ways. */
508
+ readonly carrierReversed: string;
509
+ /** Heads the issues the two documents disagree about. */
510
+ readonly issuesChanged: string;
511
+ /** Shown when the diff has nothing to show on THIS panel. */
512
+ readonly diffEmpty: string;
513
+ /**
514
+ * Why a `retry on latest` could not even read.
515
+ *
516
+ * The store's refresh is half of one operation: when it fails the record is
517
+ * restored verbatim and nothing is dispatched, so without this the button
518
+ * reads as dead on exactly the failure the store went to trouble to make
519
+ * observable.
520
+ */
521
+ readonly retryFailed: string;
522
+ /**
523
+ * Heads the region for recoveries with no panel to sit on.
524
+ *
525
+ * See {@link WorkspaceRecovery.carrier}: a write about issues this backlog
526
+ * does not hold has no panel, and dropping it would leave the reader's work
527
+ * unrecoverable.
528
+ */
529
+ readonly unplaced: string;
530
+ }
531
+ /**
532
+ * One unsettled write the reader can still act on: §17b's `failed` and
533
+ * `conflict`.
534
+ *
535
+ * NARROW FOR THE REASON {@link WorkspaceRefusal} IS, and shaped the same way.
536
+ * The whole write ledger is the larger surface this deliberately is not: a
537
+ * `WriteRecord` carries a `Mutation` and, on a conflict, an entire second
538
+ * `GraphDocument`, none of which a renderer can draw. What a card needs is the
539
+ * state, the reason or the difference, and which panel states it.
540
+ *
541
+ * A UNION, BECAUSE THE TWO STATES CARRY DIFFERENT FACTS. `WriteRecord` is
542
+ * already a union on this exact line. Two optionals would admit a conflict with
543
+ * no diff and a failure with one, and the renderer would need a runtime guard
544
+ * for a case the type can refuse outright.
545
+ *
546
+ * NO `phantom` EQUIVALENT, and the absence is deliberate rather than an
547
+ * oversight. {@link WorkspaceRefusal.phantom} exists because a refusal either
548
+ * JOINS a relationship row or REPLACES it, and getting that wrong deletes a
549
+ * removable relationship from the panel. A recovery card is its own element in
550
+ * its own region and replaces nothing, so the question does not arise.
551
+ */
552
+ export type WorkspaceRecovery = {
553
+ readonly kind: 'failed';
554
+ readonly mutationId: MutationId;
555
+ readonly edgeId: EdgeId;
556
+ readonly carrier: string | null;
557
+ /**
558
+ * The adapter's own sentence, verbatim.
559
+ *
560
+ * NOT KEYED OFF A CODE, and the asymmetry with {@link
561
+ * WorkspaceWords.refusals} is the argument. The store's refusal codes are
562
+ * a CLOSED set the store owns, so a host can be made total over them. An
563
+ * adapter's failure is an open one — a status, a rate limit, a network
564
+ * message — and there is no code to key on, so the sentence travels.
565
+ */
566
+ readonly reason: string;
567
+ } | {
568
+ readonly kind: 'conflict';
569
+ readonly mutationId: MutationId;
570
+ readonly edgeId: EdgeId;
571
+ readonly carrier: string | null;
572
+ /** Both sides, held apart. See `recovery.ts`. */
573
+ readonly diff: ConflictDiff;
574
+ /**
575
+ * Why the last refresh could not read, or `null`.
576
+ *
577
+ * A `retry on latest` whose READ fails restores the record exactly as it
578
+ * was and dispatches nothing — the conflict keeps its held document so it
579
+ * can still be compared against. Without this the card re-renders
580
+ * identically and the button reads as dead.
581
+ */
582
+ readonly refreshError: string | null;
583
+ };
584
+ /**
585
+ * One refused edit, as the panel needs it: which edge, and why.
586
+ *
587
+ * NARROW ON PURPOSE, AND THE NARROWNESS IS THE ARGUMENT. `WorkspaceOptions.projected`
588
+ * already carries every edge's states, and `invalid` is among them — but a
589
+ * state says an edge was refused and never says why, because the code lives on
590
+ * `WriteRecord.reason`, which reaches no renderer. The alternative was to hand
591
+ * the panel the whole write ledger, which is the input #137's conflict cards
592
+ * need and is a much larger surface: both versions of a conflicted body, the
593
+ * upstream document, the retry and discard affordances. This is one code per
594
+ * edge id, derivable in a shell from what the store already publishes, and
595
+ * nothing here is blocked on that larger shape.
596
+ */
597
+ export interface WorkspaceRefusal {
598
+ readonly edgeId: EdgeId;
599
+ readonly code: InvalidCode;
600
+ /**
601
+ * The issue whose panel states this refusal.
602
+ *
603
+ * THE EDIT'S OWN SUBJECT, DECIDED BY WHOEVER MADE THE EDIT. This panel used
604
+ * to work it out for itself, by asking whether the refused edge's identity
605
+ * named the issue on show — a derivation entirely separate from the one that
606
+ * produced the subject, and free to disagree with it. It did, three times
607
+ * over: an unfiltered orphan branch drew every refusal on every panel; a
608
+ * retype whose projection hid the edge the panel was filtered to left the
609
+ * panel with no subject and the reason nowhere; and a draft begun from a
610
+ * together-unit partner produced an edge naming the partner while
611
+ * `inspectorView` had canonicalized the panel onto the slot's lead, so
612
+ * nothing matched. None of the three is a question this renderer can answer
613
+ * from an identity, because none of them is about the identity.
614
+ *
615
+ * A MOUNT KNOWS IT WITHOUT GUESSING, BECAUSE IT WROTE IT DOWN. `editCarrier`
616
+ * in `host.ts` answers it — for a create from a draft, for a retype or flip
617
+ * whose produced identity differs from the one the reader named, and for a
618
+ * delete — and `mountWorkspace` asks it as the edit goes OUT, while the
619
+ * document still holds the relationship, then keeps the answer against the
620
+ * write. It cannot be asked again afterwards: by the time a refusal comes
621
+ * back a sibling write may have removed the edge, and an identity records
622
+ * which two issues a relationship was between and never which of them
623
+ * declared it. A host rendering without `mountWorkspace` states the same fact
624
+ * the same way: whichever issue's panel it wants the refusal read on, decided
625
+ * when it made the edit.
626
+ *
627
+ * NOT DERIVABLE FROM {@link WorkspaceRefusal.edgeId}, which is why it is a
628
+ * second field rather than a lookup. `unknown-issue` names an issue the
629
+ * document does not hold; `unknown-edge` names an edge it does not hold; and
630
+ * a together unit's panel is headed by a key the edge may never mention.
631
+ */
632
+ readonly carrier: string;
633
+ /**
634
+ * Whether the edge this refusal marks exists only because the refusal does.
635
+ *
636
+ * NOT EVERY REFUSAL IS ABOUT AN EDIT THAT LEFT NO TRACE, and reading them as
637
+ * if they were deleted a relationship the reader still has. Three of the
638
+ * store's codes mark an edge that is already LANDED — measured against the
639
+ * store rather than reasoned from the code list: `duplicate-edge` (a create
640
+ * of a relationship that exists; `project` folds the phantom onto the real
641
+ * edge), and `unchanged-kind` and `symmetric-edge` (a retype or flip whose
642
+ * result has the identity it started from, so `edgeChangeFor` marks the
643
+ * original rather than swapping it). `cardinality` looks like a fourth and is
644
+ * not: through a create it marks the edge it would have made, and through a
645
+ * retype it marks the edge it would have BECOME — a phantom either way, with
646
+ * the original hidden — which is exactly why this is asked of the document
647
+ * rather than keyed off a list of codes. With the capsule REPLACING the row
648
+ * for all of them, telling a reader "this relationship is already declared"
649
+ * removed the declared relationship's row from the panel, remove control
650
+ * included — so the one message they were given was contradicted by the list
651
+ * beside it, and nothing on the surface could undo the edge they had just
652
+ * been told about.
653
+ *
654
+ * TRUE MEANS THE ROW IS THE REFUSAL. A refused CREATE of a genuinely new
655
+ * relationship draws as a capsule in the row it would have been: there is no
656
+ * relationship to operate, and a `select-edge` and a `✕` on it would offer to
657
+ * remove something that was never added. False means the relationship is
658
+ * real, so the row stands and the reason is attached to it.
659
+ *
660
+ * REQUIRED, BECAUSE NEITHER DEFAULT IS SAFE. Defaulted to `true` the panel
661
+ * eats real rows; defaulted to `false` it draws a live remove control for an
662
+ * edge the document does not have. A host derives it in one line — the
663
+ * refused id is in `snapshot.landed` or it is not — and the store's own
664
+ * projection is where both halves already come from.
665
+ */
666
+ readonly phantom: boolean;
77
667
  }
78
668
  export interface WorkspaceOptions {
79
669
  /**
@@ -81,11 +671,43 @@ export interface WorkspaceOptions {
81
671
  * not invent an English sentence, and a default would be one.
82
672
  */
83
673
  readonly words: WorkspaceWords;
674
+ /**
675
+ * The store's `lastChange` — what the last landed edit did to the order.
676
+ *
677
+ * ABSENT MEANS NO EDIT HAS LANDED TO REPORT, which is not the same as an edit
678
+ * that landed and moved nothing. The second is a `change` whose summary is
679
+ * `unchanged`, and §17c draws it in the summary's own place because "landed
680
+ * and moved nothing" is the finding an owner auditing an encoding most needs.
681
+ */
682
+ readonly change?: OrderChange | null | undefined;
683
+ /**
684
+ * The store's `order.status`. `held` labels the rail and greys it one step.
685
+ *
686
+ * THE RANKS DRAWN ARE STILL THE ONES THE CALLER VOUCHED FOR. This says the
687
+ * order is being re-evaluated; it does not re-rank anything, and nothing in
688
+ * this render moves a row because a write is in flight. §122's rule —
689
+ * optimistic rendering yes, optimistic re-ORDERING no — is held by there
690
+ * being no code path here that reorders at all.
691
+ */
692
+ readonly orderStatus?: OrderStatus | undefined;
84
693
  readonly selection?: WorkspaceSelection | undefined;
85
694
  /** The ladder's reader position — search, focus, opened isolates. */
86
695
  readonly scale?: ScaleState | undefined;
87
696
  /** Which slice of the order the rail draws. See {@link railWindow}. */
88
697
  readonly rail?: RailWindowOptions | undefined;
698
+ /**
699
+ * The rail rows whose provenance is open, by lead key.
700
+ *
701
+ * §16f's `→ expands provenance` names a per-row state, and the VIEWER CANNOT
702
+ * HOLD ONE: it is a pure renderer, re-run from scratch on every change. So
703
+ * the host owns the set and hands it back in, exactly as `selection` is.
704
+ *
705
+ * ONLY THE RAIL READS IT. The canvas draws stations rather than rows and has
706
+ * no provenance to open, and §17j already shows provenance inline on the wide
707
+ * panel — where the expand affordance would be a control for something
708
+ * already visible.
709
+ */
710
+ readonly expanded?: readonly string[] | undefined;
89
711
  /**
90
712
  * The audit's input, when the host has one.
91
713
  *
@@ -108,6 +730,73 @@ export interface WorkspaceOptions {
108
730
  * Ignored with no audit, because there is nothing to filter by.
109
731
  */
110
732
  readonly auditFiltered?: boolean | undefined;
733
+ /**
734
+ * Whether §17d's audit overlay is open.
735
+ *
736
+ * SEPARATE FROM `auditFiltered`, and they used to be one thing. The header
737
+ * count was the filter toggle; `RULINGS.md` §3 makes it this overlay's anchor
738
+ * and moves the filter into the panel head, so the two states are now two
739
+ * questions: which rows the rail shows, and whether the panel is on screen.
740
+ *
741
+ * Ignored with no audit, because there is no panel to open.
742
+ */
743
+ readonly auditOpen?: boolean | undefined;
744
+ /**
745
+ * Whether §17k's collapsed canvas has been opened — `HostState.canvasOpen`.
746
+ *
747
+ * PUBLISHED AS AN ATTRIBUTE ON THE SURFACE ROOT, and read only by the
748
+ * stylesheet's narrow block. Nothing here decides whether the canvas IS a
749
+ * strip: that is a question about the box the surface was given, the
750
+ * container query is the only party that can answer it, and §17k's
751
+ * implementation note is explicit that the package must not ask the window
752
+ * instead. So this says what the reader asked for and the sheet says whether
753
+ * it is currently drawn.
754
+ *
755
+ * Absent means shut, which is §17k's resting state for the strip.
756
+ */
757
+ readonly canvasOpen?: boolean | undefined;
758
+ /**
759
+ * Whether the reader has dismissed §17k's lifted inspector —
760
+ * `HostState.inspectorDismissed`.
761
+ *
762
+ * COMBINED WITH THE SELECTION, NEVER READ ALONE. §17k opens the lifted panel
763
+ * "by selection", so the attribute this becomes is `lifted` only when
764
+ * something is selected AND the reader has not pressed Escape since. A render
765
+ * with nothing selected draws the panel dismissed however this is set, which
766
+ * is what keeps an empty "nothing selected" card from floating over the
767
+ * canvas at the middle width.
768
+ *
769
+ * Ignored at the two other widths, where the inspector is a zone.
770
+ */
771
+ readonly inspectorDismissed?: boolean | undefined;
772
+ /**
773
+ * Take over §17j's rail density, instead of letting the rail's own box decide.
774
+ *
775
+ * FORWARDED, NOT INVENTED. `@issuegraph/viewer` owns the rule and the
776
+ * default: the rail reads its density off the column it was given, and the
777
+ * crossover it uses — `430px` — is that package's default rather than a
778
+ * figure Design published. This exists so a consumer of the EDITOR can reach
779
+ * the same escape hatch without rendering the rail themselves.
780
+ *
781
+ * THE WORKSPACE ITSELF NEEDS IT FOR NOTHING. §17k holds the rail at one
782
+ * measure now — 390 at every width, which is the rule §17k states outright —
783
+ * so this surface never crosses the threshold and never has an opinion. It is
784
+ * threaded for the host's sake, and absent it changes nothing.
785
+ */
786
+ readonly railDensity?: 'dense' | 'wide' | undefined;
787
+ /**
788
+ * Where an issue lives in the host's own world, for §17d's refused block.
789
+ *
790
+ * THE HOST'S ANSWER BECAUSE IT IS THE ONLY PARTY THAT HAS ONE. A store ref is
791
+ * opaque in here and this package holds no repository identity, so a URL is
792
+ * supplied rather than derived. Absent, `null`, or a scheme the viewer's
793
+ * allowlist refuses all mean the same thing — no link is drawn. See
794
+ * {@link EncodingRefusedBlockOptions.issueUrl}, which owns that rule and the
795
+ * reason an `href` cannot be trusted to escaping alone.
796
+ *
797
+ * Ignored with no audit, because a refusal arrives on the audit's input.
798
+ */
799
+ readonly issueUrl?: EncodingRefusedBlockOptions['issueUrl'];
111
800
  readonly theme?: Theme | undefined;
112
801
  /** The selector the theme's custom properties are written onto. */
113
802
  readonly themeSelector?: string | undefined;
@@ -115,8 +804,96 @@ export interface WorkspaceOptions {
115
804
  * The store's projection, for the canvas to draw each edge's write states.
116
805
  * See `ScaleLadderOptions.projected`; the workspace forwards it and reads
117
806
  * none of it, because the rail and the inspector draw no line to overlay.
807
+ *
808
+ * AND IT IS NOT WHERE THE INSPECTOR'S REFUSALS COME FROM, which is worth
809
+ * saying here because it is the obvious place to look. A `ProjectedEdge`
810
+ * carries `states` and `writes` — so it says an edge is `invalid` and never
811
+ * says why. The code is on the write record, and the panel takes it through
812
+ * {@link WorkspaceOptions.refusals}.
118
813
  */
119
814
  readonly projected?: readonly ProjectedEdge[] | undefined;
815
+ /**
816
+ * The create draft in flight, so the panel can draw the step the reader is on.
817
+ *
818
+ * §17b's inspector path is `+ add` → kind → target, and `create/draft.ts`
819
+ * makes `begin` RESET the kind and the target on purpose. An always-visible
820
+ * kind list would therefore let a reader fill a slot that the next `+ add`
821
+ * silently discards, so the list is drawn only while a draft is live —
822
+ * which the panel cannot know without being told.
823
+ *
824
+ * Absent means idle, which is the state a host that has no create path is in.
825
+ */
826
+ readonly draft?: CreateDraft | undefined;
827
+ /**
828
+ * Where a canvas drop landed, or `null`. Read for its NULLNESS alone.
829
+ *
830
+ * THE PANEL SUPPRESSES ITS KIND LIST WHILE A DROP IS LIVE, which is the
831
+ * invariant that keeps the two choosers mutually exclusive. A drag that ends
832
+ * on the canvas opens a chooser AT THE DROP POINT; a panel list drawn at the
833
+ * same moment would be a second copy of the same step, one under the
834
+ * reader's pointer and one in the column beside it, both writing to the one
835
+ * draft. The shell held that rule alone while it drew both, and it cannot
836
+ * hold it any more now that one of them is package markup.
837
+ *
838
+ * The host's own value is forwarded rather than a boolean derived from it:
839
+ * a `dropInFlight: boolean` would be a second spelling of a fact the shell
840
+ * already holds, free to disagree with it on the render where it matters.
841
+ */
842
+ readonly drop?: Point | null | undefined;
843
+ /**
844
+ * The edits the store refused, so the panel can say so where the reader made
845
+ * them. See {@link WorkspaceRefusal}.
846
+ *
847
+ * IN THE ORDER THE STORE RECORDED THEM, and that is part of the contract
848
+ * rather than an accident of how a caller happened to build it. One row
849
+ * states one reason, so two refusals naming one edge collapse to the LAST —
850
+ * the one the reader just caused, rather than the one they have read and
851
+ * moved past. A caller assembling this from two passes and concatenating
852
+ * them loses that chronology at the join; `mountWorkspace` walks
853
+ * `snapshot.writes` once for exactly this reason.
854
+ *
855
+ * ABSENT MEANS "NOTHING REFUSED", which is unlike {@link WorkspaceOptions.audit}
856
+ * and is safe for the reason that one is not: a refusal is a fact the store
857
+ * produces, so a host with no refusals to report and a host that never asked
858
+ * are in the same position — there is nothing to draw either way. An audit
859
+ * count is a NUMBER, and inventing a zero for one nobody computed is the
860
+ * distinction that option exists to keep.
861
+ */
862
+ readonly refusals?: readonly WorkspaceRefusal[] | undefined;
863
+ /**
864
+ * The failed and conflicted writes the reader can still act on.
865
+ *
866
+ * OPTIONAL, on the same reasoning as {@link WorkspaceOptions.refusals}: an
867
+ * unsettled write is a fact the store produces, so "none" and "never asked"
868
+ * leave the panel with the same nothing to draw.
869
+ */
870
+ readonly recoveries?: readonly WorkspaceRecovery[] | undefined;
871
+ /**
872
+ * Which conflict has its difference open, if any.
873
+ *
874
+ * ONE AT A TIME, because the region is inside a panel rather than a dialog.
875
+ * The shell holds it for the reason `auditFiltered` is held: the card
876
+ * publishes a toggle, so something has to carry its state or every render
877
+ * answers "closed" and the control cannot complete what it advertises.
878
+ */
879
+ readonly diffOpen?: MutationId | null | undefined;
880
+ /**
881
+ * A value unique to this surface, used to bind §17a's rail footer to the
882
+ * isolated list the canvas draws.
883
+ *
884
+ * SUPPLIED BY WHOEVER KNOWS THE PAGE. The footer's toggle and the list are in
885
+ * different zones, so the toggle names the list with `aria-controls` — and an
886
+ * id invented in here would be emitted twice by a host rendering two
887
+ * workspaces, leaving the second toggle pointing at the first surface's list.
888
+ * `scale/render.ts`'s `searchSpec` records the same hazard for its own label.
889
+ *
890
+ * ABSENT, NO ASSOCIATION IS DRAWN, and that is the honest default rather than
891
+ * a degraded one: `mountWorkspace` mints a value per mount, and markup served
892
+ * WITHOUT a mount is not operable anyway — the same argument
893
+ * `CanvasWords.editMode` is optional under. A toggle nobody can press needs no
894
+ * `aria-controls`.
895
+ */
896
+ readonly surfaceId?: string | undefined;
120
897
  }
121
898
  export interface WorkspaceView {
122
899
  readonly selection: WorkspaceSelection;