@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
package/README.md CHANGED
@@ -109,9 +109,28 @@ auditRowAttributes(overlay, ref); // {} for a clean row; the severity mark fo
109
109
 
110
110
  **"Long-closed" is not available here.** A document carries no timestamp, so every closed blocker is reported — the safe direction for a finding whose whole severity is `misleading`, and one a host can narrow with a date it does have.
111
111
 
112
- **Ambient, and the list of things it is not.** A persistent header count that never moves and never animates, a `--ig-stroke` gold left-bar on affected rail rows, and a filter — not a mode, because *"a mode you must enter is a mode you forget"*. No modals, toasts, red banners, badge animation, or **auto-fix**: every finding is a judgment call, so the surface offers navigation and never a remedy. That prohibition is asserted over the emitted markup and the stylesheet bytes rather than stated here alone.
112
+ **Three classes are a list; the fourth is its own block.** `renderAuditPanel` draws the cycle, the stale blocker and the dead duplicate ref as cards, because each says something about a relationship that exists. `renderEncodingRefusedBlock` draws the refusal separately — SPEC surfaces it *"on the issue itself, because until it parses the issue has no edges at all and would otherwise look simply unencoded"* — with the ref, the reader's diagnostic, the offending source line, and two controls.
113
113
 
114
- **The bar is CSS on this package's own attribute, not an element drawn into a viewer row.** Layer 1's markup primitive is deliberately not on its public surface, so an overlay drawn from out here would have to re-implement HTML escaping — duplication with an injection shape rather than a mirror that merely drifts. `auditRowAttributes` answers what a row carries, `auditStylesheet` draws the bar from it, and the exchange is data.
114
+ ```ts
115
+ import { renderAuditPanel, renderEncodingRefusedBlock } from '@issuegraph/editor';
116
+
117
+ renderAuditPanel(overlay, { words, known, edges }); // the three relationship findings
118
+ renderEncodingRefusedBlock(encodingRefused, { words, known, issueUrl });
119
+ ```
120
+
121
+ `known` is the issue refs your surface draws and `edges` the edge ids it draws — both answered by you, not derived here, because the audit reads a document that is not always the one on screen. They gate the controls: a remedy that selects an edge your surface does not hold would clear the reader's selection and open nothing, so it is withheld. The finding is still drawn; it is the control that is withheld, never the information.
122
+
123
+ Both return `null` when they have nothing to draw. `renderWorkspace` composes them for you into §17d's overlay over the **canvas** — anchored to the header count, dismissed on Escape, and never over the rail, because judging a finding is largely a question of what it would do to the order.
124
+
125
+ **Two counts, and they are different numbers.** `renderAuditHeader`'s count is every finding — it is the persistent control, it sits in the workspace chrome beside both surfaces, and a reader works through all four classes. The count in the *panel's own head* is that panel's own cards, because it is enclosed by the list it counts.
126
+
127
+ **`issueUrl` is yours to answer, and its answer is checked.** The package holds an opaque reference and no repository identity, so the outward link is drawn only for a URL you resolve — and only if it passes `@issuegraph/viewer`'s `isLinkable`. An `href` runs in your origin, so `javascript:` and `data:` are script sinks that escaping does not close; absent, `null` and a refused scheme all draw no link. `Rewrite from editor` publishes `reveal-issue`, never `select-issue`: the latter is a pointer that completes a relationship draft, so it could WRITE from a surface whose rule is that it never writes.
128
+
129
+ **Ambient, and the list of things it is not.** A persistent header count that never moves and never animates, a `--ig-stroke` gold left-bar on affected rail rows, and a filter — not a mode, because *"a mode you must enter is a mode you forget"*. No modals, toasts, red banners, badge animation, or **auto-fix**. That prohibition is asserted over the emitted markup and the stylesheet bytes rather than stated here alone.
130
+
131
+ **Remedies are required, and auto-fix is still forbidden.** An earlier revision of this file said the surface *"offers navigation and never a remedy"*, and Design ruled the other way: `17d` draws `Remove the edge`, `Keep as history`, `Repoint or clear` and `pick one edge to drop`, and only AUTO-fix is banned. The two are reconciled by the mechanism — **not one remedy writes.** Each publishes a selection and hands the work to the inspector, which already owns every edit affordance, so every button costs one deliberate human keystroke and the judgment stays with the owner.
132
+
133
+ **The bar is CSS on this package's own attribute, not an element drawn into a viewer row.** The reason is **ownership of the row**, not escaping: the rows belong to `@issuegraph/viewer`, and rewriting a row's `class` from out here reaches past the surface layer 2 composes through. (Layer 1's `renderMarkup` *is* public and five leaves here import it — an earlier revision of this paragraph said otherwise, and `audit/surface.ts` records the same correction.) `auditRowAttributes` answers what a row carries, `auditStylesheet` draws the bar from it, and the exchange is data.
115
134
 
116
135
  ## The three equivalent create paths
117
136
 
@@ -138,7 +157,7 @@ let { draft, proposal } = createReducer(IDLE_CREATE_DRAFT, { kind: 'begin', sour
138
157
 
139
158
  **The draft carries no path identity**, deliberately. A `source` filled by a drag and one filled by `R` are the same fact, and a field recording which arrived would be a place for the paths to grow apart. What genuinely differs between them is where the picker is *drawn*, and that is geometry — `pickerPlacement`, from measured bounds — rather than state.
140
159
 
141
- **Direction is the gather order**, `from` = source. Nothing infers it: §17b states direction and offers a flip, and the picker re-derives after the edit lands, so a wrong guess is one act from correct. That is the same reasoning `picker/view.ts` records for retyping across the directed/symmetric split.
160
+ **Direction is the gather order**, `from` = source. Nothing infers it: §17b states direction and offers a flip — both on the selected relationship's own row, drawn by `renderWorkspace`, so a host that wires the published attributes without mounting gets them too — and `pickerView` re-derives after the edit lands, so a wrong guess is one act from correct. That is the same reasoning `picker/view.ts` records for retyping across the directed/symmetric split.
142
161
 
143
162
  **The keyboard is a full loop with no pointer step.** `keyIntent` is a pure key map — a key **press** and a context in, an intent out, no DOM — exactly as the viewer's `navigation.ts` is, so the whole map is exhaustively testable on a runtime with no DOM at all. The digits read `EDGE_FIELDS` from `@issuegraph/core` rather than restating it, so a sixth field gets a `6` for free and the picker and the keyboard cannot disagree. `⌫` binds **both** `Backspace` and `Delete`, because the key §17b draws as `⌫` reports differently across keyboards and binding one would make "no pointer" false on the other. An unbound key answers `none` and is left to the host.
144
163
 
@@ -154,7 +173,8 @@ element.addEventListener('keydown', (event) => {
154
173
  selectedEdge,
155
174
  // Which of the create flow's own interactions is the keyboard in?
156
175
  // Only the shell can see this.
157
- interaction: activeInteraction(), // 'canvas' | 'target-search' | 'elsewhere'
176
+ // 'canvas' | 'kind-chooser' | 'target-search' | 'add-control' | 'elsewhere'
177
+ interaction: activeInteraction(),
158
178
  });
159
179
  if (intent.kind === 'none') return; // someone else's key — let it through
160
180
  event.preventDefault();
@@ -164,17 +184,31 @@ element.addEventListener('keydown', (event) => {
164
184
 
165
185
  **It asks about *our* interaction, not about who else might own the key** — and that is the design decision worth reading, because it replaced the obvious one. Four review rounds each found a different owner the map had failed to anticipate: the platform's `Cmd+R`, the target search's digits, an input method's `⏎`, then an unrelated editable control's `Escape`. Every fix was correct and every one invited the next, because they answered an unanswerable question. *Who else might own this press?* is an inventory of the **host's** widgets — unbounded from in here, and one entry longer every time a host grows a control.
166
186
 
167
- So `CreateInteraction` enumerates **this design's own flow**, which §17b fixes at three states, and the host says which one it is in:
187
+ So `CreateInteraction` enumerates **this design's own flow** — one state per step of it — and the host says which one it is in:
168
188
 
169
189
  | state | what reaches the map |
170
190
  |---|---|
171
191
  | `canvas` | every binding |
192
+ | `kind-chooser` | `R`, the digits `1`–`5`, and `Escape` — **not** `⏎` |
172
193
  | `target-search` | only `⏎` and `Escape` |
194
+ | `add-control` | only `R` |
173
195
  | `elsewhere` | nothing |
174
196
 
175
- `elsewhere` is what closes the set: it is *everything that is not our own search box* — an inline title, a filter, a modal, a control this package has never heard of. A fifth widget adds no code here, and `Escape` is surrendered along with the rest, because that control needs `Escape` to cancel its own edit.
197
+ `elsewhere` is what closes the set: it is *everything that is not one of our own steps* — an inline title, a filter, a modal, a control this package has never heard of. Another widget adds no code here, and `Escape` is surrendered along with the rest, because that control needs `Escape` to cancel its own edit.
198
+
199
+ **The set is closed by the flow, not by a count.** It has grown twice, both times because a step of *this* flow was missing from it rather than because the closure argument failed — see `kind-chooser` and `add-control` below. What has never moved is the invariant that matters: no state here names a host's widget.
200
+
201
+ **`kind-chooser` is a fact about the DRAFT, not about focus** — a source gathered and no kind yet, with a focused text box excluded so a search box keeps its own digits. It was folded into `canvas` until [#152](https://github.com/autnmy/issuegraph/issues/152), on the assumption that focus stays on the row a draft was begun from; activating the add control *replaces* that control with the kind list, so focus lands wherever the host's restore puts it, and where a filter has emptied the list there is no row to land on at all. `canvas` answers only for a focused row, so the chooser's own digits were handed back at the one step whose entire purpose is to be answered with a digit.
202
+
203
+ `⏎` is withheld there deliberately, and it is the reason this is a state rather than an alias for `canvas`: `commit-target` fires on `match`, a target query survives a restarted draft, and a `⏎` at the kind step would commit a target before any kind was chosen — while also being taken from the kind button under focus.
204
+
205
+ **`add-control` is the SAME shape, one control over.** §17b gives the create flow three equivalent entries — canvas, keyboard, and the inspector's `+ add` — and the third one's control was `elsewhere`, so `R` was inert at exactly the position that *draws* `R` as its hint ([#173](https://github.com/autnmy/issuegraph/issues/173)). **A host must report this state for that to work**: report `elsewhere` while your add control has focus and `R` stays inert there, whatever version you are on.
176
206
 
177
- Which bindings reach `target-search` is **data on the binding table**, so no call site decides it and a sixth binding is a compile error until the table answers. `⏎` and `Escape` reach it because the search box is focused at exactly the moment `⏎` must commit the target — the middle of `R → digit → search → ⏎`. Its printable keys do not: most issue references carry a digit, so a map that claimed `1`–`5` there would eat nearly every query, and `⌫` deletes a *character* rather than the reader's selected edge.
207
+ The test that decides whether a control gets its own state — and stops the list growing on taste — is **does this control draw a key the map reads**. The kind chooser passes it because its digits are `KIND_KEYS`; the add control passes it because it draws `RELATE_KEY` itself. A control that tells the reader to press a key cannot be a control that key is refused by. A filter, a remove button, and every control that draws no key stay `elsewhere`.
208
+
209
+ It admits **`R` and nothing else**, which follows from the same sentence rather than from caution: `⌫` there would delete a selected edge the reader is not looking at, and a digit would fill a kind slot on a draft that has not begun. And unlike `kind-chooser` it *is* a fact about focus — legitimately, because at the begin step there is no draft yet, so the missing fact is the **subject** to begin from, which the control publishes on `data-ig-target`. `mountWorkspace` answers both states itself and asks the draft first, so once a draft begins `kind-chooser` takes over.
210
+
211
+ Which bindings survive `kind-chooser`, `target-search` and `add-control` is **data on the binding table**, so no call site decides it and a sixth binding is a compile error until the table answers. `⏎` and `Escape` reach it because the search box is focused at exactly the moment `⏎` must commit the target — the middle of `R → digit → search → ⏎`. Its printable keys do not: most issue references carry a digit, so a map that claimed `1`–`5` there would eat nearly every query, and `⌫` deletes a *character* rather than the reader's selected edge.
178
212
 
179
213
  `interaction` is **required, not optional**. Every default is wrong for some host, and the plausible one — assume the canvas — is the one that steals keystrokes.
180
214
 
@@ -201,6 +235,8 @@ The assembly leaf: the rail on the left, the canvas in the centre, the inspector
201
235
 
202
236
  **Positions are fixed, and that is §17f rather than a layout preference.** The rail answers *"what gets worked next"* for the whole backlog and must never refuse; the canvas answers *"what surrounds this issue"* and refuses above its budget. Assembling them must not average the two — so the grid gives each zone its own track, and a large document grows the canvas's refusal instead of squeezing the rail out.
203
237
 
238
+ **The canvas zone leads with a caption, and only where it is true.** Refusing loudly is the easy half of §17f; the quiet decline is the `direct` tier, where the ladder narrows to one component, draws it successfully, and would otherwise say nothing about the rest — leaving a canvas of 6 issues out of 312 indistinguishable from a backlog that has 6. So the row states the focused key and the ratio there, and nowhere else: above the budget there is no canvas to describe and the refusal already carries its own count. Its words are the host's (`WorkspaceWords.canvas`), and the `edit mode` pill beside them is separately optional, because markup served without `mountWorkspace` is not editable and must not claim to be.
239
+
204
240
  **The rail is virtualised, which is what lets it stay complete.** Those read as opposites and are not: the MODEL holds every slot and `addressOf` answers for every rank in the order, while the WINDOW bounds only how many rows are drawn. A reader looking at rows 1–50 of 312 can still ask what is at rank 287 and get an answer. Windowing is therefore a rail *requirement* — the alternative, a rail that paginates, has stopped answering its question.
205
241
 
206
242
  The window is an **offset**, not a rank, because a held slot has `rank: null` and ranks are not a coordinate you can slice on. Every out-of-range value is clamped rather than refused: this reads a scroll position, and taking the rail down over a rounding error is the one thing it may not do.
@@ -223,12 +259,26 @@ The selection reaches the **canvas** too, through two additive options on `Scale
223
259
 
224
260
  **Originating one is layer 1's half.** Every edge mark the graph draws — the stroke, its terminal, and a `together-with` connector — carries `edgeIdentity(...)` on `data-ig-group`, so `onSelect` reports an edge identity that `findEdge` resolves and a host turns into `select-edge`. Before that, four of the five relationships could not be pointed at on the canvas at all.
225
261
 
226
- **The audit is ambient.** A persistent count in the header and a 2px left-bar on affected rail rows — no modal, no auto-fix, no animation, and a filter rather than a mode. The bar is applied by walking the rail's `ElementSpec` tree and adding `data-ig-audit` to the keyed rows, never by splicing the rendered string: `scene.root` is data and `KEY_ATTRIBUTE` is published, so this is a pure transform over a public value and no attribute in this package is escaped by anything but `renderMarkup`.
262
+ **The audit is ambient.** A persistent count in the header and a 2px left-bar on affected rail rows — no modal, no auto-fix, no animation, and a filter rather than a mode. The findings themselves live in a transient overlay that count opens, over the canvas and never over the rail. The bar is applied by walking the rail's `ElementSpec` tree and adding `data-ig-audit` to the keyed rows, never by splicing the rendered string: `scene.root` is data and `KEY_ATTRIBUTE` is published, so this is a pure transform over a public value and no attribute in this package is escaped by anything but `renderMarkup`.
227
263
 
228
264
  `auditFiltered` is the toggle's state, and the workspace holds it: the header draws a `button` with `aria-pressed`, so without somewhere to keep that the control could never complete the action it advertised. The filter narrows the rail **before** the window, or it would narrow only the rows the window had already reached and read as doing nothing on a long backlog. It narrows the rail and nothing else — §17a gives the audit a filter for focus, and the canvas answers a different question.
229
265
 
230
266
  Ranking a unit's members is `heaviestRow`'s job, in the audit module, **because the weights live there**. The shortcut — take the first matching entry in `overlay.rows` — is wrong in a way that looks right: those rows are sorted by `ref`, lexicographically, so a `stale-blocker` on `a` masks a `cycle` on `b`. A row's severity is the heaviest across its **members**, not its lead. A `together-with` unit is one row and several refs, and a finding can name a member that does not lead — read off the lead alone, an affected unit renders clean, which is the audit failing silently on exactly the rows where an encoding error is hardest to see.
231
267
 
268
+ **An unsettled write the reader can act on gets a card.** §17b draws `failed` as a ghost with a ✕ and `conflict` as a doubled line, and gives each a set of resolutions: `retry` and `discard mine` for a failure, plus `view diff` and `retry on latest` for a conflict — and **never** an auto-merge. `WorkspaceOptions.recoveries` is the narrow input those cards need: the state, the reason or the difference, and the panel it belongs on. Not the write ledger, which carries a mutation and an entire second document per conflict.
269
+
270
+ **The buttons are read from `OVERLAY_TREATMENTS`, which is what makes the prohibition structural.** A card emits one control per affordance the grammar table declares for its state, through a mapping declared `satisfies Record<OverlayAffordance, string>` — so a fourth affordance is a compile error rather than a control that renders and does nothing, and since `OverlayAffordance` has no `merge` member no table entry can produce one. Be exact about what that buys: **no merge button can be drawn**. The behavioural guarantee is one layer down, in a store with no merge call and a `HostEffect` with no merge arm.
271
+
272
+ `retry` and `retryOnLatest` are separate words because they are separate calls. The second re-reads and **adopts the upstream document as the new base** before dispatching again, so the order can move on the press — a single word would make one card understate what its button does.
273
+
274
+ **Every card control carries `data-ig-target` naming its write.** The reducer's `retry` and `discard` arms emit no effect without it, so an omitted attribute leaves three controls that render, read correctly and do nothing. `view-diff` is a `control` case beside them rather than a command arm of its own, because the shell turns every `data-ig-command` into a `control` and an arm outside that shape has no route from a button.
275
+
276
+ **The difference is narrowed by the panel's key set, not by the carrier.** An issue panel speaks for every member of its slot, so a together unit's lead states a partner's conflict — and scoping the difference to the carrier alone would drop the partner's own edge from the one panel entitled to show it. The reader's side of that difference is rebuilt from the mutation through `edgeChangeFor`, because the landed document by contract does not carry an unlanded edit. It covers **issues as well as edges**: §17b's stated cause is that the body changed upstream, and an edges-only difference is empty for the commonest conflict there is.
277
+
278
+ **A recovery with no panel is still drawn.** Where a refusal whose carrier is `null` is dropped — there is nowhere to state it, and the cost is a sentence — a recovery in that position keeps the reader's only retry and discard, so it goes in a region at the foot of the panel under a heading that says why it is not on a relationship.
279
+
280
+ **Not yet drawn: the gold double line.** §17b's conflict *treatment* is a doubled line holding both versions, and `second-version` is a mark for whoever computed the layout. The card is the *affordance* surface; the treatment is [#102](https://github.com/autnmy/issuegraph/issues/102).
281
+
232
282
  **Dark only.** The pass-2 brief carries "light + dark" over from pass 1; light was cut after that pass. There is no forked token set and no `prefers-color-scheme` block — the palette is the viewer's, reached through its custom properties.
233
283
 
234
284
  ## The mount
@@ -259,11 +309,17 @@ handle.destroy(); // every listener off, the nodes it built rem
259
309
 
260
310
  **The canvas draws each edge's write states.** `renderWorkspace` and `renderScaleLadder` take `projected` — the store's own `ProjectedEdge` list — and the ladder attaches an overlay per edge the tier draws: `pending-write`'s dash, an `invalid` or `failed` cross, a `conflict`'s second version, composed with the selection halo on the same line. The mount passes `snapshot.projected` and reconciles the viewer document the host projected with it — the unsettled edges are added, and a landed edge the store hides behind a pending retype or flip is dropped — because the host projects the **order** from the landed document, which must not move for an edit that did not land, while what to **draw** is the store's projection by the store's own contract. The order's status is published on the surface as `data-order="held"` while a write is in flight, in the store's vocabulary, so a host says so without this package inventing the sentence.
261
311
 
312
+ **The first pass is composed, and the scanner is not.** `MountWorkspaceOptions.firstPass` takes one bundle — a `CandidateSource`, the queue's `FirstPassWords`, and three strings the mount draws itself (`exit`, `scanning`, `scanFailed`) — all of it or none, because a scan with no words cannot be drawn and words with no scan have nothing to draw. Supplied, §17a's entry opens a modal over the three zones: the scan, then the queue, then whatever the reader answers. Absent, the mount withholds the command and the entry is inert — the button is drawn *inside* the surface the mount rewrites on every render, so unlike a control a host draws beside the element, it cannot be intercepted from outside. Why no default scanner: the evidence §17e asks for lives in issue bodies, comments and the host's own index, which is the tracker's world and precisely what `@issuegraph/store` refuses to fetch; a heuristic in here would be one vendor's idea of a duplicate, shipped with no way to replace it. `demo/src/firstpass.ts` is a worked example of the host half.
313
+
314
+ **Consent and progress are properties of the shape.** Nothing but an `apply` builds a `Proposal` — opening, drawing, rejecting and skipping emit nothing because there is no arm on which they could — and the mount is pinned at the composed level: a drawn queue writes nothing, a whole queue answered `N`/`S` writes nothing, and one `Y` writes exactly one edge. The denominator is the candidates the host found and never the backlog size, and the mount hands the renderer a `QueueState`, a type that does not hold the backlog size at all. `⌫` steps the queue back and calls the store's own `discardMine`; it does **not** propose a compensating `delete`, so a create that already landed stays landed — the store has no undo for one, and inventing a second retraction path is what `firstpass/queue.ts` refuses in terms.
315
+
316
+ **Closing re-scans, and remembers.** A queue held across a close would go on asking about relationships the reader has since made, so it is dropped — but the candidates already applied or rejected ride outside the phase and filter the next scan, or forty rejections would come back on the next open. A `skip` is *not* filtered: §17e's `S` is "not now", and the deferred set is what a second pass is built from. Each scan carries a generation, so a slow scan answering after a close-and-reopen cannot beat the fresh one.
317
+
262
318
  **Words are the host's, as everywhere here.** `MountWords` extends `WorkspaceWords` with the picker's vocabulary and the chrome's five strings; `keys` is the one optional entry. A command the reducer does not know changes nothing, so a host's own chrome may publish `data-ig-command` on the same attribute outside the element and read it from its own listener — the demo's theme switch does exactly that, and hands the writes log's `retry` and `discard` to `handle.dispatch`.
263
319
 
264
320
  ## Status
265
321
 
266
- The first-pass review queue lands as its own change. The mount is this package's from `0.3.0`; what a host still supplies is what the specification puts there — the data source, the order, the executor holds, the words, the theme — and every listener is the mount's.
322
+ The first-pass review queue is composed from `0.6.0`: §17a's `First pass →` entry opens it, and a host supplies the one thing this package refuses to — a `CandidateSource`. §17e's multi-select bulk path is not composed yet ([#140](https://github.com/autnmy/issuegraph/issues/140)); `planBatch` and `resumeBatch` ship and are tested, and what they still need is a workspace that can hold a selection SET. The mount is this package's from `0.3.0`; what a host still supplies is what the specification puts there — the data source, the order, the executor holds, the words, the theme — and every listener is the mount's.
267
323
 
268
324
  ## Licence
269
325
 
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The accessible contract of every command control, read off a rendered
3
+ * surface — so it is committed as an artifact rather than asserted in prose.
4
+ *
5
+ * ## Why a DOM node and not an `ElementSpec`
6
+ *
7
+ * There is no spec tree for the workspace to walk. `renderWorkspace` returns
8
+ * `markup: string`; it composes its four zones by joining independently
9
+ * rendered strings, the `data-zone` wrapper is the one tag written by hand in
10
+ * that file, and every `…Spec` builder is module-private. Worse for a spec
11
+ * walk, a substantial part of the interactive surface never passes a renderer
12
+ * at all — `mountWorkspace` builds the kind chooser, the target search and its
13
+ * match list with `createElement`, and says so ("WHAT IS LEFT HERE IS WHAT
14
+ * NEEDS A DOM"). A baseline taken over specs would silently omit exactly the
15
+ * keyboard-relevant controls.
16
+ *
17
+ * The rendered DOM has all of it, and it is also what a screen reader actually
18
+ * meets.
19
+ *
20
+ * ## NOT EXPORTED, and that is the correction of a real mistake
21
+ *
22
+ * An earlier revision published `controlSurface` on the package surface,
23
+ * reasoning that a host composing these zones into its own chrome would want
24
+ * the same record. Nothing asked for it, and `index.ts` states the rule it
25
+ * broke: "a published package can add an export later and can never take one
26
+ * back, so nothing is exported before something is owed."
27
+ *
28
+ * The cost was not theoretical. Exporting it turned this file into a
29
+ * general-purpose accessible-name and focusability reader, and accessible-name
30
+ * computation has a long tail — `title`, `placeholder`, an input's own `value`,
31
+ * whitespace-only labels, hidden inputs, `contenteditable`. Three consecutive
32
+ * review rounds produced findings of exactly that shape: each real, each cheap,
33
+ * and none about markup this package draws. That tail has no end, and this
34
+ * change cannot discharge it.
35
+ *
36
+ * Internal, the obligation is bounded and discharged: the reader only ever
37
+ * meets what this package renders, which is a closed set recorded in six
38
+ * states. If a host ever owes this, export it then — with its tail addressed,
39
+ * and against a consumer that actually needs it.
40
+ *
41
+ * ## Structurally typed, so this module still touches no global
42
+ *
43
+ * {@link SurfaceElement} is declared by what it must answer rather than as
44
+ * `Element`, which is `element.ts`'s own `SpecDocument` idiom — "declared
45
+ * structurally rather than as `Document` so a host can pass any implementation
46
+ * that answers these five calls, and so this module stays honest about how
47
+ * little of the DOM it actually needs". `purity.test.ts` imports every shipped
48
+ * module with the browser globals removed; nothing here reaches for one.
49
+ *
50
+ * ## What is recorded, and what is deliberately not
51
+ *
52
+ * VALUES ARE KEPT FOR MACHINE STATES AND DROPPED FOR HUMAN-READABLE NAMES.
53
+ * That split is by provenance, not one blanket rule, because the two fail
54
+ * differently. `aria-pressed` is rendered as `cond ? 'true' : 'false'`, so a
55
+ * baseline holding only the attribute NAME is byte-identical when a toggle
56
+ * inverts — the regression it exists to catch. A label, meanwhile, is often
57
+ * the host's word (`picker/words.ts`, `reevaluate/words.ts` and
58
+ * `firstpass/words.ts` all require a `Record<K, string>` from the host and
59
+ * default none), so pinning the string would fail any host that translated it.
60
+ *
61
+ * An earlier draft justified dropping every value with "the package ships no
62
+ * English", which is false: `scale/render.ts` writes `'connected components'`,
63
+ * `'what to do next'`, `'search matches'` and `'isolated issues'` itself, and
64
+ * the viewer ships more landmark names. The true, narrower claim is the one
65
+ * above.
66
+ *
67
+ * AN IDREF IS RECORDED BY WHETHER IT RESOLVES, never by its value. The
68
+ * disclosure's `aria-controls` names `diffRegionId(recovery.mutationId)`, and a
69
+ * mutation id is generated per run — pinning it would make the artifact churn
70
+ * on every render for no signal. What is worth pinning is the fact a reader
71
+ * depends on: that the id names a region that is actually there. A dangling
72
+ * `aria-controls` is a real defect and a stable one to record.
73
+ */
74
+ /** A text node, or anything else with no tag. */
75
+ export interface SurfaceNode {
76
+ readonly nodeType: number;
77
+ readonly nodeValue: string | null;
78
+ }
79
+ /** The slice of `Element` this module reads. */
80
+ export interface SurfaceElement extends SurfaceNode {
81
+ readonly tagName: string;
82
+ readonly childNodes: ArrayLike<SurfaceNode>;
83
+ getAttribute(name: string): string | null;
84
+ closest(selectors: string): SurfaceElement | null;
85
+ querySelectorAll(selectors: string): Iterable<SurfaceElement>;
86
+ }
87
+ /**
88
+ * Where a control's accessible name comes from, or why it has none.
89
+ *
90
+ * `label` IS A REAL NAME AND WAS MISSING. `scale/render.ts` wraps its search
91
+ * input in a `<label>` carrying the text — the input has a genuine accessible
92
+ * name and no text of its own, no `aria-label` and no `aria-labelledby`. Read
93
+ * without this, it came back `none`, so the naming rule reported a false
94
+ * failure on correct markup and the control could not join the baseline at all.
95
+ * An exported reader that is wrong about native HTML is worse than no reader.
96
+ *
97
+ * `empty` IS ITS OWN ANSWER, not a kind of `none`. An `aria-label=""` is an
98
+ * author who meant to supply a name and supplied nothing — a different defect
99
+ * from a control nobody labelled, and one that reads as deliberate in the
100
+ * markup. Recording it separately is what lets a rule name it. THE VALUE IS
101
+ * NEVER RECORDED: a label is routinely the host's own word (`picker/words.ts`
102
+ * and `reevaluate/words.ts` both require a `Record<K, string>` from the host
103
+ * and default none), so pinning the string would fail any host that translated
104
+ * it. Whether one was supplied is a fact about the markup; what it says is not.
105
+ */
106
+ export type NameSource = 'aria-label' | 'aria-labelledby' | 'label' | 'text' | 'empty' | 'none';
107
+ /**
108
+ * How the keyboard reaches a control.
109
+ *
110
+ * NOT A BOOLEAN, and that is the whole point of the field. This package uses a
111
+ * roving tab stop — the projections render `tabindex: focused ? 0 : -1` — and
112
+ * the viewer's own focusability predicate counts `tabindex="-1"` as focusable.
113
+ * So a boolean "focusable" is satisfied by a control reachable only by pointer
114
+ * and by programmatic focus, which is precisely the defect a keyboard record
115
+ * exists to catch.
116
+ */
117
+ export type TabStop = 'tab' | 'programmatic' | 'none';
118
+ /** One control's accessible contract. */
119
+ export interface ControlEntry {
120
+ readonly zone: string | null;
121
+ /** Which attribute published it — see {@link CONTROL_ATTRIBUTES}. */
122
+ readonly channel: string;
123
+ /** That attribute's value: the command, the answer, or the filter's state. */
124
+ readonly control: string;
125
+ /**
126
+ * What the control acts on, and which option it is.
127
+ *
128
+ * RECORDED BECAUSE THEY SEPARATE OTHERWISE IDENTICAL ROWS. Every relationship
129
+ * row publishes `select-edge`, and the kind list publishes `kind` several
130
+ * times — without these, N controls collapse to N byte-identical entries and
131
+ * losing one of them is invisible in the artifact's diff.
132
+ */
133
+ readonly target: string | null;
134
+ readonly value: string | null;
135
+ readonly tag: string;
136
+ /** `null` where this module has no mapping, which is a fact; a guess would not be. */
137
+ readonly role: string | null;
138
+ readonly tabStop: TabStop;
139
+ /**
140
+ * Whether the control sits under an `inert` subtree.
141
+ *
142
+ * DELIBERATE UNREACHABILITY IS NOT A DEFECT, and without recording it the two
143
+ * are indistinguishable. The mount sets `inert` on every zone while the
144
+ * first-pass overlay is up — "the zones go inert under it" — so those
145
+ * controls are correctly out of the tab order, and a rule that simply
146
+ * demanded reachability would fail on the modal working as designed. Recorded
147
+ * rather than filtered out, because "these eight controls are deliberately
148
+ * unreachable right now" is exactly what a reader of a modal state wants to
149
+ * see.
150
+ */
151
+ readonly inert: boolean;
152
+ readonly name: NameSource;
153
+ /** Enumerated ARIA states with their values, and IDREFs by whether they resolve. */
154
+ readonly aria: Readonly<Record<string, string>>;
155
+ }
156
+ /**
157
+ * Every attribute a control publishes itself on.
158
+ *
159
+ * THREE CHANNELS, NOT ONE, and reading only the first is how a record comes to
160
+ * claim a surface it never saw. `data-ig-command` is the general one, but the
161
+ * first pass's y/n/s answers carry `data-ig-answer` and the audit header's
162
+ * toggle carries `data-ig-audit-filter` — `firstpass/render.ts` and
163
+ * `audit/surface.ts` each say so where they declare theirs. Keyed on the
164
+ * command alone, this would have recorded `null` for the audit toggle and for
165
+ * every answer, and the rules below would have skipped exactly the controls a
166
+ * keyboard reader depends on most.
167
+ */
168
+ export declare const CONTROL_ATTRIBUTES: readonly string[];
169
+ /**
170
+ * Every control under `root`, in document order, sorted so the record is stable.
171
+ *
172
+ * Pure and total. SORTED RATHER THAN LEFT IN DOCUMENT ORDER: a baseline is
173
+ * diffed by people, and a control moving between zones should read as one
174
+ * changed line rather than as a reordering of the whole file. Document order is
175
+ * a fact about layout, which the zone already carries.
176
+ *
177
+ * The bound it does NOT cover is stated where it can be checked: a control
178
+ * publishing itself on some fourth attribute is invisible here, and
179
+ * {@link CONTROL_ATTRIBUTES} is the list to extend.
180
+ */
181
+ export declare function controlSurface(root: SurfaceElement): readonly ControlEntry[];
182
+ //# sourceMappingURL=baseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../../src/a11y/baseline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AAEH,iDAAiD;AACjD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,gDAAgD;AAChD,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IAC5C,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC1C,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAAC;IAClD,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;CAC/D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,iBAAiB,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAEhG;;;;;;;;;GASG;AACH,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,cAAc,GAAG,MAAM,CAAC;AAEtD,yCAAyC;AACzC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,oFAAoF;IACpF,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACjD;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAQ9C,CAAC;AA6QH;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,YAAY,EAAE,CAqD5E"}