@issuegraph/editor 0.1.0

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 (143) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +238 -0
  3. package/dist/audit/findings.d.ts +225 -0
  4. package/dist/audit/findings.d.ts.map +1 -0
  5. package/dist/audit/findings.js +348 -0
  6. package/dist/audit/findings.js.map +1 -0
  7. package/dist/audit/styles.d.ts +37 -0
  8. package/dist/audit/styles.d.ts.map +1 -0
  9. package/dist/audit/styles.js +72 -0
  10. package/dist/audit/styles.js.map +1 -0
  11. package/dist/audit/surface.d.ts +155 -0
  12. package/dist/audit/surface.d.ts.map +1 -0
  13. package/dist/audit/surface.js +202 -0
  14. package/dist/audit/surface.js.map +1 -0
  15. package/dist/create/draft.d.ts +119 -0
  16. package/dist/create/draft.d.ts.map +1 -0
  17. package/dist/create/draft.js +114 -0
  18. package/dist/create/draft.js.map +1 -0
  19. package/dist/create/keys.d.ts +225 -0
  20. package/dist/create/keys.d.ts.map +1 -0
  21. package/dist/create/keys.js +228 -0
  22. package/dist/create/keys.js.map +1 -0
  23. package/dist/create/placement.d.ts +69 -0
  24. package/dist/create/placement.d.ts.map +1 -0
  25. package/dist/create/placement.js +76 -0
  26. package/dist/create/placement.js.map +1 -0
  27. package/dist/firstpass/batch.d.ts +190 -0
  28. package/dist/firstpass/batch.d.ts.map +1 -0
  29. package/dist/firstpass/batch.js +143 -0
  30. package/dist/firstpass/batch.js.map +1 -0
  31. package/dist/firstpass/candidates.d.ts +128 -0
  32. package/dist/firstpass/candidates.d.ts.map +1 -0
  33. package/dist/firstpass/candidates.js +39 -0
  34. package/dist/firstpass/candidates.js.map +1 -0
  35. package/dist/firstpass/keys.d.ts +90 -0
  36. package/dist/firstpass/keys.d.ts.map +1 -0
  37. package/dist/firstpass/keys.js +112 -0
  38. package/dist/firstpass/keys.js.map +1 -0
  39. package/dist/firstpass/queue.d.ts +183 -0
  40. package/dist/firstpass/queue.d.ts.map +1 -0
  41. package/dist/firstpass/queue.js +159 -0
  42. package/dist/firstpass/queue.js.map +1 -0
  43. package/dist/firstpass/render.d.ts +100 -0
  44. package/dist/firstpass/render.d.ts.map +1 -0
  45. package/dist/firstpass/render.js +216 -0
  46. package/dist/firstpass/render.js.map +1 -0
  47. package/dist/firstpass/styles.d.ts +36 -0
  48. package/dist/firstpass/styles.d.ts.map +1 -0
  49. package/dist/firstpass/styles.js +148 -0
  50. package/dist/firstpass/styles.js.map +1 -0
  51. package/dist/firstpass/view.d.ts +82 -0
  52. package/dist/firstpass/view.d.ts.map +1 -0
  53. package/dist/firstpass/view.js +60 -0
  54. package/dist/firstpass/view.js.map +1 -0
  55. package/dist/firstpass/words.d.ts +105 -0
  56. package/dist/firstpass/words.d.ts.map +1 -0
  57. package/dist/firstpass/words.js +28 -0
  58. package/dist/firstpass/words.js.map +1 -0
  59. package/dist/index.d.ts +150 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +147 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/overlay/grammar.d.ts +259 -0
  64. package/dist/overlay/grammar.d.ts.map +1 -0
  65. package/dist/overlay/grammar.js +223 -0
  66. package/dist/overlay/grammar.js.map +1 -0
  67. package/dist/overlay/render.d.ts +125 -0
  68. package/dist/overlay/render.d.ts.map +1 -0
  69. package/dist/overlay/render.js +439 -0
  70. package/dist/overlay/render.js.map +1 -0
  71. package/dist/overlay/styles.d.ts +41 -0
  72. package/dist/overlay/styles.d.ts.map +1 -0
  73. package/dist/overlay/styles.js +124 -0
  74. package/dist/overlay/styles.js.map +1 -0
  75. package/dist/picker/render.d.ts +80 -0
  76. package/dist/picker/render.d.ts.map +1 -0
  77. package/dist/picker/render.js +112 -0
  78. package/dist/picker/render.js.map +1 -0
  79. package/dist/picker/styles.d.ts +26 -0
  80. package/dist/picker/styles.d.ts.map +1 -0
  81. package/dist/picker/styles.js +116 -0
  82. package/dist/picker/styles.js.map +1 -0
  83. package/dist/picker/view.d.ts +135 -0
  84. package/dist/picker/view.d.ts.map +1 -0
  85. package/dist/picker/view.js +117 -0
  86. package/dist/picker/view.js.map +1 -0
  87. package/dist/picker/words.d.ts +53 -0
  88. package/dist/picker/words.d.ts.map +1 -0
  89. package/dist/picker/words.js +28 -0
  90. package/dist/picker/words.js.map +1 -0
  91. package/dist/reevaluate/render.d.ts +77 -0
  92. package/dist/reevaluate/render.d.ts.map +1 -0
  93. package/dist/reevaluate/render.js +211 -0
  94. package/dist/reevaluate/render.js.map +1 -0
  95. package/dist/reevaluate/styles.d.ts +24 -0
  96. package/dist/reevaluate/styles.d.ts.map +1 -0
  97. package/dist/reevaluate/styles.js +182 -0
  98. package/dist/reevaluate/styles.js.map +1 -0
  99. package/dist/reevaluate/view.d.ts +102 -0
  100. package/dist/reevaluate/view.d.ts.map +1 -0
  101. package/dist/reevaluate/view.js +111 -0
  102. package/dist/reevaluate/view.js.map +1 -0
  103. package/dist/reevaluate/words.d.ts +54 -0
  104. package/dist/reevaluate/words.d.ts.map +1 -0
  105. package/dist/reevaluate/words.js +29 -0
  106. package/dist/reevaluate/words.js.map +1 -0
  107. package/dist/scale/commands.d.ts +60 -0
  108. package/dist/scale/commands.d.ts.map +1 -0
  109. package/dist/scale/commands.js +49 -0
  110. package/dist/scale/commands.js.map +1 -0
  111. package/dist/scale/ladder.d.ts +141 -0
  112. package/dist/scale/ladder.d.ts.map +1 -0
  113. package/dist/scale/ladder.js +243 -0
  114. package/dist/scale/ladder.js.map +1 -0
  115. package/dist/scale/render.d.ts +85 -0
  116. package/dist/scale/render.d.ts.map +1 -0
  117. package/dist/scale/render.js +194 -0
  118. package/dist/scale/render.js.map +1 -0
  119. package/dist/scale/styles.d.ts +21 -0
  120. package/dist/scale/styles.d.ts.map +1 -0
  121. package/dist/scale/styles.js +105 -0
  122. package/dist/scale/styles.js.map +1 -0
  123. package/dist/workspace/inspector.d.ts +117 -0
  124. package/dist/workspace/inspector.d.ts.map +1 -0
  125. package/dist/workspace/inspector.js +140 -0
  126. package/dist/workspace/inspector.js.map +1 -0
  127. package/dist/workspace/rail.d.ts +122 -0
  128. package/dist/workspace/rail.d.ts.map +1 -0
  129. package/dist/workspace/rail.js +216 -0
  130. package/dist/workspace/rail.js.map +1 -0
  131. package/dist/workspace/render.d.ts +139 -0
  132. package/dist/workspace/render.d.ts.map +1 -0
  133. package/dist/workspace/render.js +366 -0
  134. package/dist/workspace/render.js.map +1 -0
  135. package/dist/workspace/selection.d.ts +92 -0
  136. package/dist/workspace/selection.d.ts.map +1 -0
  137. package/dist/workspace/selection.js +83 -0
  138. package/dist/workspace/selection.js.map +1 -0
  139. package/dist/workspace/styles.d.ts +39 -0
  140. package/dist/workspace/styles.d.ts.map +1 -0
  141. package/dist/workspace/styles.js +313 -0
  142. package/dist/workspace/styles.js.map +1 -0
  143. package/package.json +58 -0
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,238 @@
1
+ # @issuegraph/editor
2
+
3
+ Everything that **mutates** an [Issuegraph](https://github.com/autnmy/issuegraph) document: the edit affordances, drawn as overlays on [`@issuegraph/viewer`](../viewer) and dispatched through [`@issuegraph/store`](../store).
4
+
5
+ > **Publishable from `0.1.0`.** The package was `private` until the change that assembles the workspace fixed its exports; that change has landed, so the manifest joins its siblings and the next release carries it. Publishing itself stays a deliberate act — `publish.yml` runs on a release or a manual dispatch, never on a push.
6
+
7
+ Layer 2 of three, and the whole of its contract:
8
+
9
+ ```
10
+ composes @issuegraph/viewer layer 1 — the node, edge and badge grammar
11
+ composes @issuegraph/store — the document, the edit set, the port
12
+ never fetching, auth, persistence, or a write of its own
13
+ ```
14
+
15
+ ## Why an editor can be published at all
16
+
17
+ Editing was never what made a layer unpublishable. The write path is already an open-source library, and the visual grammar encodes no read-only-ness. What editing drags in — auth, the network, the fail-or-conflict state machine — is **host-shaped, and injected**:
18
+
19
+ ```ts
20
+ // The editor proposes. The store dispatches. The host writes.
21
+ proposal → @issuegraph/store → DataSource (yours)
22
+ ```
23
+
24
+ An edit is a `Proposal` — `create`, `delete`, `retype`, `flip`, and no more, so one user act is one round trip and one undo entry. The store holds it, renders it optimistically, and **refuses to re-evaluate the selection order until the write lands**. A failed edit must never leave the order looking changed.
25
+
26
+ ## The seam, and why this package tests it
27
+
28
+ Layer 1 and layer 2 used to be one codebase, where the boundary was enforced by construction. Now that both ship as packages, the design's own words are that it *"stops being enforced by construction and becomes discipline — layer 2 composes layer 1 through its public surface and never reaches past it."*
29
+
30
+ Discipline that nothing checks is a comment, so it is checked in two places:
31
+
32
+ - **The seam** — an ESLint rule (`SIBLING_SUBPATHS` in the repository's `eslint.config.mjs`) refuses any import of a sibling `@issuegraph/*` package that is not its **bare specifier**, and `require()` is banned outright because it is the one call that walks past every import rule in that file. Reaching into `@issuegraph/viewer/src/…` fails lint rather than a code review that might not happen. If something is needed that a sibling does not export, the answer is to export it deliberately: a published package can add an export later and can never take one back.
33
+ - **The purity claim** — the same config bans the reaches that would break "no fetching, no auth, no persistence" — `fetch`, storage, `document.cookie`, `eval`, dynamic `import`, and any `node:` builtin — for this package and the viewer, and for neither of the packages that legitimately read files. `src/purity.test.ts` keeps the half no static rule can do: it loads every shipped module with the browser globals removed, which catches a computed access like `globalThis['fet' + 'ch']`.
34
+
35
+ Both are proved rather than asserted: `scripts/eslint-rules.test.mjs` runs each rule against source written to break it and asserts the exact rule id, with controls in the other direction — ordinary code reports nothing, and a non-rendering package may still read a file.
36
+
37
+ ## The one declared crossing
38
+
39
+ The `together-with` **connector** lives in the viewer, not here.
40
+
41
+ A `together-with` edge has to be individually selectable, retypeable and deletable, and an enclosure has no edge to click — so the connector is a hit target, and only the layer that computes the layout knows where its endpoints are. Adding it from out here would mean re-deriving positions layer 1 already has, which is the drifting second implementation the package split exists to avoid.
42
+
43
+ It is written down as a **declared** crossing rather than discovered later. Treat it as the precedent for declaring a crossing, never as permission for more.
44
+
45
+ ## The scale ladder
46
+
47
+ The first surface to land, and the one that explains what layer 2 is for.
48
+
49
+ The canvas is a **local** instrument — it answers "what surrounds this issue" — while the order list is complete at any size. So past its budget the canvas **refuses rather than degrades**: a refusal with a route forward reads as competence; a hairball reads as a bug.
50
+
51
+ | Nodes | Behaviour |
52
+ |---|---|
53
+ | ≤ `GRAPH_NODE_BUDGET` | draw the neighbourhood |
54
+ | up to `CLUSTER_ONLY_BUDGET` | component **capsules** — size, `blocked-by` count, chain depth, cycle flag |
55
+ | beyond it | capsules truncated, and **search leads** |
56
+
57
+ Both thresholds are the viewer's own exports, read rather than restated, so the ladder and the canvas cannot disagree about what "past budget" means.
58
+
59
+ ```ts
60
+ import { renderScaleLadder, scaleReducer, INITIAL_SCALE_STATE } from '@issuegraph/editor';
61
+
62
+ let state = INITIAL_SCALE_STATE;
63
+ let { markup, styles, ladder } = renderScaleLadder(document, { state });
64
+
65
+ // Every control publishes what it does: `data-ig-command`, plus `data-ig-target`
66
+ // for a focus. Read them, reduce, render again.
67
+ state = scaleReducer(state, { kind: 'focus', key: ladder.capsules[0].lead });
68
+ ```
69
+
70
+ **Why this is here and not in the viewer.** Layer 1 already decides the same three tiers and draws a refusal — deliberately an *informational* one, because that package does not narrow: it renders exactly what it is given, so a control it published could never finish the action it advertised. Narrowing is the host's, and this is the host. What lands here is only the half layer 1 refused to own: a component the reader can choose, a search that reaches one, and a chip that opens the issues the canvas leaves out.
71
+
72
+ **"Isolated" here means edge-free**, which is *not* `NormalizedDocument.isolated` — that field means "in no slot **and** on no edge", and in a grooming view every issue holds an order position, so it is empty however many relationship-free issues the backlog has. Isolated issues are the majority (248 of 312 in the design's own sample) and are excluded from the canvas by default; the chip states the count, because the count is the information they carry, and opens them **as a list**.
73
+
74
+ **`ladder.canvas` is the canvas zone's document, never the rail's.** Narrowing the canvas is not narrowing the order. The complete order rail is rendered from the whole document by the workspace that assembles the zones.
75
+
76
+ ## The ambient audit
77
+
78
+ Four findings about an *encoding*, as a pure detector plus a surface that never nags.
79
+
80
+ | Finding | Severity | Note |
81
+ |---|---|---|
82
+ | **cycle** | `blocks-work` | the only one that stops work outright — no member can ever be ready |
83
+ | **dead duplicate ref** | `dangerous` | excluded from the order while nothing tracks its work: looks handled, isn't |
84
+ | **encoding refused** | `blocks-own-edges` | its edges are incomplete until it parses, and it reads as merely unencoded |
85
+ | **stale blocker** | `misleading` | a closed blocker already satisfies readiness; clearing is bookkeeping |
86
+
87
+ Severity, and the "keep as history" affordance the last row alone carries, are **data on a frozen class table** — so no render site picks either, and a fifth class is a compile error until the table says what it costs.
88
+
89
+ ```ts
90
+ import { auditOverlay, renderAuditHeader, auditRowAttributes } from '@issuegraph/editor';
91
+
92
+ const overlay = auditOverlay({ document, graph, encodingRefused });
93
+
94
+ renderAuditHeader(overlay); // the persistent count and its filter toggle
95
+ auditRowAttributes(overlay, ref); // {} for a clean row; the severity mark for a flagged one
96
+ ```
97
+
98
+ `auditDocument` is exported too, for a host that wants the findings without a surface. **`auditOverlay` runs the audit rather than accepting one**, deliberately: taking a finding list made this a public boundary for values the compiler never checked, and every field, invariant and mutability escape then had to be defended one at a time. A host that persisted findings re-audits to draw them — which is the right way round anyway, since the audit is pure and cheap and a persisted finding may not describe the document being drawn.
99
+
100
+ **Two of the four rest on a reader, and it is a required port.** `graph` carries `Model.cycles` and `Model.duplicateCanonical` straight off `buildModel`, in the *store's* own reference spelling — the host builds the model, so the host owns the translation between an opaque store reference and a normalised model key. It is required rather than optional because a host with no reader must not quietly receive a thinner audit and read it as a complete one.
101
+
102
+ **It is the reader's answer specifically, not the write guard.** `@issuegraph/derive`'s `wouldCycleOnBlockedBy` is a *pre-write* refusal, and its divergences all lean fail-safe for a write that is about to happen: it spans closed nodes, and it does not exempt a together unit's internal `blocked-by` edges. Over-refusing is the recoverable direction before a write and simply a false finding in an audit — §6.6 says internal edges *"stay advisory … they would make every group carrying its own ordering read as stuck"*. Reading the guard as an edge-on-cycle test flags every ordinary together group that carries its own ordering.
103
+
104
+ **Duplicate resolution is transitive, and both classes need it.** With `a duplicate-of b`, `b duplicate-of c` and `c` closed, the reader excludes *both* `a` and `b`, so both references are dead — and testing each edge's immediate target reports `b` while missing `a`, because `b` is open. §4.3.3 also reads a `blocked-by` naming a duplicate as naming its **canonical**, so the same resolution decides a stale blocker.
105
+
106
+ **A finding about a closed issue is finished history.** Two of the four classes name a harm that needs an open subject — a dead duplicate ref claims work is tracked nowhere, and a stale blocker claims readiness is satisfied — so on a closed one they would report the ordinary end of a lifecycle as a defect, permanently. The other two have no such precondition and deliberately keep none: §6.6 already restricts cycles to open nodes, and an encoding refusal is a fact about a *declaration*, which the model reads from closed nodes too.
107
+
108
+ **A refused declaration is not a discharged blocker, and a partial parse is not an absent one.** Two shortfalls that pull the same way. The reader keeps a dependent unready when the thing its edge resolved to was under-read — the declaration it could not read may carry a `duplicate-of` redirecting that edge at an **open** canonical — so a closed-but-refused target is excluded from the stale-blocker class rather than presented as dischargeable bookkeeping. And a dropped *field* returns non-null data carrying the surviving relationships, so the refusal says the edges are **incomplete and untrusted**, never that there are none.
109
+
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
+
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.
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.
115
+
116
+ ## The three equivalent create paths
117
+
118
+ §17b asks for three ways to create an edge — **canvas** (drag to a target, picker at the drop point), **inspector** (`+ add` → type → issue search) and **keyboard** (`R` → `1`–`5` → search → `⏎`) — and is explicit that they are *equivalent*, not a primary path with two shortcuts. That matters at size rather than in principle: the canvas is a **local** instrument, so at any real backlog most targets are off it, and the inspector is *the only path* to those. A design where drag is the real path stops working at the size it was built for.
119
+
120
+ **Equivalence is a property of the shape here, not a promise a test keeps.** The three gather the same three facts in different orders:
121
+
122
+ ```
123
+ canvas source → target → kind
124
+ inspector source → kind → target
125
+ keyboard source → kind → target
126
+ ```
127
+
128
+ So the draft is modelled as a **set of slots, not a sequence of steps** — each filled by its own command, in any order, with the `create` proposal emitted on whichever transition completes the set. There is exactly one emitter, and none of the three paths is named in the code at all.
129
+
130
+ ```ts
131
+ import { IDLE_CREATE_DRAFT, createReducer, keyIntent, pickerPlacement } from '@issuegraph/editor';
132
+
133
+ let { draft, proposal } = createReducer(IDLE_CREATE_DRAFT, { kind: 'begin', source: '530' });
134
+ ({ draft, proposal } = createReducer(draft, { kind: 'type', edgeKind: 'blocked-by' }));
135
+ ({ draft, proposal } = createReducer(draft, { kind: 'target', ref: '602' }));
136
+ // proposal → { op: 'create', kind: 'blocked-by', from: '530', to: '602' }
137
+ ```
138
+
139
+ **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
+
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.
142
+
143
+ **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
+
145
+ ### `none` means someone else owns this press
146
+
147
+ The map's whole contract, and the thing to get right when wiring it. A host's handler is "reduce a non-`none` intent, and `preventDefault()` it" — so every press the map claims wrongly is a keystroke stolen from its real owner:
148
+
149
+ ```ts
150
+ element.addEventListener('keydown', (event) => {
151
+ const intent = keyIntent(event, {
152
+ focused,
153
+ match,
154
+ selectedEdge,
155
+ // Which of the create flow's own interactions is the keyboard in?
156
+ // Only the shell can see this.
157
+ interaction: activeInteraction(), // 'canvas' | 'target-search' | 'elsewhere'
158
+ });
159
+ if (intent.kind === 'none') return; // someone else's key — let it through
160
+ event.preventDefault();
161
+ // …reduce the intent
162
+ });
163
+ ```
164
+
165
+ **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
+
167
+ So `CreateInteraction` enumerates **this design's own flow**, which §17b fixes at three states, and the host says which one it is in:
168
+
169
+ | state | what reaches the map |
170
+ |---|---|
171
+ | `canvas` | every binding |
172
+ | `target-search` | only `⏎` and `Escape` |
173
+ | `elsewhere` | nothing |
174
+
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.
176
+
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.
178
+
179
+ `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
+
181
+ **Three press-level facts stay on `KeyPress`**, and they are bounded in a way the widget list never was: all are fields on the event itself, and `KeyboardEvent`'s shape is fixed by the platform rather than by how many controls a host has. They answer **two** questions — who owns the press, and whether it is a fresh act at all.
182
+
183
+ *Who owns it:*
184
+
185
+ - **A modified chord.** `KeyPress` is structurally a subset of `KeyboardEvent`, so the event goes straight in — a bare key name cannot tell `R` from `Cmd+R`, and the handler above would hijack reload, new-tab and tab-selection. `Ctrl`, `Meta` and `Alt` answer `none` before the table is consulted. `Shift` is deliberately *not* among them: §17b names its bindings in capitals and `Shift+r` is how a keyboard reports `R`, so treating shift as a modifier would unbind the design itself.
186
+ - **`isComposing`.** While an input method is composing, `⏎` confirms the candidate and `Escape` cancels the composition. It cannot be folded into the table: the IME owns exactly the two bindings that *reach* the target search, which is also the only place composition happens.
187
+
188
+ *Whether it is a fresh act:*
189
+
190
+ - **`repeat`.** Every binding here is a one-shot command, so a held key is one decision however many events the OS repeat delay produces. Emitting a proposal per event breaks the one-act/one-`Proposal` contract the store is built on — and the store makes that visible rather than harmless: a pending delete keeps its edge drawn and selection is client state, so the queued proposals settle into `unknown-edge` records once the first lands. It is blanket rather than a per-binding flag because there is no repeatable binding here to distinguish — `R`, `1`–`5`, `⏎`, `⌫` and `T` are all discrete commands, none a continuous motion like an arrow key.
191
+
192
+ **`T` opens the picker; it does not emit a retype.** The proposals come from `pickerView`, which already owns them — a second emitter out here would be free to disagree about what a retype is.
193
+
194
+ **A `together-with` edge needs no special case.** The viewer gives its connector an *edge* identity precisely because an enclosure has no line to click, so by the time a selection arrives here it is an ordinary edge id and `⌫` and `T` work on it unchanged.
195
+
196
+ **Validity stays in the store.** These modules emit intent; `structuralRefusal` owns `self-edge`, `duplicate-edge` and `unknown-issue`. A second validity rule out here is exactly what `picker/view.ts` refused, and for the same reason.
197
+
198
+ ## The three-zone workspace
199
+
200
+ The assembly leaf: the rail on the left, the canvas in the centre, the inspector on the right, and the ambient audit count in the header. `renderWorkspace` composes each zone through the entry point that already owns it, so nothing below is re-derived here.
201
+
202
+ **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
+
204
+ **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
+
206
+ 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.
207
+
208
+ **A spacer at each end carries the height of the rows that were not drawn**, so the scroll container is as tall as the order rather than as tall as the window — otherwise native scrolling stops at the end of the first window and a host has no offset to turn into the next `start`. The pitch is the whole outer row box — `--ig-row-height` plus the slot's `--ig-space-tight` margin — because sizing on the height alone undercounts every omitted row by the gap and puts the tail of the order out of reach. What stays approximate is only variable row height: a row carrying holds is taller than a bare one, so the scrollbar is proportional rather than exact, and measuring that needs a mount this package does not have.
209
+
210
+ **The issues and edges are windowed alongside the slots**, down to exactly what the drawn rows need. Keeping the whole issue list is the obvious thing and it is wrong: the linear projection renders a count of the keys that appear in no slot and on no edge, so every edgeless issue outside the window was reported to the reader as *isolated* — the rail describing the reader's scroll position as though it were the document.
211
+
212
+ **Selection is one value, shared, never copied.** §17b makes `selected` the only edge state that also filters the inspector, so the workspace owns exactly one `WorkspaceSelection` and each zone reads it. It is a discriminated union rather than two nullable fields for a reason worth stating: `{ issue, edge }` can represent *both at once*, which is not a state this design has — every reader would need a rule for it, and the bug would surface as two zones disagreeing about what is selected rather than as a type error.
213
+
214
+ A selection naming a **member** of a `together-with` unit resolves to that unit's lead, because the unit is one row and `ViewerSlot.lead` is documented as the detail surface's subject — the projections canonicalize the same way before drawing, so all three zones name one issue for one selection.
215
+
216
+ An edge selection **filters** the relationship list rather than opening a different panel, so the reader's frame of reference never jumps. Clearing returns to *nothing selected* rather than to a wider list — `none` is a selection with no subject, so there is no list to widen to, and the control is named `clearSelection` for exactly that reason. It also resolves to no viewer key, because `selected` renders `aria-current` on a *node* and an edge is not one — the canvas reads it through `selectedEdge` instead, below.
217
+
218
+ The selection reaches the **canvas** too, through two additive options on `ScaleLadderOptions` — one per kind, because the union has two payloads and the viewer's `selected` can only answer for one of them. `selected` takes the issue key and becomes `aria-current` on a node; `selectedEdge` takes the edge identity and draws `OVERLAY_TREATMENTS.selected` — the halo that is *"the only state that is not about a write"* — on the line itself. Without them the canvas drew the selected subject as ordinary while the other zones marked it: the single selection disagreeing with itself between zones on every render, which is precisely what holding one value was supposed to make impossible.
219
+
220
+ **The ladder applies that overlay itself rather than publishing its scene.** Exposing the scene would leave two ways to obtain the canvas's markup — `result.markup`, and a re-render of the decorated scene — with the first silently wrong whenever an edge is selected. It ships `edgeOverlayStylesheet` alongside for the same reason: a canvas that can draw a class while its sheet is a caller's to remember is a mark that renders as nothing on the host that did everything else right.
221
+
222
+ **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.
223
+
224
+ **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`.
225
+
226
+ `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.
227
+
228
+ 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.
229
+
230
+ **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.
231
+
232
+ ## Status
233
+
234
+ The first-pass review queue lands as its own change. Wiring the published `data-ig-command` controls to real listeners remains a **mount's** job and therefore a host's: this package renders, and every control says what it does as data so the host can read it, reduce, and render again.
235
+
236
+ ## Licence
237
+
238
+ Apache-2.0